资讯动态

MySQL常用内置函数详解:日期、字符串、数学与流程控制实战

发布时间:2026/10/22 16:18:02 来源:尧图企业网站定制
写SQL写多了你就会发现真正决定你效率高低的很多时候不是多复杂的查询逻辑而是你对内置函数熟不熟。就拿日期来说业务表里存的是2024-03-15 14:30:22这种完整时间戳可老板要看的报表按天汇总就行你怎么办DATE_FORMAT()一行搞定。再比如用户注册手机号有的人填了86前缀有的人填了空格你要做用户画像去重字符串函数不熟就得在应用层写一堆Java代码来清洗。所以我一直跟组里的新人说MySQL内置函数这部分看着基础其实是性价比最高的投入。这篇就把最常用的日期函数、字符串函数、数学函数还有一些容易被忽略但关键时刻能救命的相关函数流程控制、类型转换这类一次性梳理清楚。每个函数我都会结合实际的业务场景来讲语法是什么、返回值是什么、什么时候用、有什么坑尽量讲透。1. 内容整体设计与思路拆解1.1 为什么内置函数这么重要先说个我自己的体会。有段时间我在做一套数据迁移的活源库和目标库的表结构对不上字段类型、时间格式、字符串编码全都不一样。如果不会灵活运用内置函数你会陷入两种尴尬境地一是写一大堆代码在应用层做转换慢不说还容易出Bug二是在SQL里硬拼字符串写出来的东西又长又丑自己都不想看第二次。内置函数存在的意义就是把“数据变换”这件事下沉到数据库层面解决。你想啊数据从表里取出来是什么样往往不是我们最终想要的样。需要格式化、需要截断、需要计算差值、需要判空、需要类型转换这些操作如果每回都在代码里写每换一种场景就要重新写一遍但是用函数一行SQL就能处理干净。另一个重要原因在于性能。数据库内置函数是经过高度优化的C/C实现对比在应用层逐行做字符串操作效率差距不是一点半点。尤其在处理几百万行数据的报表任务时合理使用函数配合索引查询速度完全可以接受反之你在应用层循环处理内存和时间的开销都很可观。1.2 从四类函数看MySQL的设计思路MySQL把常用函数大致分成四大类正好对应业务里最常见的数据处理需求时间处理、文本处理、数值计算以及杂项补充判断、转换、加密、IP处理等。这个分类不是随便分的它基本覆盖了一个标准业务系统里面会遇到的90%以上的数据变换场景。拿日期函数来说几乎所有业务表里都有创建时间、更新时间、支付时间之类的字段。日期函数要解决的无非三类问题怎么取出当前时间、怎么格式化指定时间、怎么计算时间差。理解了这三个需求日期函数就学完了大半。字符串函数解决的是另一类问题数据不干净、格式不统一、需要截取拼接。比如手机号脱敏、地址拼接、编号前缀补零这些都是字符串函数的经典应用场景。数学函数相对简单但也不能忽视。分页计算、金额四舍五入、取随机数做数据抽样都会用到。至于“其他函数”像IF、CASE WHEN、IFNULL、CAST这类它们不直接处理某个类型的数据但是在写复杂查询时它们往往是让SQL“活起来”的关键。2. 日期函数所有业务报表的基石2.1 拿下当前时间NOW、CURDATE、CURTIME、SYSDATE日期函数里我日常用得最多的是NOW()。它返回当前日期和时间格式是YYYY-MM-DD HH:MM:SS。实际开发里给表插入一条记录时用NOW()填充created_at字段比在应用层传时间更可靠因为可以保证数据库时间的一致性。-- 返回当前日期时间2025-06-11 14:23:45 SELECT NOW(); -- 返回当前日期2025-06-11 SELECT CURDATE(); -- 返回当前时间14:23:45 SELECT CURTIME();这里多提一句SYSDATE()。从结果上看它和NOW()一样都是返回当前日期时间但二者在SQL执行过程中的行为有细微差别NOW()是语句开始执行时就确定了时间而SYSDATE()是在函数真正被执行到的那一时刻才取当前时间。在一条SQL里多次调用NOW()值是一样的多次调用SYSDATE()理论上值会有微秒级差异。所以如果你在写一条需要时间一致的复杂查询或者做时间比较优先用NOW()避免因为时间取值点不同导致计算结果出现不可复现的偏差。2.2 格式化与解析DATE_FORMAT与STR_TO_DATEDATE_FORMAT()是日期函数里出镜率最高的没有之一。它把一个日期/时间值按照你指定的格式转成字符串。比如-- 输出2025/06/11 SELECT DATE_FORMAT(NOW(), %Y/%m/%d); -- 输出06-11-2025 SELECT DATE_FORMAT(NOW(), %m-%d-%Y); -- 输出2025年6月11日 14时23分 SELECT DATE_FORMAT(NOW(), %Y年%m月%d日 %H时%i分);常用的格式符号我整理了下建议贴在手边随时查格式符含义示例%Y四位年份2025%y两位年份25%m两位月份06%c月份无前导零6%d两位日11%e日无前导零11%H24小时制时14%i分钟23%s秒45%W星期名Wednesday%a缩写星期名Wed%M月份英文名June有格式化就有反操作。STR_TO_DATE()把字符串按指定格式解析成日期这个函数在处理外部导入的数据时是神器。比如从Excel导入的日期是2025/06/11这样的文本MySQL默认不认这个格式直接用WHERE date_col 2025/06/11可能会得到错误结果因为字符串比较和日期比较的规则不一样。正确做法是先用STR_TO_DATE()转换-- 把文本解析成日期 SELECT STR_TO_DATE(2025/06/11, %Y/%m/%d); -- 实际查询中过滤指定日期之后的数据 SELECT * FROM orders WHERE order_date STR_TO_DATE(2025/06/11, %Y/%m/%d);注意STR_TO_DATE()解析失败会返回NULL同时整个查询不会报错。这既是优点也是坑——如果数据里混了脏文本解析出来是NULL条件判断会静默过滤掉这些行。排查数据缺失时别忘了先确认是不是解析返回了NULL。2.3 日期计算DATE_ADD、DATE_SUB与DATEDIFF业务里经常要算“30天前的日期”“本周一的日期”“两笔订单间隔多少天”。这三个场景分别对应DATE_ADD、DATE_SUB和DATEDIFF。DATE_ADD()和DATE_SUB()就是在指定日期上增加或减少一个时间间隔间隔可以是天、小时、分钟甚至微秒-- 当前时间加7天 SELECT DATE_ADD(NOW(), INTERVAL 7 DAY); -- 当前时间减3小时 SELECT DATE_SUB(NOW(), INTERVAL 3 HOUR); -- 查询近30天的订单注意now()时间部分的影响 SELECT * FROM orders WHERE order_date DATE_SUB(NOW(), INTERVAL 30 DAY);这里有个细节我栽过跟头DATE_SUB(NOW(), INTERVAL 30 DAY)返回的不只是日期而是带时间部分的完整时间戳。如果你和order_date可能是DATE类型比较由于隐式类型转换的存在order_date 2025-05-12 14:23:45等价于order_date 2025-05-12 00:00:00逻辑上没问题。但反过来如果你拿一个精确到小时的查询条件去和DATE类型比较就可能出现边界值的漏查。我习惯的做法是比较日期类型字段时直接用DATE_SUB(CURDATE(), INTERVAL 30 DAY)把时间部分去掉减少边界混乱。DATEDIFF()返回两个日期相差的天数注意它只算天数忽略时间部分-- 返回 1因为它只按日期算 SELECT DATEDIFF(2025-06-12 10:00:00, 2025-06-11 23:59:59);这就意味着如果你要精确计算两时间点之间相差多少小时、多少分钟DATEDIFF帮不上忙得用TIMESTAMPDIFF()。这个函数可以指定单位非常灵活-- 返回 11小时 SELECT TIMESTAMPDIFF(HOUR, 2025-06-11 23:00:00, 2025-06-12 10:00:00); -- 返回 660分钟 SELECT TIMESTAMPDIFF(MINUTE, 2025-06-11 23:00:00, 2025-06-12 10:00:00);TIMESTAMPDIFF()还有一个好处它的第一个参数是单位支持SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、QUARTER、YEAR自动帮你处理了月份天数不一样的问题。比如计算两个日期相差几个月如果手写除以30会有很大误差用TIMESTAMPDIFF(MONTH, ...)直接就是准确的月份差。日期函数还有一个聚合统计里很实用的用法按时间分组。比如统计每天订单量很多新人会直接GROUP BY order_date但如果order_date是DATETIME类型那每一个不同的时间戳都会被分出来一组统计结果会碎成一地。正确做法是先格式化成天再分组-- 按天统计订单金额 SELECT DATE_FORMAT(order_date, %Y-%m-%d) AS day, SUM(amount) AS total FROM orders GROUP BY DATE_FORMAT(order_date, %Y-%m-%d) ORDER BY day;不要担心这里用了函数分组会导致索引失效的问题在数据量可控的情况下这种统计查询的性能是可以接受的。后面我会专门讲函数与索引效率的权衡。3. 字符串函数处理“不干净”的数据3.1 拼接与截取CONCAT、SUBSTRING、LEFT、RIGHT字符串函数里最基础的是拼接和截取。CONCAT()把多个字符串拼接成一个字符串但如果任何一个参数是NULL结果就是NULL。这个特性写之前要铭记于心否则查出来一大批NULL就直接懵了。-- 输出张三-北京 SELECT CONCAT(张三, -, 北京); -- 输出NULL因为其中一个参数是NULL SELECT CONCAT(张三, NULL, 北京);那想要忽略NULL继续拼接怎么办用CONCAT_WS()第一个参数是分隔符后续参数里的NULL会被自动跳过-- 输出张三-北京而不是NULL SELECT CONCAT_WS(-, 张三, NULL, 北京);截取函数里LEFT()和RIGHT()分别从左侧和右侧取指定个数的字符SUBSTRING()则更灵活可以从任意位置开始取甚至支持负数表示从末尾倒数。-- 取手机号前3位138 SELECT LEFT(13812345678, 3); -- 取手机号后4位5678 SELECT RIGHT(13812345678, 4); -- 从第4位开始取3位123 SELECT SUBSTRING(13812345678, 4, 3); -- 从倒数第4位开始取4567 SELECT SUBSTRING(13812345678, -4, 4);这些函数最经典的场景是数据脱敏。比如外部合作需要导出用户手机号不能暴露完整号码直接一条SQL替换完事-- 手机号脱敏138****5678 SELECT CONCAT(LEFT(phone, 3), ****, RIGHT(phone, 4)) AS masked_phone FROM users;3.2 替换与清理REPLACE、TRIM、LTRIM、RTRIM脏数据的另一个典型问题是字符串前后有多余的空格或者中间有不该出现的字符。TRIM()系列函数就是干这个的。TRIM()去除首尾空格LTRIM()只去左边RTRIM()只去右边。-- 输出hello没有引号 SELECT TRIM( hello ); SELECT LTRIM( hello ); SELECT RTRIM( hello );REPLACE()则是把字符串里的指定子串全部替换成新串。这个函数在清洗数据时非常好用。比如用户填的地址里混了繁体字或者产品名称里混了废弃的旧名统一替换掉-- 把北京市替换成北京 SELECT REPLACE(北京市朝阳区, 北京市, 北京); -- 输出北京朝阳区实际工作中我做地址清洗时经常一连串嵌套多个REPLACE()。MySQL是允许函数嵌套的比如-- 多重清洗 SELECT REPLACE(REPLACE(TRIM(address), 北京市, 北京), , ) FROM user_address;这里顺便把全角空格的问题也处理了。多少人在做数据比对时发现明明肉眼一样的数据就是不相等查了半天才发现一边是全角空格一边是半角空格。这类问题用REPLACE把全角空格替换成空字符串或半角空格就能快速解决。3.3 大小写与长度UPPER、LOWER、CHAR_LENGTHUPPER()、LOWER()简单粗暴字母转大写/小写常用于标准化用户输入的邮箱、用户名等。CHAR_LENGTH()和LENGTH()的区别值得说明一下前者返回字符数后者返回字节数。在utf8mb4编码下一个汉字占3个字节LENGTH(数据)返回6CHAR_LENGTH(数据)返回2。这个区别在业务上的影响很大。比如你要验证用户输入的手机号是不是11位如果用LENGTH()遇到里面混了全角数字或特殊字符判断就会出错。一般业务规则里说“输入长度为11位”基本都应使用CHAR_LENGTH()。同理做字段长度校验、统计字符串长度时要明确业务规则到底是按字符还是按字节。大小写转换还有个常用的连带用法做不区分大小写的匹配时与其在查询条件里两边都转UPPER不如直接在应用层把参数转好再传进来查询端只写WHERE UPPER(username) ADMIN。不过这种写法会导致索引失效后面在常见问题里会细聊。3.4 字符串查找LOCATE、INSTR字符串处理里查找是刚需。最常见的需求是“这个字段里是否包含某个子串”。MySQL里有LOCATE()、INSTR()、POSITION()几个函数可以干这个事。它们的区别很小返回子串第一次出现的位置从1开始找不到时返回0。-- 输出2“a”第一次出现在第2个字符处 SELECT LOCATE(a, banana); -- 输出0表示未找到 SELECT LOCATE(x, banana); -- 输出2 SELECT INSTR(banana, a);利用返回值是否为0可以在WHERE里直接做包含判断-- 找出email为qq邮箱的用户 SELECT * FROM users WHERE LOCATE(qq.com, email) 0;用LOCATE而不是LIKE有一个好处当你知道子串的位置可以顺势和SUBSTRING配合做更精细的截取。比如要从邮箱里提取域名部分-- 从邮箱中截取之后的部分即域名 SELECT SUBSTRING(email, LOCATE(, email) 1) AS domain FROM users;这类“字符串定位截取”的组合动作写多了以后你会形成肌肉记忆看到需要解析字符串的业务需求本能就能拆解成查找、截取、替换三步。4. 数学函数数值计算与统计的幕后功臣4.1 四舍五入与截断ROUND、TRUNCATE、CEIL、FLOOR数学函数里最常用的是ROUND()它用来四舍五入可以指定小数位数。注意ROUND(x, d)的规则d为保留的小数位数如果d为负数则对整数部分进行舍入到十位、百位。-- 输出3.14 SELECT ROUND(3.14159, 2); -- 输出3.14 SELECT ROUND(3.145, 2); -- 输出3100对十位四舍五入 SELECT ROUND(3145.67, -2);第二个例子可能有人会疑惑3.145保留两位不应该是3.15吗实际上由于浮点数的二进制表示问题3.145在计算机里可能是3.144999...四舍五入就成了3.14。这个坑做财务计算时比较危险如果对精度有严格要求建议使用DECIMAL类型而不是FLOAT/DOUBLE。TRUNCATE(x, d)则直接截断不做四舍五入。-- 输出3.14直接截断 SELECT TRUNCATE(3.149, 2);CEIL()向上取整FLOOR()向下取整。这两个函数在分页、库存分配、容量估算里特别有用。比如列表总记录数是total每页显示page_size条总分页数就是CEIL(total / page_size)。-- 如果有101条记录每页20条一共6页 -- 输出6 SELECT CEIL(101 / 20);做数据分桶、报表区间统计时也是绝配。比如把用户按年龄分桶-- 按年龄每10岁一组统计数量 SELECT CONCAT(FLOOR(age / 10) * 10, -, FLOOR(age / 10) * 10 9) AS age_range, COUNT(*) AS cnt FROM users GROUP BY FLOOR(age / 10);4.2 取余与随机MOD、RANDMOD(x, y)返回x除以y的余数。最常见的使用场景是奇偶判断、分库分表的取模分片、或者按某种规则轮流分配。-- 输出1 SELECT MOD(7, 3); -- 取用户ID末位判断奇偶 SELECT id, IF(MOD(id, 2) 0, 偶数用户, 奇数用户) AS parity FROM users;RAND()返回一个0到1之间的随机浮点数包含0不包含1。可以用它来随机排序、随机抽样-- 随机取10条记录 SELECT * FROM users ORDER BY RAND() LIMIT 10;这里要提醒一句ORDER BY RAND()在大表上性能极差因为它需要对全表每行生成随机数再排序几乎等于全表扫描加全量排序。如果表有几百万行这条查询能把数据库拖垮。我在实践里替代方案是先查出一个ID范围再用随机偏移取行。-- 高效随机取一条先取出一个随机偏移量 SELECT * FROM users WHERE id ( SELECT FLOOR(RAND() * (SELECT MAX(id) FROM users)) ) ORDER BY id LIMIT 1;这种写法能利用主键索引性能会好很多。当然如果ID不连续有删除过的数据可能需要配合循环多查几次或者退而求其次在应用层处理。4.3 绝对值、开方与最值ABS、SQRT、LEAST、GREATESTABS()绝对值、SQRT()平方根使用场景相对直接。LEAST()和GREATEST()值得多说一句它们在一组值中返回最小/最大可以接受多个参数不只是两列。-- 返回2 SELECT LEAST(2, 5, 8, 10); -- 返回10 SELECT GREATEST(2, 5, 8, 10);我在实际业务中常拿GREATEST做下限控制。比如用户提现金额不能低于1元但可能计算出来的手续费减免后的金额小于1这时候直接用GREATEST(amount, 1)保证最终结果不小于1省得在应用层再判一次。数学函数还有一个组合使用场景就是聚合统计里的百分比计算。比如计算某个分类订单的金额占比-- 计算并保留两位小数 SELECT category, ROUND(SUM(amount) * 100 / (SELECT SUM(amount) FROM orders), 2) AS pct FROM orders GROUP BY category;这里的ROUND保精度*100转百分比两个数学函数配合使用报表里常见的一列就出来了。5. 其他相关函数常被忽略但关键时刻救命5.1 流程控制IF、IFNULL、NULLIF、CASE WHEN说实话如果只能从“其他函数”里挑一组学我一定先挑流程控制这一组因为它们在复杂业务查询里太常用了而且很多新人不知道怎么在SQL里写“如果...就...”。IF(expr, v1, v2)是最简单的条件判断如果expr为真返回v1否则返回v2。-- 根据库存判断状态 SELECT product_name, IF(stock 0, 在售, 售罄) AS status FROM products;IFNULL(v1, v2)则是专门处理NULL的如果v1不是NULL就返回v1如果v1是NULL就返回v2。这个函数在处理聚合结果时简直救大命。比如用LEFT JOIN关联查询时右表没有匹配记录相关字段就是NULL直接用IFNULL兜底填充默认值-- 没有订单的用户订单数显示0 SELECT u.username, IFNULL(COUNT(o.id), 0) AS order_count FROM users u LEFT JOIN orders o ON u.id o.user_id GROUP BY u.id;NULLIF(v1, v2)则是反过来的如果v1等于v2返回NULL否则返回v1。这个函数有一些特殊的用法。比如你要防止除数为0的报错-- 用NULLIF规避除零错误 SELECT amount / NULLIF(quantity, 0) AS unit_price FROM order_items;quantity如果是0NULLIF(quantity, 0)返回NULL整个表达式的结果就是NULL而非直接报错。虽然NULL也要在应用层处理但至少SQL不会中断执行处理起来要从容得多。比IF更强大的是CASE WHEN它可以实现多分支判断。这是复杂SQL的顶梁柱比如做订单分档统计-- 按订单金额分档汇总 SELECT CASE WHEN amount 100 THEN 小额订单 WHEN amount 1000 THEN 中额订单 ELSE 大额订单 END AS order_tier, COUNT(*) AS total_cnt FROM orders GROUP BY order_tier;CASE WHEN的灵活之处在于它既可以在SELECT里做字段加工也可以在WHERE里做过滤条件甚至在ORDER BY里做自定义排序。比如你想让某个状态的记录排在最前面-- 优先显示“待处理”状态的记录 SELECT * FROM tasks ORDER BY CASE status WHEN pending THEN 0 ELSE 1 END, created_at DESC;5.2 类型转换与信息函数CAST、CONVERT、COALESCE、VERSIONCAST(expr AS type)和CONVERT(expr, type)都是类型转换把值从一种类型转成另一种。很多所谓的“诡异报错”本质上都是类型隐式转换导致的。比如字符串和数字比较时MySQL会把字符串转成数字再比较如果字符串不是合法数字转换结果是0于是WHERE phone 0可能把所有非数字开头的记录都查出来这就是典型的隐式转换坑。CAST最常用的场景是拼接字符串时把数字转成字符串或者把字符串转成日期做比较-- 数字转字符串拼接 SELECT CONCAT(订单编号, CAST(id AS CHAR)) FROM orders; -- 字符串转日期比较 SELECT * FROM orders WHERE order_date CAST(2025-06-01 AS DATE);COALESCE(v1, v2, ...)是一个多参版本的空值处理函数它返回参数列表中第一个非NULL的值。这个函数比IFNULL更通用因为可以接受多个参数。比如配置表里三级优先级取第一个非空配置生效-- 优先取手机号其次邮箱最后取默认值 SELECT COALESCE(phone, email, 无联系方式) AS contact FROM users;信息函数里DATABASE()返回当前数据库名、USER()返回当前用户、VERSION()返回MySQL版本。这些函数大多用在运维脚本、监控SQL里平时写业务代码用得少但做数据库巡检、迁移脚本时偶尔能帮上忙。5.3 加密与哈希类函数MD5、SHA2再说一组即使现在用得不多也值得了解一下的MD5()、SHA1()、SHA2()。它们对传入的字符串做哈希输出定长的十六进制摘要。过去很多系统用MD5()存用户密码现在不推荐这样做了——MD5已不再安全容易碰撞破解。但它们在数据一致性校验、去重场景里还是很有用。比如要判断两条文本记录是否内容完全相同包括空格、换行直接比对大文本字段效率低可以先算哈希再比对-- 计算文本哈希便于比对 SELECT MD5(CONCAT(column1, column2)) AS content_hash FROM some_table;要注意哈希类函数是对输入串做计算输入值相同哈希才相同一旦输入值有细微差异比如多了一个空格哈希值就完全不一样。所以用它做一致性校验时前提是输入串已经被清洗规整过。6. 综合实战一条SQL搞定复杂报表需求6.1 从需求到SQL的拆解过程前面讲了那么多函数单个都明白但实际业务往往是组合拳。我拿一个真实做过的需求举例完整过一遍拆解思路。业务方提的需求是这样的运营要一份近30天注册用户的统计报表字段包括用户ID、注册日期、手机号脱敏后显示、年龄分组、是否活跃按最近7天是否有登录记录并且按注册日期和年龄分组汇总用户数。把需求拆解成函数需求近30天注册日期比较用DATE_SUB(CURDATE(), INTERVAL 30 DAY)。注册日期格式化DATE_FORMAT(register_time, %Y-%m-%d)。手机号脱敏CONCAT(LEFT(phone, 3), ****, RIGHT(phone, 4))。年龄分组CONCAT(FLOOR(age/10)*10, -, FLOOR(age/10)*109)。是否活跃用IF配合EXISTS子查询判断最近7天是否有登录记录。分组汇总GROUP BY配合COUNT、SUM。完整SQL写出来大致是这样SELECT DATE_FORMAT(u.register_time, %Y-%m-%d) AS register_day, CONCAT(FLOOR(u.age / 10) * 10, -, FLOOR(u.age / 10) * 10 9) AS age_group, COUNT(DISTINCT u.id) AS reg_user_cnt, SUM(IF(EXISTS( SELECT 1 FROM login_log l WHERE l.user_id u.id AND l.login_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) ), 1, 0)) AS active_user_cnt, ROUND( SUM(IF(EXISTS( SELECT 1 FROM login_log l WHERE l.user_id u.id AND l.login_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) ), 1, 0)) * 100.0 / COUNT(DISTINCT u.id), 2 ) AS active_rate FROM users u WHERE u.register_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY register_day, age_group ORDER BY register_day DESC, age_group;6.2 组合拳的注意点这条SQL里用到了日期函数计算时间范围、格式化日期、字符串函数拼接年龄组、数学函数计算百分比、四舍五入、流程控制函数IF判断活跃、逻辑判断EXISTS子查询。每个函数单独拎出来都简单但放在一起就是一个可交付运营的报表。有几处需要说明第一COUNT(DISTINCT u.id)用来去重统计防止用户因为多条登录记录或关联产生重复计数。在涉及多表关联时去重统计是个高频的坑。第二SUM(IF(...))是“条件计数”的经典写法。逻辑是满足条件记为1不满足记为0求和就是满足条件的数量。这和COUNT(IF(...))的语义有细微差别——SUM(IF())是把所有行都算进求和的桶里COUNT()只计数非NULL的行。实践里我更喜欢用SUM(IF())做条件计数逻辑更直观。第三ROUND放在最后对比例做精度控制避免在中间步骤舍入导致后续计算的累积误差。第四也是很重要的一点这种SQL在数据量较大时EXISTS子查询的性能需要关注。如果login_log表数据量很大建议在user_id和login_time上建联合索引否则一个近30天用户报表可能把数据库拖得很慢。这也是我在实际项目里反复强调的函数用得溜还不够SQL性能得靠索引、执行计划来兜底。7. 常见问题与排查技巧实录7.1 字符串比较时隐式类型转换的坑这是个老生常谈但总有人踩的坑。假设user_id是字符串类型存的是00123这样的值查询时写SELECT * FROM users WHERE user_id 123;MySQL会把字符串转成数字再比较00123转成数字就是123所以这条查询能查到数据。但问题来了如果表里还有user_id 123和user_id 0123它们都会匹配上。如果字段是主键问题还不大但如果查询条件来自外部参数就可能意外查出多条不同记录。反过来更危险-- email字段是字符串非数字字符串转数字后是0 SELECT * FROM users WHERE email 0;这条SQL会把所有email字段不是数字开头的记录都查出来比如abctest.com、testqq.com都会匹配因为转数字都是0。排查这类问题时第一反应就应该是看字段类型和比较值类型是不是一致必要时用CAST显式转换。7.2 日期函数导致索引失效函数作用于索引列会导致索引失效这是一个非常典型的性能问题。比如-- 在索引列date_col上用了DATE_FORMAT函数索引失效 SELECT * FROM orders WHERE DATE_FORMAT(order_date, %Y-%m-%d) 2025-06-11;正确的做法是在等号右侧函数化保持列本身的形态-- 索引可以被使用 SELECT * FROM orders WHERE order_date 2025-06-11 AND order_date 2025-06-12;同理WHERE YEAR(create_time) 2025也会导致索引失效。更好的方式是写成create_time 2025-01-01 AND create_time 2026-01-01。这不是说函数本身不行而是说要把函数从列那一侧挪走。理解这个原则很多“查询突然变慢”的问题都能自查。7.3 DATE_FORMAT的语义与区域设置DATE_FORMAT里如果用到%W星期名、%M月份名返回的文本取决于MySQL的lc_time_names系统变量。默认是en_US返回英文。如果你业务需要中文星期或月份名需要手动设置SET lc_time_names zh_CN; -- 之后再执行 SELECT DATE_FORMAT(NOW(), %Y年%m月%d日 %W);不过要注意lc_time_names是会话级变量如果通过连接池访问数据库下一条连接可能就重置了。需要全局生效就得在数据库配置里指定或者在每次查询前手动设置。这个坑虽然不常遇到但一旦遇到网上搜到的中文资料很少容易卡很久。7.4 NULL与空字符串是两码事我在做数据清洗时经常遇到一类问题同一列里既有NULL又有空字符串业务上可能都表示“没有填”但处理逻辑却不同。IFNULL只判断NULL对空字符串无效。-- 如果字段可能是NULL或空字符串用CASE WHEN判断 SELECT CASE WHEN phone IS NULL OR phone THEN 无手机号 ELSE phone END AS contact_phone FROM users;还有个相关坑COUNT(column)不会统计NULL值但会统计空字符串。所以COUNT(phone)的结果可能比COUNT(*)少因为有些行的phone是NULL。写统计SQL时先搞清楚字段里NULL的分布不然得出的数字会和业务对不上。7.5 RAND()排序的性能替代前面提过ORDER BY RAND()在数据量大时性能很差这里再给一个比较实用的替代方案。假设表的主键是自增ID想随机取N条-- 先用子查询取一个随机的ID起点 SELECT * FROM users WHERE id ( SELECT FLOOR(RAND() * (SELECT MAX(id) FROM users)) ) ORDER BY id LIMIT 10;如果表数据分布不均匀比如中间删过数据一次查询可能不足10条这时可以外层包一层循环多查几次补齐或者在应用层收集ID再查。总之原则是随机逻辑尽量用主键的范围扫描来驱动不要全表排序。8. 一点个人经验函数学多了以后我有一个深刻的体会写SQL和写代码一样先想清楚“我要什么”再选“怎么实现”。很多新人上来就问“有没有一个函数能直接把这个问题解决”但实际问题往往需要几个函数组合使用。多动手组合比背函数列表管用得多。另外一点建议是每个函数花几分钟手敲一遍看看它的返回值和边界行为尤其NULL、空串、负数、极大值这几个边界。你在测试环境跑一次胜过在文档里读十遍。我在带人时经常强调不要只看语法要自己在命令行里运行几次把函数的行为变成一种直觉查数据的时候条件反射就能想到该用什么函数。MySQL内置函数就这些吗当然不止。这篇挑的是日常业务场景里出现频率最高的那批。像LAST_DAY、PERIOD_ADD这类冷门函数等真遇到对应需求时再查也不迟。把常用函数练成肌肉记忆遇到冷门函数知道去哪查这才是最舒服的工作状态。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑