☰
MySQL函数避坑指南:分类、空值陷阱与索引优化
2026/10/22 5:56:12 网站建设 项目流程

前阵子接手一个老项目的统计报表,一条SQL跑了十秒,EXPLAIN一眼就看出问题:WHERE里写了DATE_FORMAT(create_time, '%Y-%m-%d') = '2025-05-01',create_time明明是索引列,套上函数后索引直接失效,全表扫描。类似的问题我见过太多次了。MySQL的"常见函数"表面上就是一堆API,很多人照着文档背一遍,写SQL的时候还是凭感觉来,结果要么NULL判断出错、要么选错函数导致逻辑漏洞、要么走错索引。这篇文章我想换个角度,不按文档顺序把函数罗列一遍,而是把"常见函数"背后真正决定写法的几条规则讲透:函数的分类逻辑、空值与类型陷阱、聚合函数的思维模型,以及函数对索引的影响。不管你是刚接触MySQL的新手,还是写过几年SQL却偶尔被坑的老手,都能在这里面找到可以直接抄作业的写法。

1. 函数不是拿来背的:先想清楚MySQL函数的两条主线

1.1 单行函数与聚合函数的分界线

接触一个新数据库,我建议你先别急着记忆函数名,而是建立第一个分类概念:单行函数和聚合函数。

单行函数,也就是标量函数,它接收一行输入,产出一行输出。每个输入值独立处理,互不干扰。比如CONCAT(first_name, last_name)、UPPER(name)、DATE_FORMAT(create_time, '%Y-%m')。这类函数你在SELECT子句、WHERE子句、ORDER BY子句里到处都能用,本质上是"逐行加工"。

聚合函数则完全不同:它接收多行输入,压成一行输出。COUNT(*)、SUM(amount)、AVG(price)、MAX(create_time)都属于这一类。它们的计算是按"组"进行的——如果没有GROUP BY,整张表就是一组;如果有GROUP BY,则每组各算各的。

很多新手写SQL容易乱,根本原因就是没分清楚这两类。比如有人写出SELECT member_id, COUNT(*) FROM order_info,MySQL 8.0默认开启ONLY_FULL_GROUP_BY后直接报错——因为member_id是非聚合列,又没有出现在GROUP BY里。这就是把两类函数的模型混在一起了。用生活类比理解:单行函数像给每个苹果分别削皮;聚合函数是把一筐苹果榨成一杯果汁。你要么说清楚"每个苹果削完什么样(单行)",要么说清楚"整筐榨完什么样(聚合)",不能一边削苹果一边说果汁。

1.2 输入、输出与"NULL传染"

第二个需要建立的直觉是:常规单行函数几乎都有NULL传染性。什么意思?以CONCAT为例:

SELECT CONCAT('张', '三'); -- 张三 SELECT CONCAT('张', NULL); -- NULL

第二个查询结果不是"张",而是NULL。任何参与运算的输入为NULL,输出就是NULL。+ - * /四则运算也一样,10 + NULL结果是NULL,不是10。这一点在拼接字符串、计算金额、做条件判断时非常致命,因为NULL在SQL里不等于空字符串,也不等于0,它是一个"未知"的占位符。

理解了"NULL传染"以后,你写SQL时会下意识多做一步:某个字段到底会不会为NULL?如果可能为NULL,要不要用IFNULL或COALESCE兜底?这种习惯能帮你避开大量线上事故——比如报表里某个金额列突然变成空白,排查半天发现是某个来源的订单金额字段为NULL,SUM一算整个结果就成了NULL,页面直接显示空。

1.3 函数分类地图

MySQL常见函数大致可以分成六类:

分类典型函数典型场景
字符串函数CONCAT、SUBSTRING、LEFT、RIGHT、REPLACE、TRIM、LPAD、LENGTH脱敏、拼接、补位、清洗
数值函数ROUND、CEIL、FLOOR、ABS、MOD金额取整、分页偏移
日期时间函数DATE_FORMAT、DATE_ADD、DATE_SUB、DATEDIFF、TIMESTAMPDIFF报表分组、周期计算
流程控制函数IF、IFNULL、CASE WHEN、NULLIF状态翻译、逻辑分支
聚合函数COUNT、SUM、AVG、MAX、MIN、GROUP_CONCAT统计汇总、分组聚合
加密与系统函数MD5、SHA2、UUID、DATABASE、CURRENT_DATE脱敏存值、生成主键

我不建议按这个清单一个个背。更好的方式是按业务场景记账:做报表时记住DATE_FORMAT和DATE_ADD;做接口返回时记住CONCAT和IFNULL;做状态机时记住CASE WHEN;做统计大屏时记住SUM(IF(...))。遇到什么场景,去查一遍,用两次就记住了。后面几节我按业务场景把最常用的函数过一遍,重点讲那些"看似简单但坑很深"的地方。

2. 字符串和日期函数:最常用的这批,坑也是最多的

2.1 字符串函数在真实报表里的用法

字符串函数里,CONCAT、SUBSTRING、LEFT/RIGHT、REPLACE、LPAD这几兄弟是日常使用频率最高的。说一个最常见的电信/电商场景:手机号脱敏。

某用户表member_info有字段phone,业务要求列表页显示138****1234这种格式。新手容易写一堆复杂的逻辑,其实两行就够:

SELECT CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS masked_phone FROM member_info;

LEFT(phone, 3)取前三位,RIGHT(phone, 4)取后四位,中间补四个星号。简单直接。但如果phone本身是NULL,结果也是NULL,所以更稳妥的写法是先把空值兜住:

SELECT CONCAT(LEFT(IFNULL(phone, ''), 3), '****', RIGHT(IFNULL(phone, ''), 4)) FROM member_info;

另一个常用场景是补位。订单号要求统一8位,如果底层存的是自增ID,导数据时希望展示成00001234,用LPAD:

SELECT LPAD(order_no, 8, '0') FROM order_info;

LPAD的意思是左边填充,参数分别是原始值、目标长度、填充字符。前面补0在财务对账、批次导出时特别好用。RPAD就是右边填充,场景少一些。

还需要注意一个字符长度的坑:LENGTH返回的是字节数,CHAR_LENGTH返回的是字符数。在utf8mb4编码下,一个中文字符占用3~4个字节,一个emoji占用4个字节。如果用户昵称里有emoji,你用LENGTH(nickname) > 20做校验,跟用户感知的"20个字"完全对不上。规则是:校验展示长度用CHAR_LENGTH,校验存储大小用LENGTH,别混。

2.2 日期函数与报表的月份分组

日期函数是写报表的核心。最常见的需求是"按月份统计订单量"。很多人的第一反应是:

SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS order_cnt FROM order_info GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month DESC;

这个写法本身没错,但它有一个隐含问题:GROUP BY是对DATE_FORMAT(create_time, '%Y-%m')这个表达式分组的,create_time上的索引没法参与排序和聚合。后面我会专门讲函数与索引的冲突,这里先记住一个结论:当数据量只有几百几千行时,怎么写都行;当数据量到百万级,日期函数做分组通常会全表扫描。优化方案是把create_time的范围先缩窄,或者按实际的create_time分组后再在SELECT里格式化:

SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS order_cnt FROM order_info WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01' GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month DESC;

至少先通过WHERE把扫描范围控制住,聚合压力小很多。

日期计算方面,DATE_ADD和DATE_SUB是周期计算的利器。比如统计"最近7天每天的订单量":

SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM order_info WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY day;

这里DATE_SUB(CURDATE(), INTERVAL 7 DAY)会先计算出7天前的日期,再作为条件,属于可接受的写法。但注意WHERE子句里如果写成DATE(create_time) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY),因为对create_time用了DATE()函数,又会引发索引失效。正确的做法是create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY),让列保持原样,计算放在右边。

DATEDIFF和TIMESTAMPDIFF都能算两个日期差几天。区别在于DATEDIFF只算天数,TIMESTAMPDIFF可以精确到秒、分钟、小时、月、年:

SELECT DATEDIFF('2025-06-01', '2025-05-01'); -- 31 SELECT TIMESTAMPDIFF(MONTH, '2025-01-01', '2025-06-01'); -- 5

注意TIMESTAMPDIFF的单位要写在第一个参数,顺序容易搞反。面试里经常考,实际写代码时也要小心。

2.3 这些坑我踩过

日期格式化串的大小写坑。DATE_FORMAT的格式化串里,%Y是四位数年份,%y是两位数年份;%m是带前导零的月份,%c是不带前导零的月份;%d是带前导零的日期,%e是不带前导零的日期;%H是24小时制,%h是12小时制。写错一个字符,整个报表维度就错了。我见过有人统计小时分布时用了%h,结果上午和下午的数据混在一起,排查了一下午。

DATE_FORMAT返回的是字符串,不是日期。这带来一个隐式类型转换问题:如果你拿格式化后的字符串去跟日期字面量比较,MySQL会做类型转换。在索引列上做这种比较,执行计划基本就是全表扫描。后面第5节我会详细讲一条完整排查链路。

日期范围统计还有一个边界细节:统计某一天的订单量,优先用create_time >= '2025-05-10 00:00:00' AND create_time < '2025-05-11 00:00:00'这种左闭右开区间,少用BETWEEN '2025-05-10 00:00:00' AND '2025-05-10 23:59:59'。因为如果你的时间是精确到毫秒的DATETIME(3),23:59:59会漏掉23:59:59.500这条记录。左闭右开不会漏。

3. 逻辑分支与空值处理:IF、CASE WHEN、IFNULL、COALESCE 的选择

3.1 IF 与 CASE WHEN 的选型判断

在SQL里做逻辑分支,最常用的就是IF(expr, value_if_true, value_if_false)和CASE WHEN ... THEN ... ELSE ... END。两者的区别很多人没认真想过。

IF适合只有一个条件判断的简单场景。比如查订单时,把状态码0/1翻译成中文:

SELECT order_no, IF(order_status = 1, '已支付', '未支付') AS status_desc FROM order_info;

简洁明了。但一旦分支超过两个,IF嵌套就变得非常难读:

-- 不推荐 SELECT order_no, IF(order_status = 1, '已支付', IF(order_status = 2, '已发货', IF(order_status = 3, '已完成', '未知'))) AS status_desc FROM order_info;

三层嵌套勉强能看,五层以上就完全没法维护了。这种多分支场景,应该用CASE WHEN:

SELECT order_no, CASE WHEN order_status = 1 THEN '已支付' WHEN order_status = 2 THEN '已发货' WHEN order_status = 3 THEN '已完成' ELSE '未知' END AS status_desc FROM order_info;

以我的经验,选型标准很简单:单条件用IF,多条件用CASE WHEN。CASE WHEN还支持等值判断的简写形式CASE order_status WHEN 1 THEN ... END,不过当条件不是等值判断时,还是要写完整的CASE WHEN。

3.2 空值处理的四种工具

空值处理是SQL里最容易出bug的地方。这里要先把概念理清:NULL不是空字符串,也不是0,它是一个"未知"的标记。所以IFNULL(null_expr, default)和COALESCE(expr1, expr2, ...)都是用来提供兜底值的:

函数行为适用场景
IFNULL(a, b)如果a为NULL则返回b,否则返回a单字段兜底
COALESCE(a, b, c, ...)从左到右返回第一个非NULL值多字段取第一个有效值
NULLIF(a, b)如果a等于b则返回NULL,否则返回a避免除零、清洗特定值
IF(expr, a, b)条件判断,expr为真返回a,否则返回b通用逻辑分支

这里有个细节:COALESCE是SQL标准函数,IFNULL是MySQL特有的。功能上IFNULL(a, b)等价于COALESCE(a, b),但COALESCE可以接收多个参数,比如COALESCE(mobile, phone, '无联系方式')。写多字段兜底的时候,COALESCE比嵌套IFNULL清爽得多。

NULLIF用得相对少,但避免除零错误特别有用。报表里计算占比时,分母可能为0:

SELECT category, SUM(amount) / NULLIF(SUM(cnt), 0) AS avg_amount FROM order_info GROUP BY category;

SUM(cnt)为0时,NULLIF(SUM(cnt), 0)返回NULL,整个除法结果变成NULL,而NULL在前端展示时通常会显示为"暂无数据",比报一个Division by zero错误友好得多。

另一个非常隐蔽的坑:NULL参与=比较时结果不是"不等",而是"未知"。WHERE name = NULL永远查不到任何行,必须写作WHERE name IS NULL。新手在写接口过滤条件时尤其容易踩——某个可空字段没传值,前端传了空字符串,代码里拼成了WHERE nickname = '',而库里的NULL数据全被过滤掉了。

3.3 聚合路上的空值

聚合函数对NULL的处理也是不同的。COUNT(column)会忽略该列为NULL的行,COUNT(*)不会忽略任何行。看这个例子:

SELECT COUNT(*), COUNT(phone), COUNT(IFNULL(phone, '')) FROM member_info;

假设用户表里有100条记录,其中10条phone是NULL,那么COUNT(*)返回100,COUNT(phone)返回90,COUNT(IFNULL(phone, ''))返回100。当你用COUNT(某一列)去统计数量时,如果这列存在空值,结果会偏小,而且不会报错——这才是最危险的,数据对不上账的时候你根本不知道问题出在NULL上。

SUM和AVG也会忽略NULL行,但有一个额外陷阱:如果某列全都是NULL,SUM的结果是NULL而不是0。所以统计金额时建议写SELECT IFNULL(SUM(amount), 0) FROM order_info,避免结果集里出现NULL导致前端展示空白。这一点在写数据接口时几乎是必踩的。

4. 聚合函数与分组思想:从"行"到"组"的思维转换

4.1 GROUP BY 之后,SQL的世界变了

聚合函数的核心价值是把"行视角"变成"组视角"。写聚合SQL时,脑子里要有一个明确的模型:先分组,再聚合。WHERE是在分组之前过滤行,HAVING是在分组之后过滤组。这个执行顺序搞不清楚,写出来的SQL经常逻辑错误。

比如,想查"下单次数超过5次的会员":

SELECT member_id, COUNT(*) AS order_cnt FROM order_info WHERE order_status = 'SUCCESS' -- 先过滤掉不成功的订单 GROUP BY member_id HAVING COUNT(*) > 5; -- 再过滤掉订单数不足5的会员

WHERE和HAVING用的绝对不是同一个逻辑层面。如果你把COUNT(*) > 5写到WHERE里,MySQL直接报错:聚合函数不能出现在WHERE子句。这个报错本身就是帮你理解执行顺序的好老师。

还有一个常见报错:SELECT member_id, order_no, COUNT(*) FROM order_info GROUP BY member_id。在ONLY_FULL_GROUP_BY模式下,MySQL不允许选择那些既不在聚合函数里、也不在GROUP BY里的列。这里的order_no就是"悬空列",因为分组后每组有多个order_no,MySQL不知道该取哪一个。出现这种报错时,你要么把order_no加进GROUP BY,要么用ANY_VALUE(order_no)明确告诉MySQL"随便取一个",要么改成GROUP_CONCAT(order_no)聚合列出。

4.2 GROUP_CONCAT 的截断问题

GROUP_CONCAT可以把组内多行的某个字段拼成一个字符串,做"分组内明细列表"很好用。比如按会员聚合订单号:

SELECT member_id, GROUP_CONCAT(order_no ORDER BY create_time SEPARATOR ',') AS order_list FROM order_info GROUP BY member_id;

这里ORDER BY create_time控制组内拼接顺序,SEPARATOR ','指定分隔符,默认是逗号。很多人在这一步就满足了,但真正的坑在数据量大之后:GROUP_CONCAT有一个最大长度限制,默认是1024字节。一旦拼接结果超过这个长度,数据会被静默截断,而且不报错。你查出来的order_list只有一半,拿去导出、展示、下游处理,数据全对不上。

解决方法是根据业务量合理调大:

SET SESSION group_concat_max_len = 102400;

注意一定要在会话级别设置,不是全局的。如果是一个定时任务里跑的SQL,就在SQL脚本开头先执行这条;如果是应用程序里的查询,可能需要通过连接初始化语句设置。否则你以为调大了,换个连接又变回1024。

4.3 条件聚合小技巧

聚合函数与IF结合,可以写出非常紧凑的"交叉统计"SQL。比如统计每个月的成功订单数和失败订单数:

SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, SUM(IF(order_status = 'SUCCESS', 1, 0)) AS success_cnt, SUM(IF(order_status = 'FAIL', 1, 0)) AS fail_cnt FROM order_info WHERE create_time >= '2025-01-01' AND create_time < '2025-07-01' GROUP BY DATE_FORMAT(create_time, '%Y-%m');

SUM(IF(condition, 1, 0))的思路是:满足条件加1,不满足加0,最后求和就是满足条件的行数。这种条件聚合写法在报表里出镜率极高。另一种等价写法是COUNT(IF(condition, 1, NULL)),注意第二个分支必须是NULL而不能是0,因为COUNT只统计非NULL值。如果你写COUNT(IF(condition, 1, 0)),第二个分支的0也会被计数,结果恒等于COUNT(*)。这个细节也是高频bug:条件没生效,统计结果虚高,但SQL不报错。

一个我常用的经验:遇到"一张表里要出多个指标"的需求,比如"同时统计总数、成功数、失败数、平均金额",优先用条件聚合一次查出来,而不是写四条独立的SQL再去内存里拼。前者性能高一个量级,代码也更紧凑。

4.4 聚合结果排序与ROLLUP(简述)

ORDER BY通常写在GROUP BY之后,对聚合结果排序。也可以配合WITH ROLLUP多出一行总计。例如:

SELECT member_id, COUNT(*) FROM order_info GROUP BY member_id WITH ROLLUP;

最后会多一行member_id = NULL,代表全部订单的总数。做总计行时挺方便,但注意前端解析时需要对NULL做特殊处理,别把总计行当普通数据。

5. 函数引发的线上故障:一条慢查询SQL的完整排查链路

5.1 事故现场:统计SQL突然变慢

某天,一个定时统计任务开始超时。任务逻辑很简单:统计指定日期内每个渠道的订单量。SQL长这样:

SELECT channel_id, COUNT(*) AS cnt FROM order_info WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2025-05-10' GROUP BY channel_id;

order_info表有百万级数据,create_time上明明建了索引。某天数据量增长后,这条SQL从原来的几十毫秒变成几秒,再后来直接拖垮了数据库的CPU。

排查过程第一步,先SHOW FULL PROCESSLIST看有没有大量慢查询堆积。看到的确是这个SQL占满了连接。第二步,EXPLAIN看执行计划:

EXPLAIN SELECT channel_id, COUNT(*) AS cnt FROM order_info WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2025-05-10' GROUP BY channel_id;

执行计划里type是ALL,key是NULL,rows估算扫描百万行。明明possible_keys里有idx_create_time,但优化器就是没用上——因为在WHERE子句里,create_time被DATE_FORMAT函数包裹,索引的有序性对函数变换后的值毫无意义,优化器无法把索引区间映射成等价的查找范围,只能放弃索引走全表。

5.2 修复方案:函数换边、范围改写、生成列兜底

第一优先的改写方案,是让索引列保持"裸列",把函数计算放到等号的另一边。需求本质是"查某一天",所以完全可以用区间查询替代:

SELECT channel_id, COUNT(*) AS cnt FROM order_info WHERE create_time >= '2025-05-10 00:00:00' AND create_time < '2025-05-11 00:00:00' GROUP BY channel_id;

改写后的执行计划里,type变成range,key用的是idx_create_time,扫描行数从百万变成这一天内的几千行,耗时回到几十毫秒。这背后的逻辑是:优化器喜欢"范围"而不是"变换"。它能轻松理解BETWEEN 2025-05-10 00:00:00 AND 2025-05-10 23:59:59.999等价于索引区间扫描,却无法反向推导DATE_FORMAT(create_time, '%Y-%m-%d') = '2025-05-10'应该匹配哪些索引区间。

有些场景实在没法改写,比如接口里必须传一个格式化后的日期字符串。这时候有两个备选方案。第一个是MySQL 5.7+引入的生成列(Generated Column)。建表时加一列:

ALTER TABLE order_info ADD COLUMN create_date DATE GENERATED ALWAYS AS (DATE(create_time)) STORED, ADD INDEX idx_create_date (create_date);

查询时就写WHERE create_date = '2025-05-10',索引正常生效。生成列的计算由存储引擎完成,应用层无感。第二个方案是业务层预处理:取数据时把当天的开始时间和结束时间算好,SQL里直接传两个参数。大多数场景用这个方案就够了。

5.3 隐式类型转换:另一个静默杀手

这个案例让我想起另一个高频故障:隐式类型转换。假如phone列是VARCHAR,你写了WHERE phone = 13800138000,注意右边是数字不是字符串。MySQL会尝试把字符串列转成数字再做比较,结果就是索引失效,全表扫描。手机号这种字段一旦全表扫描,代价极高。正确写法永远是WHERE phone = '13800138000'。

如何判断有没有发生隐式转换?执行计划里如果key从有到无、rows暴涨,并且Extra列出现Using where,就要怀疑类型或字符集。也可以直接EXPLAIN后看key_len是否有异常。更简单的自查方法:记住一条铁律——比较运算两边的类型和字符集要一致。关联查询里,两个表的关联字段如果一边是utf8一边是utf8mb4,同样会导致索引失效,因为MySQL需要先把其中一边的字符集转换后才能比较。

5.4 写完SQL先看执行计划

这次排查之后,我养成了一个习惯:写完SQL先跑一次EXPLAIN。尤其是聚合、多表关联、大数据量单表查询,十秒钟看一眼执行计划,能省一小时排障。MySQL 8.0 还提供了EXPLAIN ANALYZE,可以直接输出每步实际耗时,定位瓶颈更精准。

EXPLAIN ANALYZE SELECT channel_id, COUNT(*) AS cnt FROM order_info WHERE create_time >= '2025-05-10 00:00:00' AND create_time < '2025-05-11 00:00:00' GROUP BY channel_id;

输出会包含每一步的actual time、rows、loops。看到actual rows和估算rows差距巨大,或者某个步骤耗时异常,基本就能锁定问题。排查慢SQL时,先看执行计划永远比对着SQL发呆有效。

6. 我写SQL时一直遵守的几条函数军规

6.1 索引列上不套函数

这条是第一军规。无论是WHERE、JOIN ON、ORDER BY还是GROUP BY,只要列上套了函数,索引大概率用不上。我说的不是"绝对",因为MySQL 8.0对某些函数支持函数索引,你可以显式CREATE INDEX idx_name ON t (DATE(create_time))来解决;但默认情况下,普通索引经不起函数折腾。我建议所有人在写SQL时先假设"索引列套函数=索引失效",除非你明确知道自己建了函数索引。

具体到写法上,就是本能的把函数移到等号右侧:

  • WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2025-05-10'→WHERE create_time >= '2025-05-10 00:00:00' AND create_time < '2025-05-11 00:00:00'
  • WHERE YEAR(create_time) = 2025→WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01'
  • WHERE LEFT(phone, 3) = '138'→WHERE phone LIKE '138%'

这类改写本质上是把"对列做变换"转化为"对值做范围约束",优化器就能高效利用B+树有序性了。

6.2 空值兜底要趁早

IFNULL和COALESCE不要等到报表层面才处理,应该在SQL里就处理好。尤其涉及金额、数量的字段,聚合前先想清楚NULL会怎么传导:SUM(NULL)返回NULL,CONCAT遇到NULL返回NULL,CASE WHEN里NULL不等于任何值。我写统计SQL时,凡是"可能出现NULL的列参与运算"的地方,都会下意识套一个IFNULL或COALESCE。如果某列业务上不允许NULL,我会在SQL里过滤掉WHERE xxx IS NOT NULL,避免后续所有人被NULL坑。

6.3 聚合查询里,COUNT 别乱用列名

统计行数用COUNT(*)或者COUNT(1),这两个结果一致,只是写法习惯不同。统计某列非NULL值个数时才用COUNT(column)。很多时候报表里"数量对不上"的根因,就是用了COUNT(column)且该列有空值。团队里如果新人多,我建议代码评审时重点盯一下COUNT的用法,这个bug非常隐蔽。

SUM也是一样。需要结果为0而不是NULL时,写成IFNULL(SUM(column), 0)。前端拿到NULL和0展示完全不同,0代表"有数据但没有",NULL代表"未知",业务上一般是前者。

6.4 按场景建自己的"函数笔记"

最后一个建议:别按官方文档顺序学函数,按业务场景积累。我自己习惯在笔记里按场景分类记:脱敏场景、周期统计场景、状态翻译场景、分页排序场景、空值兜底场景。每踩一个坑就往上补一条,例如"DATE_FORMAT 的 %i 是分钟,%H 是24小时制小时,写错直接报表错乱"。这种场景化的积累比背函数列表高效得多。下次遇到类似需求,翻自己的笔记,十秒钟就能选对函数、避开旧坑。

很多人觉得MySQL常见函数"太简单了,没什么好学的",实际写起来才知道,细节里全是坑。我这几年见过的最严重的SQL事故,不是复杂的多表关联,而是DATE_FORMAT包了索引列导致全表扫描、COUNT计了NULL列导致数据对不上、GROUP_CONCAT被1024字节截断导致导出文件残缺。希望这篇能把函数的底层规则和常见坑讲清楚,你写SQL的时候少走几步弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询