MySQL内置函数这块,是每个做后端开发的人早晚都要正面刚的东西。不管你是写业务SQL还是做数据分析,日期、字符串、数学这几类函数几乎是天天见。很多人平时只会用个NOW()和COUNT(),真到要处理复杂业务逻辑的时候,就开始在百度上翻来翻去,效率极低。我整理这份笔记的初衷很简单:把MySQL里最常用、最容易被坑的内置函数一次讲透,配合真实的业务场景给出来,让你看完就能直接用。
这篇文章适合所有跟MySQL打交道的人——刚入门的新手可以把它当速查手册,写过几年SQL的老手也能从中找到一些平时没注意到的细节和性能坑。我会按照日期函数、字符串函数、数学函数、其他相关函数四个大块来拆,每一块都会附上可复现的SQL示例和踩坑经验。
1. 日期函数:时间处理是业务开发的地基
日期函数用的频率极高,几乎所有业务表里都有create_time、update_time这类字段。你写统计报表、做定时任务、按天/月/季度聚合数据,全都离不开日期函数。这一章我把最核心的日期函数按用途拆开讲。
1.1 最常用的日期获取与格式化函数
先看获取当前时间的几个基础函数:
| 函数 | 返回值 | 示例结果 |
|---|---|---|
NOW() | 当前日期和时间 | 2025-01-15 10:30:45 |
CURDATE() | 当前日期 | 2025-01-15 |
CURTIME() | 当前时间 | 10:30:45 |
UTC_DATE() | UTC日期 | 2025-01-15 |
UTC_TIME() | UTC时间 | 02:30:45 |
这里有个真实业务中常踩的坑:NOW()返回的是会话所在时区的当前时间,而UTC_DATE()返回的是UTC时间。如果服务器时区设置不对,或者客户端连接串里没指定时区,你可能会发现“明明库里的时间是对的,但Java程序查出来差了8小时”。MySQL 8.0默认时区通常是系统时区,强烈建议在连接参数里显式写上serverTimezone=Asia/Shanghai,避免因为时区问题导致时间错乱。
格式化方面,DATE_FORMAT()是出镜率最高的函数。语法是DATE_FORMAT(date, format),format 用占位符控制输出:
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'); -- 结果:2025-01-15 10:30:45 SELECT DATE_FORMAT(NOW(), '%Y年%m月%d日 %H时%i分%s秒'); -- 结果:2025年01月15日 10时30分45秒常用占位符我列个速查表:
| 占位符 | 含义 | 示例 |
|---|---|---|
%Y | 四位年份 | 2025 |
%y | 两位年份 | 25 |
%m | 两位月份 | 01 |
%c | 月份(1-12) | 1 |
%d | 两位日 | 15 |
%e | 日(1-31) | 15 |
%H | 24小时制 | 10 |
%h | 12小时制 | 10 |
%i | 分钟 | 30 |
%s | 秒 | 45 |
%W | 星期名(英文) | Wednesday |
%w | 星期数字(0=周日) | 3 |
反向操作是STR_TO_DATE(str, format),把字符串解析成日期。比如前端传过来一个2025-01-15 10:30:45,你想按天分组,直接:
SELECT DATE_FORMAT(STR_TO_DATE('2025-01-15 10:30:45', '%Y-%m-%d %H:%i:%s'), '%Y-%m-%d');STR_TO_DATE在导入CSV、解析外部系统接口数据的时候特别有用,但要注意:如果字符串和format格式对不上,MySQL会返回NULL,不会报错。排查数据“莫名丢失”的时候,先怀疑这里。
1.2 日期计算与区间判断
日期计算的核心函数有这几个:DATE_ADD()、DATE_SUB()、DATEDIFF()、TIMESTAMPDIFF()、LAST_DAY()。
DATE_ADD(date, INTERVAL expr unit)用于给日期加上一个时间间隔,DATE_SUB()就是减。INTERVAL后面的单位支持DAY、MONTH、YEAR、HOUR、MINUTE、SECOND等。举个例子:
-- 三天后的日期 SELECT DATE_ADD(CURDATE(), INTERVAL 3 DAY); -- 结果:2025-01-18 -- 两个月前的日期 SELECT DATE_SUB(CURDATE(), INTERVAL 2 MONTH); -- 结果:2024-11-15 -- 也可以用负数,效果一样 SELECT DATE_ADD(CURDATE(), INTERVAL -3 DAY);实际工作中我更喜欢用负数写法,这样不用区分加法还是减法函数,统一用DATE_ADD就行,逻辑更简洁。注意INTERVAL后面的单位是单数,DAY不是DAYS,写错了直接报语法错误。
两个日期之间相差的天数用DATEDIFF(d1, d2),结果是d1 - d2的天数,只看日期部分,忽略时间:
SELECT DATEDIFF('2025-01-20', '2025-01-15'); -- 结果:5 SELECT DATEDIFF(NOW(), '2025-01-01'); -- 如果今天是2025-01-15,结果是14要计算精确到小时、分钟、秒的差值,就得用TIMESTAMPDIFF(unit, start, end),注意参数顺序是“结束时间在前”还是“开始时间在前”很多人容易搞混。我记的诀窍是:返回值 = end - start,单位由第一个参数指定。
SELECT TIMESTAMPDIFF(HOUR, '2025-01-15 08:00:00', '2025-01-15 18:30:00'); -- 结果:10,只计算整数小时,不到1小时直接舍去 SELECT TIMESTAMPDIFF(MINUTE, '2025-01-15 08:00:00', '2025-01-15 18:30:00'); -- 结果:630LAST_DAY(date)返回所在月份的最后一天,做月末统计特别方便:
SELECT LAST_DAY('2025-02-10'); -- 结果:2025-02-28 -- 求上个月最后一天 SELECT DATE_SUB(LAST_DAY(CURDATE()), INTERVAL 1 MONTH);1.3 日期函数实战:统计昨日/当月/近N天数据
纸上谈兵没意思,直接上几个实际场景。
场景一:统计昨天新增的用户数
很多人的第一反应是写:
SELECT COUNT(*) FROM user WHERE create_time BETWEEN '2025-01-14 00:00:00' AND '2025-01-14 23:59:59';这样写有两个问题:第一,你要手动算日期;第二,如果你漏了23:59:59,就会丢掉当天最后一秒的数据。更优雅的写法:
SELECT COUNT(*) FROM user WHERE DATE(create_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY);但要注意,DATE(create_time)包了一层函数,会导致索引失效(后面章节细说)。如果create_time有索引且数据量大,建议写成范围查询:
SELECT COUNT(*) FROM user WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY) AND create_time < CURDATE();这个写法不仅可以用到索引,而且逻辑上覆盖了“昨天00:00:00到昨天23:59:59.999”的所有数据,堪称最稳写法。
场景二:统计本月每天的订单量
用GROUP BY配合DATE_FORMAT:
SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS day, COUNT(*) AS order_count FROM orders WHERE order_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') AND order_time < DATE_ADD(DATE_FORMAT(CURDATE(), '%Y-%m-01'), INTERVAL 1 MONTH) GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d');DATE_FORMAT(CURDATE(), '%Y-%m-01')是当月第一天,DATE_ADD(..., INTERVAL 1 MONTH)是下个月第一天,用左闭右开区间[当月第一天, 下月第一天)过滤,可以捞到整个月的所有数据。
2. 字符串函数:文本处理的十八般武艺
字符串函数在数据清洗、脱敏、拼接、格式转换里天天用到。MySQL的字符串函数比较多,但核心的就那几个,玩熟了能解决绝大多数问题。
2.1 拼接、截取与替换
拼接用CONCAT(str1, str2, ...):
SELECT CONCAT('MySQL', ' ', '函数'); -- 结果:MySQL 函数这里有一个新手必踩的坑:CONCAT 只要有一个参数是 NULL,整个结果就是 NULL。很多人拼用户地址的时候,某个字段为空,结果整条地址都没了。解决办法是用CONCAT_WS或者IFNULL。
CONCAT_WS(separator, str1, str2, ...)是带分隔符的拼接,而且它会跳过NULL值,不会跳过空字符串:
SELECT CONCAT_WS('-', '2025', '01', '15'); -- 结果:2025-01-15 SELECT CONCAT_WS('-', '2025', NULL, '15'); -- 结果:2025-15(NULL被跳过)截取子串用SUBSTRING(str, pos, len)或者SUBSTR(str, pos, len),两者等价:
SELECT SUBSTRING('Hello MySQL', 7, 5); -- 结果:MySQL SELECT SUBSTRING('Hello MySQL', -5, 5); -- 结果:MySQL,负数表示从右边数第几个开始截字符串替换用REPLACE(str, from_str, to_str):
SELECT REPLACE('www.mysql.com', 'mysql', 'oracle'); -- 结果:www.oracle.com注意REPLACE是替换所有出现的子串,不是只替换第一个。如果需要只替换第一个,得自己写函数或者用正则,一般业务中很少用到。
2.2 大小写转换、去空格与填充
大小写转换没什么可说的,UPPER()和LOWER():
SELECT UPPER('mysql'), LOWER('MySQL'); -- 结果:MYSQL 和 mysql去空格有三个函数,这里要特别讲清楚:
| 函数 | 作用 |
|---|---|
TRIM(str) | 去掉字符串首尾的空格 |
LTRIM(str) | 去掉开头的空格 |
RTRIM(str) | 去掉结尾的空格 |
注意TRIM默认只去空格,不去\t制表符和换行符。数据导入时经常出现“看着没空格,但查不到”的情况,大多是隐藏的换行符在捣鬼,可以用REPLACE把\r\n清掉:
SELECT REPLACE(REPLACE(column_name, '\r', ''), '\n', '') FROM table_name;填充函数LPAD(str, len, padstr)和RPAD(str, len, padstr),常在生成流水号、订单号时用:
SELECT LPAD('42', 5, '0'); -- 结果:00042 SELECT RPAD('A1', 4, '*'); -- 结果:A1**LPAD的实际场景很多,比如把自增ID拼成固定长度的编号:CONCAT('ORD', LPAD(id, 6, '0')),生成ORD000001这样的订单号。
2.3 字符串位置与判断:LOCATE/INSTR/FIND_IN_SET
查找子串位置,最常用的是LOCATE(substr, str)和INSTR(str, substr),两者略有差别:LOCATE可以附加第三个参数指定起始位置,INSTR不行。返回的是第一次出现的位置(从1开始),找不到返回0。
SELECT LOCATE('SQL', 'MySQL SQL'); -- 结果:3 SELECT INSTR('MySQL SQL', 'SQL'); -- 结果:3 SELECT LOCATE('SQL', 'MySQL SQL', 4); -- 结果:7,从第4个字符开始找FIND_IN_SET(str, strlist)这个函数很多人首次看到会懵。它是判断str是否存在于逗号分隔的字符串列表中,常用于处理“标签”字段(比如用户表里有个 tags 字段存'1,3,5',想查包含标签3的用户):
SELECT FIND_IN_SET('3', '1,3,5'); -- 结果:2(返回位置,从1开始) SELECT FIND_IN_SET('2', '1,3,5'); -- 结果:0注意strlist里的逗号后面不要加空格,否则匹配不到。这个函数很实用,但它的性能不怎么样,因为它无法走索引,只能在数据量小的维度表上用,或者配合全文索引使用,否则还是老老实实做关联表吧。
2.4 字符串函数实战:手机号脱敏、订单号生成
脱敏是高频需求。前端展示用户列表时,手机号不能全显,要变成138****1234:
SELECT CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS masked_phone FROM user;这个写法简洁,但要注意phone字段必须是字符串类型,如果是数字,要用CAST(phone AS CHAR)转一下,否则LEFT和RIGHT会先把数字转成字符串再处理,倒也不会报错,但建议显式转换,逻辑更清晰。
订单号生成也是字符串函数的典型应用。自增ID太短、太容易被遍历,通常需要补零加前缀:
SELECT CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(id, 6, '0')) FROM orders;假设id是42,输出就是ORD20250115000042。日期部分带上之后,光看订单号就知道下单日期,后续做分库分表按订单号取模也方便。
3. 数学函数:数值运算与统计利器
数学函数不复杂,但用不对会产生很隐蔽的错误。这里我把常用函数按功能分组,重点讲易错点。
3.1 取整与随机:ROUND/CEIL/FLOOR/RAND
四个取整函数,差别必须记牢:
| 函数 | 行为 | 示例 |
|---|---|---|
ROUND(x) | 四舍五入到整数 | ROUND(2.5)= 3 |
ROUND(x, d) | 四舍五入保留d位小数 | ROUND(2.567, 2)= 2.57 |
CEIL(x)/CEILING(x) | 向上取整(进一法) | CEIL(2.1)= 3 |
FLOOR(x) | 向下取整(去尾法) | FLOOR(2.9)= 2 |
一个我实测过的细节:MySQL的ROUND()对0.5的处理是“远离零”,而ROUND(2.5)返回3。但在某些编程语言或Excel里,ROUND(2.5)可能返回2(银行家舍入)。如果你们的业务对精度要求极高,比如财务金额计算,建议在应用层进行舍入,别完全依赖MySQL。
RAND()返回[0,1)的随机浮点数,无参数每次调用结果都不同:
SELECT RAND(); -- 0.123456789... SELECT RAND(100); -- 固定种子,结果固定。排序取出随机N条,经典写法:
SELECT * FROM article ORDER BY RAND() LIMIT 5;这个写法在数据量大时性能极差,因为ORDER BY RAND()要对所有行生成随机值再排序,全表扫描,十几万行都能卡半天。如果只是要随机一条,可以先SELECT COUNT(*)然后用随机offset,或者使用ORDER BY RAND()配合LIMIT,但限定表数据量小或者只查一列时再用。更好的方案是:SELECT * FROM article WHERE id >= (SELECT FLOOR(RAND() * (SELECT MAX(id) FROM article))) ORDER BY id LIMIT 5;——但注意如果id有空洞(删除过数据),结果可能少于5条。生产环境我一般建议用应用层取随机ID的策略,数据库只负责按ID取数据。
3.2 绝对值、符号与余数:ABS/SIGN/MOD
这三个函数简单但实用场景不少。
ABS(x)绝对值:
SELECT ABS(-10); -- 结果:10SIGN(x)返回负数、0、正数的符号,分别对应 -1、0、1:
SELECT SIGN(-9), SIGN(0), SIGN(9); -- 结果:-1 0 1MOD(x, y)取余数,等价于x % y:
SELECT MOD(10, 3); -- 结果:1MOD常用于分库分表、按ID分桶、奇偶判断。比如把用户ID为奇数的分到A库,偶数的分到B库,就可以在SQL里写WHERE MOD(user_id, 2) = 0来筛选。
这里提一个信号:MySQL里MOD()的结果符号跟随被除数,也就是MOD(-10, 3)结果是 -1 而不是1。如果你在业务中期望取余结果永远是正数(比如哈希环形分桶),记得先取绝对值再取余。
3.3 幂、开方与对数:POWER/SQRT/EXP/LOG
POWER(x, y)或者POW(x, y)是 x 的 y 次方,SQRT(x)是开平方:
SELECT POWER(2, 10); -- 结果:1024 SELECT SQRT(16); -- 结果:4EXP(x)是自然常数 e 的 x 次方,LOG(x)是自然对数,LOG10(x)是常用对数。这几个在数据分析和算法计算中偶尔用到,业务SQL中不多见,但做机器学习特征工程时,有时候会用LOG压缩数据范围,比如价格取对数后再做回归。
3.4 数学函数实战:价格核算、分页随机、百分比计算
场景一:金额四舍五入
订单金额往往需要保留两位小数,但数据库里存的是DECIMAL(10,2),如果中间算出了超过两位的小数,就要用ROUND修正:
SELECT ROUND(price * quantity * discount, 2) AS actual_paid FROM order_detail;注意DECIMAL字段精度定义,如果DECIMAL(10,2),MySQL 在运算时有可能自动转换成DECIMAL高精度再计算,不用担心精度丢失,但如果你用的是FLOAT或DOUBLE,浮点误差就开始了,金额字段请务必用DECIMAL,这是血泪教训。
场景二:随机分页
随机推荐场景,可以先计算总数再随机偏移:
SET @total = (SELECT COUNT(*) FROM product); SET @offset = FLOOR(RAND() * @total); SELECT * FROM product ORDER BY id LIMIT @offset, 1;用变量方式可以避免ORDER BY RAND()全排序,但OFFSET过大时性能依然不理想。如果产品表有自增ID且基本连续,最推荐用SELECT * FROM product WHERE id >= FLOOR(RAND() * (SELECT MAX(id) FROM product)) ORDER BY id LIMIT 1;。
场景三:计算百分比
统计订单状态占比,结果保留两位小数并拼接百分号:
SELECT status, CONCAT(ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2), '%') AS percent FROM orders GROUP BY status;这里用了窗口函数SUM(COUNT(*)) OVER()求总数,是MySQL 8.0的特性。如果还跑在5.7,可以用子查询:
SELECT status, CONCAT(ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM orders), 2), '%') AS percent FROM orders GROUP BY status;注意COUNT(*) * 100.0里那个.0,目的是把整数计数转成浮点,避免整数除法导致结果为0。这个坑特别隐蔽,整数百分比算出来全是0,排查半天才发现没有转浮点。
4. 其他相关函数:流程控制、聚合与加密
除了三大类常用函数,MySQL还有一批“其他函数”,在日常写SQL时同样不可或缺。我把它们归为流程控制、聚合、加密和信息类来聊。
4.1 流程控制函数:IF、IFNULL、NULLIF、CASE WHEN
IF(expr, if_true, if_false)是写SQL时非常顺手的三目运算符:
SELECT name, IF(age >= 18, '成年', '未成年') AS age_group FROM user;IFNULL(expr1, expr2)专门处理NULL替换,只要第一个参数不为NULL就返回它,否则返回第二个:
SELECT IFNULL(nickname, '未设置昵称') FROM user;NULLIF(expr1, expr2)和IFNULL相反:如果两个参数相等,返回NULL;不相等,返回第一个参数。它常用在防止除零错误:
SELECT total_amount / NULLIF(quantity, 0) AS avg_price FROM orders;如果quantity为0,NULLIF返回NULL,整个除法结果为NULL,而不会报“division by 0”错误。你可以在应用层再判断结果是否为NULL。但是要注意,这个只是“延缓”了报错,如果avg_price在WHERE里参与比较,NULL会直接被过滤掉,别以为是bug。
CASE WHEN是功能最强大的条件表达式,写多条件判断时比多个IF嵌套清晰得多:
SELECT id, CASE WHEN score >= 90 THEN '优秀' WHEN score >= 60 THEN '及格' ELSE '不及格' END AS grade FROM exam_result;注意:CASE的每个分支返回类型最好一致,比如都是字符串。否则MySQL会隐式转换,可能带来意想不到的结果。CASE还有简单的写法CASE expr WHEN value THEN ... END,但它只能做等值判断,实际中我常用第一种搜索式。
4.2 聚合函数与分组统计:COUNT/SUM/AVG/MAX/MIN、GROUP_CONCAT
聚合函数都熟,但COUNT有好几个版本要注意:
| 表达式 | 行为 |
|---|---|
COUNT(*) | 统计行数,包含NULL |
COUNT(1) | 统计行数,包含NULL,效果等同COUNT(*) |
COUNT(column) | 统计该列“非NULL”的行数 |
COUNT(DISTINCT column) | 统计该列不同的非NULL值数量 |
统计用户数,但某个用户nickname是NULL,你写COUNT(nickname)就会少算。除非确实想统计非空昵称数,否则一律用COUNT(*)。COUNT(DISTINCT col)常用于UV统计:
SELECT COUNT(DISTINCT user_id) AS uv FROM access_log;注意DISTINCT在多列上使用是COUNT(DISTINCT col1, col2),表示两列组合的去重,不是分别去重后相加,这个容易混淆。
GROUP_CONCAT是一个特别加分的函数,它能把分组内的多行数据拼接成一个字符串。比如查一个班级所有学生的姓名:
SELECT class_id, GROUP_CONCAT(student_name ORDER BY student_id SEPARATOR ',') AS students FROM class_student GROUP BY class_id;默认分隔符是逗号,默认长度限制是group_concat_max_len参数,默认1024字节。拼接内容很长时会静默截断,造成数据缺失。如果遇到GROUP_CONCAT结果长度不对,记得先查一下这个变量,必要时调大:
SET SESSION group_concat_max_len = 10240;GROUP_CONCAT配合DISTINCT可以给拼接结果去重:GROUP_CONCAT(DISTINCT status)。
聚合函数最常用的场景是分组统计,但很多人会犯一个语义错误:在SELECT里同时出现聚合函数和非聚合列,且非聚合列不在GROUP BY中。MySQL 5.7默认只开启了ONLY_FULL_GROUP_BY的提示模式,8.0默认开启严格模式,会直接报错。解决方法是把非聚合列加到GROUP BY,或者用ANY_VALUE()包一下:
-- 错误示例(8.0会报错) SELECT name, COUNT(*) FROM user GROUP BY age; -- 正确示例 SELECT name, COUNT(*) FROM user GROUP BY name, age;4.3 加密与信息函数:MD5/SHA1/PASSWORD、DATABASE/USER/VERSION
加密函数里,最常用的是MD5(str)和SHA1(str),它们返回固定长度的十六进制哈希字符串:
SELECT MD5('123456'); -- 结果:e10adc3949ba59abbe56e057f20f883e SELECT SHA1('123456'); -- 结果:7c4a8d09ca3762af61e59520943dc26494f8941bMD5通常用于生成文件的指纹、校验数据一致性、做签名校验。这里必须提醒:不要用MD5存密码。MD5已经被大量彩虹表覆盖,跑字典极快。即便加盐,也建议用SHA2()配合高强度盐,或者干脆用bcrypt等专用密码哈希算法在应用层完成。MySQL也提供了PASSWORD(str)函数,但这个函数是给MySQL用户账号系统用的,5.7开始官方就标记废弃了,8.0里已经被移除。如果你在8.0里调用PASSWORD()会直接报错。所以业务代码里别碰它,老老实实MD5或SHA2,密码存储更推荐应用层做。
信息函数平时用得不频繁,但调试时很有用:
SELECT DATABASE(); -- 当前数据库名 SELECT USER(); -- 当前用户,如 root@localhost SELECT VERSION(); -- MySQL版本号,如 8.0.36 SELECT CONNECTION_ID(); -- 当前连接ID,用于排查线程写通用工具类SQL时,可以用VERSION()判断版本,做兼容逻辑。DATABASE()在动态SQL拼接横表转竖表、生成列名时会用到。
4.4 其他函数实战:状态判断、数据脱敏、兼容性处理
场景一:订单状态的中文展示
订单表存status枚举数字(0待支付、1已支付、2已发货、3已完成、4已取消),展示时需要翻译:
SELECT id, CASE status WHEN 0 THEN '待支付' WHEN 1 THEN '已支付' WHEN 2 THEN '已发货' WHEN 3 THEN '已完成' WHEN 4 THEN '已取消' ELSE '未知状态' END AS status_text FROM orders;场景二:cookie/敏感数据脱敏
对用户邮箱做部分遮盖:
SELECT CONCAT(LEFT(email, 3), '***', SUBSTRING(email, LOCATE('@', email))) AS masked_email FROM user;这个写法先把@之前的字符拿3个,然后拼***,再拼从@开始的域名部分。比直接在应用层处理要效率高,一次查询直接输出前端可展示的字段。
场景三:兼容MySQL 5.7和8.0
有些函数是8.0新增的,比如REGEXP_REPLACE()、REGEXP_LIKE()、RANK()等窗口函数。如果项目需要兼容5.7,千万别在SQL里直接写这些函数。遇到需要正则替换的场景,5.7里只能用多层REPLACE()嵌套模拟,或者把数据拉到应用层处理。上线前建议统一SELECT VERSION()做一次环境嗅探,避免函数解析失败。
5. 函数使用避坑指南与性能建议
前面讲函数时穿插了不少坑,这一章我集中整理最常见的坑和性能优化思路,都是实战中容易踩雷的地方。
5.1 常见错误与NULL陷阱
NULL运算传染:任何算术运算、字符串拼接里出现NULL,结果都是NULL。
SELECT 1 + NULL; -- 结果:NULL SELECT CONCAT('a', NULL); -- 结果:NULL解决办法要么用IFNULL转换,要么用COALESCE(col, 0)。COALESCE支持多个参数,返回第一个非NULL,比IFNULL更灵活。
字符串与数字隐式转换:'abc'和数字比较时,MySQL会把字符串转成数字,转换不了就变成0,从而导致逻辑错误。比如SELECT * FROM user WHERE age = 'abc',实际查询的是age = 0,匹配到一堆年龄为0(或NULL,NULL不等于0,所以不会命中)的记录。建议每次变量传入前检查类型。
日期比较别用字符串:WHERE create_time = '2025-01-15'这个是合法的,但不会命中2025-01-15 10:30:45这条记录,因为日期时间不等于日期。要么用DATE(create_time)包一层(会牺牲索引),要么写成create_time >= '2025-01-15' AND create_time < '2025-01-16',后者索引有效,强烈推荐。
5.2 函数与索引失效问题
这个坑太重要,单独说。MySQL B+树索引是按列原始值排序的。如果你在索引列上套一个函数:
WHERE DATE(create_time) >= '2025-01-01'MySQL无法利用create_time索引,因为索引里存的是完整日期时间,而你在比较的时候用的是函数处理后的值,不是原始值的范围。优化方式是把函数挪到等号另一边,或者改写成范围条件:
-- 反例:DATE(create_time) 包住了索引列,索引失效 SELECT * FROM orders WHERE DATE(create_time) = '2025-01-15'; -- 正例:等值范围,索引可用 SELECT * FROM orders WHERE create_time >= '2025-01-15 00:00:00' AND create_time < '2025-01-16';同理,LEFT(name, 1) = '张'也会导致索引失效,应该写成name LIKE '张%',这个能走索引前缀。
经验法则:你写的WHERE条件里,如果索引列被函数包裹、参与了算术运算(id + 1 = 100)或者类型转换,索引大概率失效。优化方向是改变条件写法,而不是牺牲索引去迁就函数。
5.3 版本兼容性:哪些函数是8.0新增或废弃的
MySQL 5.7到8.0,函数层面有几个值得注意的变化:
| 函数 | 5.7 | 8.0 | 备注 |
|---|---|---|---|
PASSWORD() | 可用(提示废弃) | 移除 | 报错,不能再用于业务 |
GROUP_CONCAT() | 可用 | 可用 | 行为基本一致 |
RAND() | 可用 | 可用 | 性能问题依旧 |
REGEXP_REPLACE() | 不可用 | 可用 | 正则替换神器 |
REGEXP_LIKE() | 不可用 | 可用 | 正则判断 |
窗口函数RANK()等 | 不可用 | 可用 | 需要8.0 |
如果项目要从5.7升级到8.0,先全局搜索一下PASSWORD(和REGEXP相关的代码。另外,MySQL 8.0默认字符集是utf8mb4,和5.7默认utf8有差异,可能会影响字符串函数的排序和长度计算,升级后要重新检查涉及LENGTH()、CHAR_LENGTH()的SQL。
5.4 函数调用的性能思维
避免在SELECT里对大量行使用昂贵的函数。比如ORDER BY RAND(),这个前面讲过,数据量大时全表排序会拖垮性能。再比如对每行执行MD5()做校验,能用批处理的尽量在应用层批量计算。
另一个性能点是:尽量把函数计算下推到应用层,还是上推到数据库?我的经验是:
- 简单的加减乘除、字符串截取、日期格式化,数据库算没问题。
- 复杂的加密哈希、JSON解析,能提前算好存字段,就别在SQL里算。
- 如果实在需要在SQL里用
REGEXP_REPLACE、JSON_EXTRACT对大数据集做处理,先限缩数据量,再加并行查询,别一股脑全表扫描。
遇到SQL卡慢,先EXPLAIN,看看有没有Using filesort、Using temporary,再结合索引失效的排查点逐项检查。函数类性能问题多半是索引失效引起的,所以最核心的还是那句话:不要在索引列上做函数运算。
结尾:一点个人体会
内置函数我用了这么多年,最深的感触是:函数本身不难,难的是“组合”和“边界”。日期函数要配合业务日历理解,字符串函数要时刻提防NULL和隐式转换,数学函数要清楚浮点误差和取整规则……每一个背后都藏着实际业务里会遇到的坑。平时我写SQL有个习惯,凡是拿不准的函数,先SELECT单测一遍,把各种边界输入都试一下,再放到正式查询里。尤其是NULL参与运算的场景,十个逻辑错误里至少有五个都是从NULL来的。
另一点小技巧是:把高频使用的函数片段沉淀成项目笔记。比如脱敏SQL、日期区间SQL、分组拼接SQL,这些代码套路几乎每个项目都会复用。用的时候复制过来改个表名就行,既减少出错概率,也省得每次重新查文档。MySQL内置函数不算多,静下心来花半天时间把所有函数过一遍,未来写SQL的效率和信心都会明显提升。希望这篇笔记能帮你省掉一些不必要的踩坑时间。