☰
MySQL高频函数全解析:六大类函数用法与避坑指南
2026/10/20 5:26:54 网站建设 项目流程

你是不是也有这种感觉:每天写 SQL,来来回回用到的函数就那么几个,但一到关键时候就卡壳。比如要把两个字段拼起来,突然忘了 CONCAT 遇到 NULL 会不会出错;想按月份做统计,DATE_FORMAT 的格式符还得临时翻手册;更别提 ROUND、TRUNCATE、CEILING、FLOOR 这四个取整函数,每次用都要纠结一下。说实话我做了这么多年数据开发和系统维护,MySQL 函数相关的坑没少踩,尤其是字符集、时区和隐式类型转换这几个方向,网上资料讲得分散,踩一次就得浪费半天去排查。

这篇博文我打算把 MySQL 里最常见、最高频的函数系统梳理一遍,包括字符串、数值、日期时间、流程控制、聚合和类型转换六大类,每个函数都会说明适用场景、执行逻辑和容易踩的坑。重点不是背函数清单,而是搞清楚“什么时候用哪个、为什么这么用”。内容基于 MySQL 8.0 的默认行为,也适合刚入门没多久的开发同学,写过几年 SQL 但没系统整理过的人同样能从中找到平时忽略的细节。

1. 字符串函数:日常写 SQL 最先用到的工具箱

字符串处理是 SQL 里最频繁的操作,无论是拼接报表字段、截取编号、脱敏手机号,还是做模糊条件判断,都离不开这一组函数。但这一组函数也是初学者最容易写出“能跑但不对”的 SQL 的地方,因为 NULL 的传播特性、字符集差异和隐式转换都会在背后捣乱。

1.1 拼接、截取、大小写:文本处理三板斧

先说说 CONCAT。它是用来拼接多个字符串的,比如CONCAT('订单号: ', order_no),功能很直白,但有一个非常经典的坑:只要参与拼接的任何一个值是 NULL,整个结果就会变成 NULL。原因很简单,NULL 在 SQL 语义里代表“未知”,未知和任何字符串拼接的结果依然未知。实际业务里经常有“用户没有填姓氏”这种场景,一拼出来整列都是空白,排查半天才发现是 NULL 的问题。

更好的做法是用CONCAT_WS(separator, str1, str2, ...),它的第一个参数是分隔符,并且会自动忽略 NULL 值。举例来说,拼接省市区地址字段时,CONCAT_WS('-', province, city, district)不会因为某个区为空就整个地址消失,而是只显示已填的部分。这一点在导出报表、生成日志时特别有用。

截取函数 SUBSTRING(也常写 SUBSTR)用得也很多,语法是SUBSTRING(str, pos, len)。注意 MySQL 的字符下标从 1 开始,不是从 0 开始,这是很多从其他语言转过来的开发者最容易搞混的地方。它支持负索引,SUBSTRING('abcdef', -3, 2)会从倒数第三个字符开始往后取两个,结果是 "de",适合从右侧取指定长度的字符串,比如取订单号后四位。

大小写转换函数 UPPER 和 LOWER 没什么好说的,一个是转大写一个是转小写。但有一点值得注意:它们作用于非英文字符时,行为取决于字符集和排序规则。在 utf8mb4 下对中文调用 UPPER 不会有什么变化,这很正常。

1.2 查找替换与去空格:处理文本内容的核心组合

查找某个子串在字符串中第一次出现的位置,用到的是 LOCATE 或者 INSTR。LOCATE(substr, str)返回子串的位置,找不到就返回 0;INSTR(str, substr)参数顺序跟 LOCATE 是反的,我见过不少人把这两个记混,导致查出来的结果莫名其妙。建议只记住一个,我习惯用 LOCATE,因为它的参数顺序更符合自然语言“在谁里面找什么”。

字符串补位函数 LPAD 和 RPAD 也值得单独拎出来说。比如生成订单流水号时,要求编号统一是 8 位,不足部分在左侧补零:LPAD(CAST(id AS CHAR), 8, '0')。这个函数经常在报表编号、优惠券编码场景里配合 CAST 一起使用,非常实用。

REPLACE 是全量替换函数,REPLACE(str, from_str, to_str)。它的典型场景是做脱敏,比如把手机号中间四位替换成星号:REPLACE(phone, SUBSTRING(phone, 4, 4), '****')。不过这里有个经验之谈:脱敏最好写明确,不要用这种动态截取再替换的写法,因为如果手机号中间四位在某些数据里出现多次,REPLACE 会把所有匹配的地方都替换掉。安全起见,直接用 CONCAT 把前三后四拼出来更可控。

去空格方面,TRIM 去两端空格,LTRIM 只去左边,RTRIM 只去右边。TRIM 还能去掉指定字符,比如TRIM(BOTH 'x' FROM 'xxhelloxx')会返回 "hello"。需要注意,这里默认去的是空格,如果数据里有换行符\n或制表符\t,TRIM 不会处理,得配合 REPLACE 先把换行符替换掉再 TRIM。

1.3 CHAR_LENGTH 和 LENGTH 的区别:中文场景最容易翻车

很多人在 utf8mb4 字符集下统计字符串长度时会踩坑,问题就出在 CHAR_LENGTH 和 LENGTH 的区别上。

CHAR_LENGTH 返回的是字符数,也就是我们人眼看到的“有多少个字符”;LENGTH 返回的是字节数。在 utf8mb4 编码下,一个英文字母占 1 个字节,但一个中文汉字占了 3 个字节。所以CHAR_LENGTH('你好')的结果是 2,而LENGTH('你好')的结果是 6。如果你用 LENGTH 去判断一段用户评论是否超过某个长度,就会把中文字数放大三倍,导致明明是 50 个字却提示超长。

这里要特别提醒:建表时 varchar(20) 中的 20 表示的是字符数,不是字节数。所以在做输入校验时,应该用 CHAR_LENGTH 来对齐业务上的“字数”概念;只有当你真正关心存储占用,比如判断某个字段是否超过 B 树索引的字节上限时,才需要看 LENGTH。我见过有人用 LENGTH 做评论字数限制,上线后中文内容全被卡住,就是一个非常典型的案例。

1.4 实操心得:字符集、排序规则与索引的那些事

字符串函数用得好不好,很多时候取决于你对字符集和排序规则有没有概念。MySQL 8.0 默认是 utf8mb4,默认排序规则一般是 utf8mb4_0900_ai_ci,其中 ai 表示口音不敏感,ci 表示大小写不敏感。也就是说,条件里写WHERE username = 'Admin',可能会把 'admin'、'ADMIN' 都查出来。如果业务上要求用户名严格区分大小写,就要把排序规则改成 ..._bin,或者在设计表时就确定好。

还有一条铁律:不要在索引列上直接套函数。比如WHERE UPPER(username) = 'ADMIN',写法看着没毛病,但 MySQL 必须先对每一行的 username 执行 UPPER,再拿结果去比较,原来的 B+ 树索引直接失效,数据量一大就是全表扫描。解决办法通常是:存储时统一大小写格式,或者使用 MySQL 8.0 的生成列把UPPER(username)作为单独的列并加上索引,查询时直接用这个生成列。

我自己在维护业务系统时,遇到过不止一次因为字符串函数导致线上慢查询的案例。排查到最后,几乎都是同样的套路:条件字段的类型和排序规则没确认,函数一上去,索引就废了。所以现在写 SQL 前我都会先确认列的类型和排序规则,条件里尽量不做函数包裹。

2. 数值函数:别在计算上栽跟头

数值计算看着简单,真正埋雷的地方往往不在函数本身,而在数据类型。你在 SELECT 里写 0.1 + 0.2,表面上是两个小数相加,实际上 MySQL 的浮点类型在二进制层面根本没法精确表示 0.1 和 0.2,所以结果会变成 0.30000000000000004。这种问题在金额计算里是不可接受的。

2.1 四舍五入与取整:ROUND、TRUNCATE、CEILING、FLOOR 的边界

ROUND(x, d) 是四舍五入函数,d 表示保留的小数位数。ROUND(2.567, 2)结果是 2.57。如果不写 d,相当于四舍五入到整数,ROUND(2.5)会返回 3。注意 MySQL 这里的四舍五入规则是“逢五进位”,跟某些编程语言的“银行家舍入”不一样,做财务计算时不要凭经验套用其他语言的行为。

TRUNCATE(x, d) 是直接截断,不四舍五入。TRUNCATE(2.567, 2)结果是 2.56。这两个函数的差别在会计场景里很关键:如果系统里的金额显示只需要保留两位小数,但实际计算又不想因为四舍五入产生累计误差,那就得想清楚每一层到底用 ROUND 还是 TRUNCATE。

向上取整是 CEILING(CEIL 是它的别名),向下取整是 FLOOR。CEILING(2.1)返回 3,FLOOR(2.9)返回 2。买票、计费、分页这类场景经常会用到 CEILING,比如按 100 条一页算总页数,就是CEILING(total / 100)。

还有一个很多人不知道的用法:ROUND 和 TRUNCATE 的位数 d 可以是负数,表示在小数点左侧进行舍入。例如ROUND(1234.567, -2)的结果是 1200,ROUND(1250, -2)的结果则是 1300,因为它按百位四舍五入。这个技巧在做“金额以百元为单位汇总”的报表时很实用,但可读性差,建议在 SQL 注释里写清楚。

2.2 取余、绝对值与随机数:MOD、ABS、RAND 的冷知识

MOD(x, y) 是取余函数,等价于x % y。它的一个细节是结果的符号跟被除数一致,MOD(-7, 2)在 MySQL 里返回 -1,而不是 1。如果业务里需要判断一个数是奇数还是偶数,注意不要被负数干扰,稳妥写法是MOD(ABS(column), 2)。

ABS 是绝对值函数,RAND 是随机数函数。RAND() 返回 [0, 1) 区间的一个随机小数,RAND(N) 则是一个固定种子,同一个种子生成的随机序列是确定的。这个特性可以用来生成测试数据,保证每次跑出来的数据一样,方便复现问题。

日常开发里常见的需求是从表中随机取 N 条记录,很多人会直接ORDER BY RAND() LIMIT 10。这个写法在小表上没有感觉,但在千万级的大表上会非常痛苦,因为 MySQL 需要对全表每一行都生成随机数,再做排序。更合理的思路是查出主键的最小值和最大值,在区间内随机取一个主键值,再用主键去定位记录。当然,如果业务允许稍微复杂的写法,用子查询随机取主键再关联原表,性能也好很多。

2.3 实战经验:金额计算不要直接用浮点类型

这个坑我必须单独拿出来说。MySQL 的 FLOAT 和 DOUBLE 是二进制浮点数,无法精确表示很多十进制小数。比如:

SELECT 0.1 + 0.2;

在 MySQL 8.0 里执行,结果不是 0.3,而是 0.30000000000000004。这个误差在单次计算里几乎看不出来,但一旦累加、汇总,就会积累出不可忽略的差异。订单金额、账户余额、单价这类数据,设计表结构时就必须用 DECIMAL,比如DECIMAL(10,2)表示最多 10 位数字,保留 2 位小数。

如果因为历史原因,某个金额字段已经是 DOUBLE,想要纠正数据时也要小心。直接 UPDATE 回写很容易因为精度问题产生新的误差,建议先把数据读出来,在应用层使用高精度类型处理,再写回。日常写聚合 SQL 时,对 DECIMAL 字段做 SUM 一般是安全的,但做除法或 AVG 可能产生很多位小数,最终展示前用 ROUND 控制一下位数,能避免报表里出现一连串诡异数字。

3. 日期时间函数:时区与索引是最容易踩的坑

日期时间是业务系统里最绕不开的类型,但也是新手翻车重灾区。原因主要有两个:一个是 MySQL 的日期时间函数数量多、格式符多,另一个是时区问题让同一段时间在不同环境下表现出完全不同的结果。再加上函数套列导致的索引失效,日期条件往往成为慢查询的头号元凶。

3.1 获取当前时间:NOW、CURRENT_TIMESTAMP、SYSDATE 的细微差别

NOW() 和 CURRENT_TIMESTAMP 基本是同一个函数,返回的是当前语句开始执行时的日期时间,精确到秒或微秒取决于版本和参数。重点要讲的是 SYSDATE(),它跟 NOW() 看起来一样,实际却有一个微妙差异:SYSDATE() 返回的是函数被调用那一刻的实时时间。

你可以做个简单验证:

SELECT NOW(), SYSDATE(), SLEEP(2), NOW(), SYSDATE();

这个语句里,两个 NOW() 的值是一样的,而两个 SYSDATE() 会相差 2 秒,因为中间 SLEEP 了 2 秒。看似不起眼,但在长事务、存储过程或者批量更新脚本里,SYSDATE() 会导致“同一个事务里取到不同时间”的情况,进而影响数据的一致性判断。所以我的建议是:业务逻辑里统一用 NOW(),不要随手用 SYSDATE()。

配套的几个函数也顺便记一下:CURDATE() 和 CURRENT_DATE() 返回当前日期,CURTIME() 和 CURRENT_TIME() 返回当前时间。这些函数返回的类型分别是 DATE 和 TIME,在做日期区间查询时可以直接参与比较。

3.2 格式化与解析:DATE_FORMAT 与 STR_TO_DATE

DATE_FORMAT 是把日期转成指定格式的字符串,格式符非常丰富,常用的有:

  • %Y 四位年份,%y 两位年份
  • %m 两位月份,%c 月份(不补零)
  • %d 两位天,%e 天(不补零)
  • %H 24 小时制,%h 12 小时制,%i 分钟,%s 秒
  • %W 星期几的英文名,%M 月份的英文名

典型用法是DATE_FORMAT(order_time, '%Y-%m-%d')把 datetime 转成 "2024-03-15" 格式,用来按天分组统计:

SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS order_day, COUNT(*) AS cnt FROM orders GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d');

STR_TO_DATE 是反向操作,把字符串按指定格式解析成日期类型,多用在导入外部数据。比如 CSV 里的日期列是 "15/03/2024 10:30:00",可以用STR_TO_DATE('15/03/2024 10:30:00', '%d/%m/%Y %H:%i:%s')转成标准 DATETIME。注意格式符必须跟字符串完全对应,否则会返回 NULL 或报错。

这里有个经验:在做报表时,如果日期字段是 datetime 类型,按天分组更推荐用区间判断,而不是 DATE_FORMAT 函数套列。下面的 3.4 会细讲。

3.3 日期运算与区间提取:DATE_ADD、DATEDIFF、TIMESTAMPDIFF

DATE_ADD(date, INTERVAL expr unit) 是在日期上增加一段时间,DATE_SUB 则是减少。单位可以是 YEAR、QUARTER、MONTH、WEEK、DAY、HOUR、MINUTE、SECOND。比如:

SELECT DATE_ADD('2024-03-15', INTERVAL 1 MONTH); -- 2024-04-15 SELECT DATE_SUB('2024-03-15', INTERVAL 7 DAY); -- 2024-03-08

INTERVAL 后面的数值也可以是负数,所以 DATE_SUB 完全可以用 DATE_ADD 加负值替代。区分两个函数的意义更多在于可读性。顺手记一个非常实用的函数 LAST_DAY(date),它返回指定日期所在月份的最后一天。LAST_DAY('2024-02-10')返回 '2024-02-29',做月末结算、出账单时特别方便。

DATEDIFF(end_date, start_date) 返回两个日期相差的天数,计算逻辑是end_date - start_date。DATEDIFF('2024-03-20', '2024-03-15')返回 5。TIMESTAMPDIFF(unit, start_date, end_date) 更灵活,可以按 MONTH、HOUR、MINUTE 等粒度计算差值,比如计算两个时间相差几个月、几小时。注意 TIMESTAMPDIFF 的参数顺序是“结束时间在前、开始时间在后”,跟 DATEDIFF 相反,很多人第一次用会搞反,导致结果出现负数。

日期中提取部分字段的函数也很好用:YEAR(date)、MONTH(date)、DAY(date) 分别取年、月、日;更通用的还有 EXTRACT(YEAR FROM date)、EXTRACT(MONTH FROM date)。统计“本年度注册用户”或“某月订单”这些需求,都会用到。

3.4 实操心得:别在索引列上套函数

日期函数导致索引失效,是我排查慢查询时遇到频率最高的问题之一。最典型的写法是这样:

SELECT * FROM orders WHERE DATE_FORMAT(order_time, '%Y-%m-%d') = '2024-03-15';

这个写法语义上没错,但 MySQL 没办法用 order_time 上的索引,因为每一行的 order_time 都必须先经过 DATE_FORMAT 转成字符串,再跟目标值比较。处理上百万行数据时,全表扫描的代价非常明显。更好的写法是范围查询:

SELECT * FROM orders WHERE order_time >= '2024-03-15 00:00:00' AND order_time < '2024-03-16 00:00:00';

这样既覆盖了 3 月 15 日全天,又保留了 B+ 树索引的顺序匹配能力。再比如“最近 7 天订单”,很多人写成WHERE DATE_SUB(NOW(), INTERVAL 7 DAY) <= order_time,其实没问题,因为左侧的 DATE_SUB 是作用于常量 NOW(),不是作用于列;真正要避免的是在 order_time 上套函数。

时区问题也需要警惕。如果程序写入的时间是 UTC,数据库却按本地时区读取,日期查询和报表统计就会凭空差出 8 小时。处理办法是让数据库、连接参数和业务代码的时区配置保持一致。MySQL 的 CONVERT_TZ 函数可以转换时区,但依赖系统的时区表是否已加载,实际使用中反而容易引入新的不确定性,不如从源头统一。

4. 条件函数与流程控制:让 SQL 具备“逻辑判断”能力

你有没有遇到过这样的场景:一个查询要根据某个字段的不同值返回不同结果,比如订单状态 1 显示为“待支付”,2 显示为“已支付”。如果全靠应用层去处理,写出来的代码又长又啰嗦。条件函数就是为这类需求准备的,它们让 SQL 有能力做判断和分支,把部分展示逻辑落到数据库里。

4.1 IF、IFNULL、NULLIF:三个易混但常用的条件函数

IF(expr, true_value, false_value) 是三目运算符。IF(amount > 100, '大额', '普通')会根据条件返回对应的值。这个函数很小巧,但我更推荐在复杂逻辑里使用 CASE WHEN,因为条件多的时候 IF 嵌套会让 SQL 变得非常难读。

IFNULL(expr1, expr2) 用来处理 NULL:expr1 是 NULL 就返回 expr2,否则返回 expr1。这个函数在查询用户昵称、备注、扩展字段时特别常用,比如IFNULL(nickname, '匿名用户')。要注意的是,IFNULL 在返回时可能触发隐式类型转换,如果两个参数类型不一致,尽量先手动做好类型转换,别依赖隐式行为。

NULLIF(expr1, expr2) 跟 IFNULL 不是一回事:当两个参数相等时,它返回 NULL,否则返回第一个参数。最经典的用法是防除零。比如要计算“完成率 = 完成数 / 总数”,当总数为 0 时直接除会报错,写成完成数 / NULLIF(总数, 0),分母变成 NULL,整个表达式的结果就变成 NULL,不会报错,展示层再统一处理 NULL 即可。

4.2 CASE WHEN:表达式的两种写法与适用场景

CASE WHEN 有两种写法。第一种是简单 CASE 表达式,语法是CASE value WHEN expected_value THEN result ELSE default END,适合做等值匹配,比如CASE status WHEN 1 THEN '待支付' WHEN 2 THEN '已支付' END。第二种是搜索 CASE 表达式,语法是CASE WHEN condition THEN result ELSE default END,条件可以写任意比较表达式,比如范围判断、LIKE、IN 等。

我个人的习惯是尽量用搜索式,哪怕等值匹配也写成CASE WHEN status = 1 THEN ...。原因很简单:可读性更好,后续如果要从等值判断改成范围判断,不需要重写整个表达式。另外,搜索式 CASE 是标准 SQL 写法,跨数据库迁移时兼容性也更好。

CASE WHEN 最容易被忽略的一点是 ELSE 缺省时默认返回 NULL。如果某个分支没有匹配,结果就是 NULL,这在后续聚合、拼接时会产生连锁反应。如果你确定业务上“没有匹配就返回 0”,一定要写 ELSE 0。

4.3 COALESCE:一次性处理多个字段的默认值

COALESCE 的作用是返回参数列表里第一个非 NULL 的值,可以传多个参数,这在多字段叠加取值的场景里非常好用。比如一个用户表里有 nickname、phone、reg_source 三列,想在前台展示时“昵称优先,没有昵称用手机号,再没有就用注册来源”,一条 SQL 就能搞定:

SELECT COALESCE(nickname, phone, reg_source, '未知用户') AS display_name FROM users;

COALESCE 是标准 SQL,IFNULL 可以看成它只支持两个参数的简化版。两者效率上没有明显差别,但 COALESCE 更通用,建议新代码统一用它。它和 NULLIF 搭配还能解决“空字符串也要当成空值”的问题。例如COALESCE(NULLIF(TRIM(nickname), ''), '匿名用户'),先用 TRIM 去掉空格,用 NULLIF 把空串变成 NULL,再交给 COALESCE 补默认值,一步到位。

4.4 实战案例:用条件函数做分类统计

条件函数最强大的用法是放在聚合函数内部,实现“按条件计数”或“按条件求和”。比如订单表里有一列 amount,想统计每个业务员“大额订单(金额 >= 1000)有多少笔”,可以这么写:

SELECT sales_name, SUM(CASE WHEN amount >= 1000 THEN 1 ELSE 0 END) AS large_order_cnt, SUM(CASE WHEN amount < 1000 THEN 1 ELSE 0 END) AS normal_order_cnt, COUNT(*) AS total_cnt FROM orders GROUP BY sales_name;

这里的原理是:SUM 会跳过 NULL,而 CASE 表达式在条件不满足时返回 0,所以 SUM(CASE ... THEN 1 ELSE 0 END) 统计出来的就是满足条件的行数。也可以用COUNT(IF(condition, 1, NULL))达到同样效果,两者本质一样。但要小心:COUNT 的 IF 分支里不要用 0,因为 COUNT(0) 会计数,只有 NULL 才会被跳过。

这种写法可以避免拆分成多次查询再在应用层合并的麻烦,一条 SQL 就能把多个维度的统计结果拉出来,特别适合做销售看板、渠道分析这类报表。

5. 聚合函数:统计报表的正确姿势

聚合函数是报表类 SQL 的基石,COUNT、SUM、AVG、MAX、MIN 五个函数几乎覆盖了所有统计需求。但越基础的东西,越容易在细节上出错。比如 COUNT(字段) 在字段有 NULL 时统计结果会偏小,AVG 的分子分母也不是你直觉里那样。下面把几个最容易混淆的细节一次讲清楚。

5.1 COUNT 家族:COUNT(*)、COUNT(1)、COUNT(字段) 到底啥区别

网上关于 COUNT() 和 COUNT(1) 性能差异的说法五花八门,在 MySQL 8.0 的 InnoDB 引擎下,这两者基本没有性能差别。COUNT() 是统计行数,COUNT(1) 也是统计行数,因为括号里的 1 只是一个常量表达式,MySQL 不会去读具体列。真正容易出错的是 COUNT(字段)。

COUNT(title) 这样的写法,会统计 title 字段非 NULL 的行数。换句话说,如果 title 是 NULL,这一行不会被计入结果。这在统计“发表过文章的用户数”时是合理的,但如果你只是想知道表里一共有多少行,却写成 COUNT(id),一旦某行 id 为 NULL(虽然主键不会,但普通字段会),结果就不对了。最稳妥的口诀:统计行数用 COUNT(*),统计某个字段有值的行数用 COUNT(字段)。

COUNT(DISTINCT column) 也很常用,它统计的是某列去重后的非 NULL 数量。比如统计“有多少个不同城市下单”,就可以写COUNT(DISTINCT city)。如果你的需求只是判断“是否存在满足条件的记录”,用 EXISTS 通常比 COUNT(*) 更快,因为 COUNT 要数完所有符合条件的行为止,EXISTS 遇到第一条就会停下来。

5.2 SUM、AVG 与 NULL 的爱恨纠葛

SUM(字段) 会自动忽略 NULL 值,这个特性大多数人都知道。但如果被统计的字段全部都是 NULL,SUM 的结果不是 0,而是 NULL。所以前台展示时可能会看到一个空白,最好用 IFNULL 或 COALESCE 包一层,把 NULL 转成 0。

AVG 的细节更容易忽略。AVG(字段) 在计算时,分母是“该字段非 NULL 的行数”,不是表的总行数。比如你有 10 个学生,其中 3 个没有交作业导致成绩列为 NULL,AVG(score) 只算了 7 个人的平均分。如果你希望把缺考的人按 0 分计,就不能直接用 AVG,得先COALESCE(score, 0)或者用 SUM(score) / COUNT(*) 来算。

MAX 和 MIN 同样会忽略 NULL 值,这个行为在某些业务场景下是隐患。比如统计“最近一次登录时间”,如果某用户从来没有登录过,MAX(login_time) 会返回 NULL,而不是默认的 1970 年之类的东西。所以习惯是:聚合结果前想清楚,NULL 是代表“没有数据”,还是应该被当成 0 或默认值。

5.3 聚合前的过滤:WHERE 和 HAVING 的分工

这个问题在面试里经常出现,在真实场景里也经常有人写错。WHERE 是在聚合之前过滤原始行,HAVING 是在聚合之后过滤分组结果。比如要查“2024 年之后每个月的销售额,且销售额大于 1 万元”,就得先 WHERE 过滤时间范围,再 GROUP BY 月份,最后 HAVING 过滤聚合结果。

SELECT DATE_FORMAT(order_time, '%Y-%m') AS month, SUM(amount) AS month_sales FROM orders WHERE order_time >= '2024-01-01' GROUP BY month HAVING SUM(amount) > 10000;

这里有个性能原则:能用 WHERE 提前过滤的就不要留给 HAVING。因为 WHERE 在分组前就把大量无关行排除掉,聚合的数据集越小,执行速度越快。HAVING 里写的条件不能引用非聚合的非分组列,这跟 5.4 要讲的 ONLY_FULL_GROUP_BY 有关。

5.4 实操心得:ONLY_FULL_GROUP_BY 模式下的写法

从 MySQL 5.7 开始,ONLY_FULL_GROUP_BY 默认是开启的。这个 SQL 模式要求:SELECT 列表里出现的普通列,必须出现在 GROUP BY 中,或者在聚合函数里。否则会直接报错。

这个限制的初衷是消除二义性。比如:

SELECT dept, name, MAX(salary) FROM employees GROUP BY dept;

如果同部门有两个不同姓名的员工,查询应该显示谁的名字?数据库根本没有依据来选。所以 MySQL 直接拒绝执行。很多人第一次遇到这个报错,第一反应是关掉 SQL 模式,我强烈不建议这样做。关闭之后 SQL 能跑,但结果可能包含不确定的随机行,生成报表时数字对不上,排查起来更痛苦。

正确的做法是用子查询或窗口函数。比如要取每个部门工资最高的人,可以这样:

SELECT dept, name, salary FROM ( SELECT dept, name, salary, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn FROM employees ) t WHERE rn = 1;

窗口函数是理解这种需求的标准解法,它本质上是把“分组后取组内特定行”的问题从聚合函数里解放出来。如果你还没接触过窗口函数,遇到这类需求时就可以开始学习它了。

6. 类型转换与实用函数避坑清单

最后一章聊两类内容:一类是显示类型转换函数,另一类是平时容易被忽略但又很实用的其他函数。最重要的是我会整理一张速查表,把前面提到的坑汇总到一起,方便你贴在工作位上。

6.1 CAST 与 CONVERT:显式转换保平安

CAST(expr AS type) 和 CONVERT(expr, type) 是 MySQL 里的两个显式类型转换函数,功能基本一致。常见的目标类型包括 DATE、DATETIME、TIME、DECIMAL、SIGNED(带符号整数)、CHAR 等。比如:

SELECT CAST('2024-03-15' AS DATE); -- 转成日期类型 SELECT CAST('123' AS SIGNED); -- 转成整数 SELECT CONVERT('2024-01-01 10:00:00', DATETIME);

显式转换最大的价值是避免隐式转换带来不可预知的后果。举一个我排查过的真实案例:某个表的 phone 字段是 varchar,条件传了一个数字WHERE phone = 13800138000。这种写法 MySQL 会把字段值转成数字再跟数字比较,结果有两个问题:一是手机号如果前面有 0,转换后 0 会丢失,匹配不上;二是对列做隐式转换会让索引失效,造成全表扫描。正确写法是把条件里的数字写成字符串:WHERE phone = '13800138000'。

我在团队里定了条规矩:所有字符串字段跟常量比较时,常量必须加引号。这条规矩看似简单,却避免了大量因为隐式转换导致的慢查询和错误匹配。

6.2 加密与哈希函数的使用边界

MySQL 提供 MD5()、SHA1()、SHA2() 等哈希函数。哈希函数的特点是单向不可逆,相同输入产生相同输出,适合做不需要还原的数据指纹。比如对用户手机号做脱敏生成一个固定长度的标识,用于跨表关联,但不直接暴露原始号码。

需要特别提醒的是:永远不要把用户密码直接MD5(password)存进数据库。MD5 彩虹表攻击太容易了,加盐虽然能提高一些成本,但现代密码存储应该使用专门的密码哈希算法(如 bcrypt、argon2),这些算法在应用层实现,MySQL 自带的 MD5/SHA2 不是为密码存储设计的。数据库里用哈希函数,更多是生成匿名标识、校验文件内容这类需求。

另外,SHA2(str, hash_length) 需要第二个参数指定输出位数,常见的是 256,表示 SHA-256。SHA2('hello', 256)会返回一个 64 位的十六进制字符串。

6.3 函数使用避坑清单:一张表记住所有高频坑

场景容易踩的写法建议写法原因
日期相等判断WHERE DATE_FORMAT(dt, '%Y-%m-%d') = '2024-03-15'WHERE dt >= '2024-03-15 00:00:00' AND dt < '2024-03-16 00:00:00'函数套列导致索引失效
字符串列与数字比较WHERE phone = 13800138000WHERE phone = '13800138000'隐式类型转换可能让索引失效
拼接含 NULLCONCAT(a, b)`CONCAT_WS('', a, b)`
统计行数COUNT(某字段)COUNT(*)字段有 NULL 时统计结果偏小
金额字段设计FLOAT / DOUBLEDECIMAL(10, 2)二进制浮点数存在精度误差
空字符串也算空值IFNULL(name, '匿名')COALESCE(NULLIF(name, ''), '匿名')NULLIF 把空串转成 NULL
AVG 统计含缺考AVG(score)SUM(score) / COUNT(*)或先 COALESCEAVG 分母是非 NULL 行数
取分组内特定行SELECT 非聚合列 + GROUP BY窗口函数 ROW_NUMBER()满足 ONLY_FULL_GROUP_BY 规则
防除零错误完成数 / 总数完成数 / NULLIF(总数, 0)分母为 NULL 结果返回 NULL 不报错

这张表里的每一条,我都对应到真实场景踩过或帮别人排查过。函数本身不难记,难的是知道什么时候不该用、用了会产生什么连带问题。

我在实际项目里养成的一个习惯是,写完一条 SQL 先问自己三件事:列的数据类型是不是匹配?条件里有没有对列做函数包裹?聚合或分组的结果会不会因为 NULL 产生偏差?这三件事检查完,SQL 的质量基本就有底了。遇到不熟悉的函数,也别急着背,先建一张临时表,插几行边界数据(包含 NULL、空串、负数、最大长度),直接在测试环境里执行看结果,再用 EXPLAIN 看执行计划,比翻任何手写笔记都直观。

函数这东西,记不全很正常,关键是心里有一张“哪里容易翻车”的地图。希望这篇梳理能帮你在写 SQL 时少走几步弯路,把更多精力留给真正要解决的问题。

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

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

立即咨询