我不是来背函数手册的。MySQL 里那些常见函数,真正决定你是“会用”还是“用得好”的,往往不是语法本身,而是你知不知道它什么时候会坑你、什么时候能救命。这些年排查慢 SQL、做数据清洗、写统计报表,我发现自己真正高频用到、也最值得花心思去琢磨的,其实就是那么几类函数。这篇不打算按官方文档给你罗列一遍,而是挑出实战中最常踩坑、也最能提升效率的场景,配合执行计划、索引利用和真实的报错案例,一起拆开讲。
1. 函数用得不对,索引就废了:从执行计划说起
很多人在简历上写“熟悉 MySQL 常见函数”,结果第一道面试题就翻车——WHERE DATE(create_time) = '2024-01-01' 和 WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02',哪个更快?答案是后者,而且可能快出几个数量级。
原因在于函数作用于索引列之后,索引的有序性对优化器来说就失效了。DATE(create_time) 相当于对每一行都先做一次运算,再把结果拿去比较,B+Tree 的二分查找完全派不上用场,只能老老实实全表扫描。你可以用 EXPLAIN 看一眼,type 那栏从 ref 或 range 变成 ALL,rows 的估算值一下子飙上去,这就是函数拖垮索引的铁证。
再说一个更隐蔽的坑:隐式类型转换。MySQL 里如果比较的两边类型不一致,它会自动把字符转成数字,或者反过来。问题是,一旦索引列被隐式转换,和显式调用函数一样会导致索引失效。比如 phone 字段是 VARCHAR,你写 WHERE phone = 13800138000,MySQL 会把字符串列全部转成数字再比,索引就废了。类似的情况还有 status 是 INT,你写 WHERE status = '1',虽然结果可能没错,但某些版本下优化器也会做转换,扫描行数悄悄变多。
所以在写条件时记住一条原则:让索引列独立出现在比较符的一侧,别让它被函数或类型转换包裹。如果确实需要对时间字段做日期过滤,用范围条件替代函数;如果非要用函数,那就要接受全表扫描的现实,或者考虑改成生成列加索引的方案。
拿一个真实案例来说。之前有个订单表,数据量一千多万,运营每天要查当天的付款订单,SQL 写的是:
sql复制SELECT * FROM orders WHERE DATE(pay_time) = CURDATE();
这条 SQL 每次跑十几秒,把凌晨的定时任务都拖垮了。改成范围查询之后:
sql复制SELECT * FROM orders
WHERE pay_time >= CURDATE() AND pay_time < CURDATE() + INTERVAL 1 DAY;
执行时间直接掉到几十毫秒。为什么?因为 pay_time 上建的索引终于能用上了。这就是常见函数在 WHERE 里的第一个大坑——用不好,索引白建。
另外,LIKE 操作符配合前置通配符也一样,WHERE name LIKE '%张' 会让索引失效,因为字符串的匹配必须从开头才能走前缀索引。这个虽然不是函数,但背后的原理是相通的:你要让查询条件的形式能匹配上索引的排列顺序。
顺带提一下,有些函数是“无辜”的,比如在常量上调用 CURDATE()、NOW(),它们不影响索引列本身,可以放心用。真正要注意的,永远是函数包在列名外面的情况。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 聚合函数用不好,统计报表全是坑:GROUP BY 和 HAVING 的实战细节
聚合函数是统计报表的根基,但也是隐性 Bug 的重灾区。COUNT(*)、SUM()、AVG()、MAX()、MIN() 算是最常用的五个,看上去没什么技术含量,实际上细节多得很。
先说说 COUNT(*) 和 COUNT(1)、COUNT(字段) 的区别。很多老 DBA 会告诉你,COUNT(*) 和 COUNT(1) 没区别,InnoDB 引擎下 optimizer 都会优化成一样的执行计划。但 COUNT(字段) 就不同了,它只统计该字段非 NULL 的行数。如果你统计用户表中“填写了手机号的用户数”,用 COUNT(phone) 是合理的;但如果你以为 COUNT(phone) 等于总用户数,而 phone 字段允许 NULL,那结果就会少。所以计数之前,先想清楚你统计的语义是“行数”还是“非空值个数”。
另一个经典问题是 SUM() 遇到 NULL 的情况。MySQL 的聚合函数会自动忽略 NULL,但如果这一组数据全是 NULL,SUM() 返回的是 NULL 而不是 0。这个坑在出报表时尤其致命——JAVA 端用 BigDecimal 接收还好,如果用基本类型接收,直接抛空指针。我的习惯是外层套一个 IFNULL(SUM(amount), 0),既保证语义清晰,也防止程序端炸掉。
AVG() 也有类似问题,它本质上是 SUM() / COUNT(非空),NULL 不参与分母计算。如果你期望 NULL 按 0 参与平均,那要先 IFNULL 再 AVG,或者用 SUM/COUNT(*) 自己算。
GROUP BY 后面能不能跟别名?MySQL 的扩展语法允许,但 SQL 标准不允许,而且一旦用到函数别名(比如 GROUP BY 年份)在部分模式下会报错。为了兼容性和可读性,建议在 GROUP BY 里写原始表达式或列名,不要写别名。
HAVING 和 WHERE 的执行顺序也是高频考点。WHERE 在分组之前过滤行,HAVING 在分组之后过滤组。很多人用 WHERE SUM(amount) > 100 直接报错,就是因为聚合的结果在 WHERE 阶段还不存在。反过来,能用 WHERE 过滤掉的数据,尽量不要留到 HAVING 里,因为先缩小数据集再做聚合,性能会好很多。
举个例子,统计每个品类的销售额,只看金额超过一万的品类:
sql复制SELECT
category_id,
SUM(amount) AS total_amount
FROM orders
WHERE order_status = 'paid' -- 先用 WHERE 缩小范围
GROUP BY category_id
HAVING SUM(amount) > 10000; -- 再对聚合结果过滤
这个 SQL 的顺序是:先取已支付订单,再按品类分组求和,最后筛掉不达标的组。如果你把 order_status = 'paid' 写进 HAVING,虽然也能跑出正确结果,但 MySQL 需要把更多行带入分组阶段,白白浪费资源。
再说一个 GROUP BY 的隐藏问题:ONLY_FULL_GROUP_BY 模式。MySQL 5.7 之后默认开启这个模式,意思是你 SELECT 出来的非聚合列,必须出现在 GROUP BY 里。很多人从 5.6 升级到 5.7,以前跑得好好的 SQL 突然报错,就是因为这个。比如:
sql复制SELECT user_id, order_date, SUM(amount)
FROM orders
GROUP BY user_id;
在 5.6 下能跑,但取出的 order_date 是这一组里的任意一条,具有不确定性。5.7 之后直接报错,逼着你把 SQL 写严谨。我看到不少人在生产环境遇到这个问题,第一反应是想改 sql_mode,我劝你别这么干——默认严格模式是 MySQL 官方深思熟虑的,它保护的正是数据的确定性。真的要取“每个用户最近一次下单日期和金额”,老老实实写子查询或窗口函数。
聚合函数里还有一个容易被忽视的性能点:GROUP BY 的列如果能走索引,MySQL 不需要临时表和文件排序,执行计划的 Extra 字段里就不会出现 Using temporary 和 Using filesort。如果你的分组查询经常出现这两个关键字,就要检查一下索引设计是否匹配分组字段。
