☰
MySQL内置函数全解析:从日期字符串到性能避坑指南
2026/10/1 3:49:41 网站建设 项目流程

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
%H24小时制10
%h12小时制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'); -- 结果:630

LAST_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); -- 结果:10

SIGN(x)返回负数、0、正数的符号,分别对应 -1、0、1:

SELECT SIGN(-9), SIGN(0), SIGN(9); -- 结果:-1 0 1

MOD(x, y)取余数,等价于x % y:

SELECT MOD(10, 3); -- 结果:1

MOD常用于分库分表、按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); -- 结果:4

EXP(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'); -- 结果:7c4a8d09ca3762af61e59520943dc26494f8941b

MD5通常用于生成文件的指纹、校验数据一致性、做签名校验。这里必须提醒:不要用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.78.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的效率和信心都会明显提升。希望这篇笔记能帮你省掉一些不必要的踩坑时间。

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

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

立即咨询