☰
MySQL内置函数详解:日期、字符串、数学函数实战指南
2026/10/1 3:49:41 网站建设 项目流程

做后端开发,尤其是每天跟数据打交道的,几乎离不开SQL。写SQL的时候最烦什么?不是join查不出来,而是数据处理那点破事——日期要格式化、字符串要拼接、数值要取整,你要是全靠应用层代码去搞,逻辑绕一大圈不说,性能还浪费。MySQL内置函数就是干这个用的,把常见的日期计算、字符串处理、数学运算、流程判断直接下沉到SQL层面,一条语句解决问题。

这篇文章不聊高深理论,就把MySQL内置函数系统地过一遍:日期函数、字符串函数、数学函数,以及其他常用函数。我会结合真实开发场景来聊,每个函数都给出可复现的案例,包括参数踩坑和版本注意事项。适合刚接触MySQL的初学者,也适合想系统梳理一遍函数体系的开发同学。看完了你至少能知道:遇到时间区间统计怎么写、字符串截取怎么不踩下标坑、金额计算怎么避免精度翻车。

1. 内置函数概览:为什么你该系统掌握这些函数

MySQL内置函数是一组预定义好的功能模块,你只需要调用函数名、传对参数,就能拿到处理后的结果。它覆盖了数据操作的大部分高频场景,分类清晰,语法统一,用好了能省掉大量应用层代码。

1.1 MySQL内置函数分类一览

函数按用途大致可以分成以下几类,我先把最常用的列出来:

函数分类代表函数高频应用场景
日期函数NOW、DATE_FORMAT、DATEDIFF、DATE_ADD时间格式化、时间区间统计、年龄计算
字符串函数CONCAT、SUBSTRING、REPLACE、TRIM字段拼接、文本截取、脏数据清洗
数学函数ROUND、CEIL、FLOOR、ABS金额取整、分页随机、数值精度处理
流程控制函数IF、IFNULL、CASE WHEN业务逻辑分支、空值兜底
类型转换函数CAST、CONVERT、COALESCE字符串转数字、字符集转换
聚合函数COUNT、SUM、AVG、GROUP_CONCAT统计报表、分组汇总
加密函数MD5、SHA2、AES_ENCRYPT数据脱敏、校验和计算

实际开发中,日期、字符串、数学这三类用得最频繁,后两类属于关键时刻能救命的那种。就像你工具箱里的螺丝刀,可能不是每天用,但真需要的时候没有它,你就得拿牙咬。

1.2 为什么优先用内置函数而不是程序处理

很多人有个习惯:SQL只负责查询,所有处理逻辑都丢给Java、Python、Go。这种思路在数据量小的时候没啥问题,但一旦数据量上来,问题就明显了。

第一是性能差距。你写一条SQL从数据库拉出上万行原始数据,再在应用层循环处理,跟直接在SQL里用函数处理完只返回结果,数据库网络传输的数据量差了几个量级。数据库本身的函数是编译好的,执行效率远高于你应用层的循环。

第二是逻辑统一。如果多个服务都依赖同一套处理逻辑,比如时间格式化方式、字符串脱敏规则,放在SQL层写死,所有调用方拿到的结果是一致的。要是各端自己实现,格式对不上就是线上事故。

第三是开发效率。一条SQL能完成的事,没必要写十几行代码。举个最典型的例子:统计最近7天每天订单量,应用层方案是先查出所有订单,再在内存里写循环按天分组;SQL方案就是一行GROUP BY加一个日期函数的事,语句短、逻辑直观,还不用写完代码再写单元测试。

当然也要提醒一句:内置函数不是越多越好。过于复杂的逻辑塞进SQL里,可读性会急剧下降,而且后续不好维护。我的习惯是:单表内的数据处理优先用函数,跨表汇总的复杂逻辑还是放应用层更合适。

2. 日期函数:时间处理的核心

日期函数是我每天都会碰到的函数类别。业务系统里几乎每张表都有create_time、update_time,报表统计、数据筛选、定时任务全都离不开日期计算。MySQL的日期函数设计得相当全面,关键是你得知道什么场景用哪个。

2.1 最常用的日期获取与格式化函数

先过一遍最基础的:

  • NOW():返回当前日期和时间,格式是YYYY-MM-DD HH:MM:SS。它取的是语句执行时刻的时间,同一个事务内多次调用结果一致。
  • CURDATE():只返回当前日期,结果是YYYY-MM-DD。
  • CURTIME():只返回当前时间,结果是HH:MM:SS。
  • DATE_FORMAT(date, format):把日期按指定格式输出,这是用得最多的一个格式化函数。
  • STR_TO_DATE(str, format):反过来,把字符串解析成日期,常用于导入外部数据时的字段转换。

看几个实际执行结果:

SELECT NOW(); -- 2025-01-15 14:23:45 SELECT CURDATE(); -- 2025-01-15 SELECT CURTIME(); -- 14:23:45 SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'); -- 2025-01-15 14:23:45 SELECT DATE_FORMAT(NOW(), '%Y年%m月%d日'); -- 2025年01月15日

DATE_FORMAT的格式符是新手最容易记混的地方,我把常用的整理一下:

格式符含义示例
%Y四位年份2025
%y两位年份25
%m两位月份01
%d两位日期15
%H24小时制小时14
%i分钟23
%s秒45
%W星期名Wednesday
%M月份名January

STR_TO_DATE的格式串必须跟输入字符串严格匹配,它对不上就直接返回NULL,不会报错。这个特性经常坑人,数据查着查着发现少了,实际上是转化失败被过滤了:

SELECT STR_TO_DATE('2025-01-15 14:30:00', '%Y-%m-%d %H:%i:%s'); -- 2025-01-15 14:30:00 SELECT STR_TO_DATE('2025/01/15', '%Y-%m-%d'); -- NULL,因为字符串是斜杠分隔,格式串却写了横杠

2.2 日期运算与区间计算

时间格式化只是基础,真正常用的是日期运算:加几天、减几个月、算两个日期间隔。这几个函数必须熟练:

  • DATE_ADD(date, INTERVAL expr unit):给日期加上指定时间间隔。
  • DATE_SUB(date, INTERVAL expr unit):给日期减去指定时间间隔。
  • DATEDIFF(date1, date2):返回两个日期相差的天数,是date1减date2。
  • TIMESTAMPDIFF(unit, datetime1, datetime2):返回两个时间在指定单位上的差值,这里是datetime2减datetime1。

INTERVAL支持的单位相当丰富:DAY、WEEK、MONTH、QUARTER、YEAR、HOUR、MINUTE、SECOND都可以用。比如:

SELECT DATE_ADD('2025-01-15', INTERVAL 3 DAY); -- 2025-01-18 SELECT DATE_SUB('2025-01-15', INTERVAL 1 MONTH); -- 2024-12-15 SELECT DATEDIFF('2025-03-01', '2025-01-15'); -- 45 SELECT TIMESTAMPDIFF(MONTH, '2024-10-01', '2025-01-15'); -- 3

这里要特别注意TIMESTAMPDIFF和DATEDIFF的单位差异。DATEDIFF只算天数,TIMESTAMPDIFF可以按MONTH、YEAR算,但两者的参数顺序相反,一个是前减后,一个是后减前。我写的时候经常搞混,后来干脆记住:TIMESTAMPDIFF后面那个减前面那个,刚好和DATEDIFF相反。

日期运算最常见的场景之一是计算用户年龄:

SELECT TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age FROM users;

这样算出来的年龄是按整年计的,比你自己在程序里处理闰年、月份差要省心得多。

2.3 日期函数实战:按月分组统计订单

说了这么多,看一个完整的实战场景。假设orders表里有create_time和amount字段,要统计最近6个月每月的订单数和销售额,SQL怎么写:

SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) GROUP BY month ORDER BY month DESC;

这里有两个细节值得展开。

第一,为什么用DATE_FORMAT而不是直接GROUP BY create_time。create_time是精确到秒的时间戳,按它分组一个月会拆成几十万组,显然不对。DATE_FORMAT把时间统一成YYYY-MM格式,才能按月份归组。

第二,GROUP BY后面直接用了别名month。这是MySQL的一个扩展特性,允许GROUP BY和ORDER BY引用SELECT子句里的别名。但要注意,在ONLY_FULL_GROUP_BY模式下,SELECT里的非聚合列必须出现在GROUP BY中,写成GROUP BY month而month本身是表达式的结果,在MySQL 5.7及以后版本默认开启的严格模式下是可以的,但在某些其他数据库里不行。

我早期写过一条老SQL,用的是MySQL 5.5的宽松模式,GROUP BY写得很随意。后来升级到MySQL 5.7,默认SQL_MODE带上了ONLY_FULL_GROUP_BY,那条SQL直接报错,排查了半天才想起来是这个模式的问题。遇到这种情况,要么把列全部加进GROUP BY,要么改SQL_MODE,但一般不建议改全局配置,调SQL更靠谱。

3. 字符串函数:文本处理工具箱

字符串函数是我在数据清洗场景里最依赖的东西。导入的Excel数据格式乱七八糟,手机号带空格、姓名有全角字符、地址字段里混着换行符,全靠字符串函数一条条清理。这一节把常用的挨个过一遍,每个都配上实际用法。

3.1 拼接、截取与替换

先看最常用的几个:

  • CONCAT(str1, str2, ...):拼接字符串,有任何一个参数是NULL,结果就是NULL。
  • CONCAT_WS(sep, str1, str2, ...):带分隔符的拼接,第一个参数是分隔符,会跳过NULL值。
  • SUBSTRING(str, pos, len):从指定位置截取字符串,注意下标从1开始,不是从0开始。
  • SUBSTRING_INDEX(str, delimiter, count):按分隔符截取,count为正数取左边第几个,负数取右边第几个。
  • REPLACE(str, from_str, to_str):替换字符串中的子串。
  • LEFT(str, len)和RIGHT(str, len):从左边或右边截取固定长度。

代码示例:

SELECT CONCAT('Hello', ' ', 'World'); -- Hello World SELECT CONCAT_WS('-', '2025', '01', '15'); -- 2025-01-15 SELECT SUBSTRING('hello world', 7, 5); -- world SELECT LEFT('hello world', 5); -- hello SELECT RIGHT('hello world', 5); -- world SELECT SUBSTRING_INDEX('技术,生活,随笔', ',', 1); -- 技术 SELECT SUBSTRING_INDEX('技术,生活,随笔', ',', -1); -- 随笔

SUBSTRING从1开始计数这一条,对从Python、JavaScript转来的同学是个典型的坑。Python里字符串从0开始,写习惯了直接套过来,算出来的结果永远差一位。我建议在本地跑个测试确认结果再往正式SQL里写。

CONCAT和CONCAT_WS的区别值得多说一句。CONCAT碰到NULL直接返回NULL,比如用户表里last_name有值、first_name是NULL,CONCAT(last_name, first_name)结果整个变成NULL。而CONCAT_WS会跳过NULL,在拼接用户姓名时用CONCAT_WS更安全:

SELECT CONCAT_WS(' ', last_name, first_name) AS full_name FROM users;

REPLACE最经典的场景是手机号脱敏。把中间四位替换成星号:

SELECT REPLACE('13812345678', SUBSTRING('13812345678', 4, 4), '****'); -- 138****5678

3.2 长度、大小写与去空格

这类函数看着简单,但每次用都能遇到边界情况:

  • LENGTH(str):返回字符串的字节数,UTF-8下中文占3个字节。
  • CHAR_LENGTH(str):返回字符串的字符数,中文算1个字符。
  • UPPER(str)和LOWER(str):转大写和小写。
  • TRIM(str):去掉首尾空格,默认只去空格。
  • LTRIM(str)、RTRIM(str):只去左侧或只去右侧空格。
  • LPAD(str, len, padstr)、RPAD(str, len, padstr):用指定字符串填充到指定长度。

LENGTH和CHAR_LENGTH的区别必须搞清楚。用UTF-8编码的MySQL里,LENGTH('中文')返回6,CHAR_LENGTH('中文')返回2。如果你要判断用户输入的密码长度是否达到8位,用CHAR_LENGTH才是符合直觉的判断。用LENGTH的话,8个中文字符会被误判为24个字符。

LPAD也特别实用。比如订单号是自增数字,要生成固定6位的编号:

SELECT LPAD('123', 6, '0'); -- 000123

再比如带计数值的排行榜名次拼接:

SELECT RPAD(CONCAT(rank, '. ', user_name), 20, ' ') FROM ranking;

TRIM有个扩展用法很多人不知道,它可以去掉指定字符,不只是空格:

SELECT TRIM(BOTH 'x' FROM 'xxxhelloxxx'); -- hello SELECT TRIM(LEADING '0' FROM '00123'); -- 123

LEADING、TRAILING、BOTH三个关键字分别控制去掉开头、结尾、两边。这在清洗格式化编号时很常用,有些导入数据里编号前补了零,要去掉补零就用TRIM(LEADING '0' FROM code)。

3.3 字符串函数实战:清洗手机号和地址数据

实际业务里,字符串函数经常组合使用。举个例子:用户表里的phone字段混入了空格、横线、+86前缀,要把它们统一清洗成纯11位数字:

UPDATE users SET phone = REPLACE(REPLACE(REPLACE( TRIM(phone), ' ', '' ), '-', ''), '+86', '') WHERE phone LIKE '% %' OR phone LIKE '%-%' OR phone LIKE '+86%';

从里到外逐层剥离:先去首尾空格,再去中间空格,再去横线,最后去掉+86前缀。这里注意REPLACE嵌套顺序,必须先处理空格再处理横线,如果顺序反了,横线处理完又冒出来空格的可能性不大,但按依赖关系排总是稳妥的。

再举一个地址拆分的场景。tags字段存的是逗号分隔的标签,比如"技术,生活,随笔",要取第一个标签分组统计:

SELECT SUBSTRING_INDEX(tags, ',', 1) AS main_tag, COUNT(*) FROM articles GROUP BY main_tag;

SUBSTRING_INDEX还有一层巧妙用法:取最后一个分隔符之后的内容。写-1就是从右往左取第一段,比如拿到一个文件路径要取文件名:

SELECT SUBSTRING_INDEX('/var/log/mysql/error.log', '/', -1); -- error.log

这个用法比先算长度再截取要干净得多。

4. 数学函数:数值计算的基石

数学函数在业务代码里用得没有日期和字符串频繁,但一旦用上就是关键节点。尤其是金额计算、百分比统计、随机抽样这几个场景,用错函数会让数据直接出错,而且很难一眼看出来。

4.1 取整与四舍五入

取整相关的函数是重灾区,因为不同函数的行为差异很大:

  • ROUND(x, d):四舍五入,保留d位小数。d不写默认取整到个位。
  • CEIL(x)/CEILING(x):向上取整,返回大于等于x的最小整数。
  • FLOOR(x):向下取整,返回小于等于x的最大整数。
  • TRUNCATE(x, d):直接截断,保留d位小数,不四舍五入。

标准示例:

SELECT ROUND(3.456, 2); -- 3.46 SELECT ROUND(3.5); -- 4 SELECT CEIL(3.01); -- 4 SELECT FLOOR(3.99); -- 3 SELECT TRUNCATE(3.456, 2); -- 3.45

CEIL和FLOOR的差异在分页计算里表现得很明显。假设每页显示10条数据,总共27条,你要算页数,用CEIL(27/10)得到3页,用FLOOR(27/10)得到2页,少了最后一页。

TRUNCATE和ROUND容易混淆,记住一点:TRUNCATE是直接砍掉后面的位数,不做任何进位判断。如果做账务对账,必须用ROUND保持四舍五入的一致性。

这里必须提一个ROUND的经典坑:在MySQL中,ROUND(2.5)的结果是3,ROUND(3.5)的结果是4,没毛病。但ROUND(2.675, 2)的结果很可能是2.67而不是2.68。原因是2.675在计算机二进制浮点表示里其实是一个略小于2.675的数,四舍五入后掉到了2.67。这不是MySQL的bug,是所有浮点数的通病。做金额计算,尽量用DECIMAL类型传进ROUND,不要用FLOAT/DOUBLE。

4.2 绝对值、符号与随机数

这类函数不多,但个个都有典型使用场景:

  • ABS(x):绝对值,计算误差范围时常用。
  • SIGN(x):返回-1、0、1,对应负数、零、正数。
  • MOD(x, y):取模运算,等价于x % y。
  • RAND():返回0到1之间的随机数,每次调用结果不同。
  • RAND(seed):传入固定种子,返回固定的随机数序列,用于测试复现。

示例:

SELECT ABS(-10); -- 10 SELECT SIGN(-5); -- -1 SELECT MOD(10, 3); -- 1 SELECT RAND(); -- 0.134567890123456(每次都不一样) SELECT RAND(42); -- 固定种子下每次返回同一个数

SIGN在业务里用得很少,但做数据对账时很有用。比如对比两个库的金额差,SIGN(a - b)可以直接判断谁大谁小,省得写一堆CASE WHEN。

RAND最常见的用法是随机抽样,但这里有个大坑:ORDER BY RAND()在表数据量大时性能极差。它需要对全表每一行生成随机数,然后做排序文件,数据量一旦过百万,查询能卡到让你怀疑服务器是不是挂了。

更好的随机取一条记录方案是利用主键范围:

SELECT * FROM users WHERE id >= (SELECT FLOOR(RAND() * (SELECT MAX(id) FROM users))) LIMIT 1;

这个方案走的是主键索引,不用全表排序,速度提升明显。当然它有个小缺陷:如果id有空洞,抽到的概率会略有不均,但对非严格的随机场景完全够用。

4.3 数值运算的精度陷阱

精度问题值得单独拿出来说。MySQL里的数值类型分几档:整数类型的INT/BIGINT,近似小数的FLOAT/DOUBLE,精确小数的DECIMAL。

FLOAT和DOUBLE是二进制浮点,内部用近似方式存储,某些十进制小数没法精确表达。经典的例子是:

SELECT 0.1 + 0.2; -- 0.30000000000000004

这在MySQL里可能显示0.3,但做等值比较时,WHERE price = 0.3可能匹配不到预期的行,因为浮点数底层不是精确的0.3。

DECIMAL则是以字符串方式存储的精确小数,运算结果不会丢精度。业务里涉及金额、税率、单价,一律用DECIMAL。比如订单金额字段,定义成DECIMAL(10, 2),运算不管怎么算,结果都精确到分。

再比如百分比计算,SUM(amount) / SUM(total) * 100,结果可能是一长串小数,用ROUND包一层,并指定小数位数:

SELECT ROUND(SUM(paid_amount) / SUM(total_amount) * 100, 2) AS paid_rate FROM orders;

注意运算顺序:先乘再除还是先除再乘会带来精度差异。一般先算比例再乘100,能保留更多有效位数。

5. 其他常用函数:流程控制与类型转换

日期、字符串、数学是三个大类,但日常SQL里还有几类函数出镜率也很高:流程控制、类型转换、加密和聚合。这些函数单独看都不难,但组合起来能写出相当灵活的统计SQL。

5.1 流程控制函数

SQL里也能写if-else,靠的是这三个函数:

  • IF(expr, if_true, if_false):简单二分支判断。
  • IFNULL(expr1, expr2):expr1为NULL时返回expr2,否则返回expr1。
  • CASE WHEN...THEN...ELSE...END:多分支判断,可以替代IF的嵌套写法。

IF的用法:

SELECT name, score, IF(score >= 60, '及格', '不及格') AS result FROM exam_scores;

CASE WHEN适合多分支场景:

SELECT name, score, CASE WHEN score >= 90 THEN '优秀' WHEN score >= 60 THEN '及格' ELSE '不及格' END AS level FROM exam_scores;

这两个写法改造成统计逻辑也很方便。比如把用户按年龄分桶统计:

SELECT CASE WHEN age < 18 THEN '未成年' WHEN age < 30 THEN '青年' WHEN age < 60 THEN '中年' ELSE '老年' END AS age_group, COUNT(*) FROM users GROUP BY age_group;

IFNULL是最常用的空值兜底函数,报表里尤其多:

SELECT id, IFNULL(user_name, '未知用户') AS user_name FROM user_info;

注意IFNULL只接受两个参数,如果你有多个字段依次取第一个非NULL的需求,用COALESCE更合适。

5.2 类型转换与比较函数

类型转换在数据集成场景里天天见。导入的数据往往是字符串,要参与计算必须转成数字:

  • CAST(expr AS type):标准类型转换语法。
  • CONVERT(expr, type):功能和CAST类似。
  • CONVERT(expr USING charset):用于字符集转换,这是CAST做不到的。
  • COALESCE(val1, val2, ...):返回参数列表中第一个非NULL的值。
  • NULLIF(expr1, expr2):两个参数相等时返回NULL,否则返回expr1。

示例:

SELECT CAST('123' AS UNSIGNED) + 1; -- 124 SELECT CAST('2025-01-15' AS DATE); -- 2025-01-15 SELECT CONVERT('中文' USING utf8mb4); -- 中文(转成utf8mb4编码)

CAST的坑在于:把非数字字符串转成数字时,在严格SQL模式下会直接报错。比如CAST('abc' AS UNSIGNED),非严格模式下返回0,严格模式下直接抛异常。所以ETL场景里,转之前最好先做一层清洗,或者用REGEXP判断一下是不是纯数字。

NULLIF的典型使用场景是防除零:

SELECT SUM(amount) / NULLIF(COUNT(DISTINCT user_id), 0) FROM orders;

如果去重用户数为0,NULLIF返回NULL,整个除法结果是NULL而不是报错。你可以在应用层判断空值并给出友好提示。

5.3 加密与聚合函数

加密函数在数据脱敏和密码存储场景会用到。

  • MD5(str):生成32位十六进制校验和。
  • SHA1(str):生成40位十六进制校验和。
  • SHA2(str, hash_len):更安全的SHA2系列,hash_len支持224、256、384、512。
SELECT MD5('password123'); -- 482c811da5d5b4bc6d497ffa98491e38

这里要说一句重要的:MD5和SHA1都已经不是安全加密算法了,主要用于文件校验、请求签名、数据去重。真正的用户密码存储,不建议在数据库层做,应该用应用层加盐哈希方案,比如bcrypt、scrypt。数据库层做密码加密一旦泄露,没加盐的哈希表很容易被彩虹表打穿。

聚合函数是报表统计的主力,每个都很常用:

  • COUNT(*):统计所有行数。
  • COUNT(column):统计某列非NULL的行数。
  • SUM(column):求和。
  • AVG(column):求平均值。
  • MAX(column)、MIN(column):最大最小值。
  • GROUP_CONCAT(column):把分组内的值拼成一个字符串。

COUNT()和COUNT(column)的差异要拎清楚。COUNT()统计行数,不管列里有没有NULL;COUNT(column)只统计该列非NULL的行。统计用户数量,用COUNT(*),统计填写了手机号的用户数量,用COUNT(phone)。

GROUP_CONCAT的灵活度很高,可以自定义分隔符、排序方式:

SELECT department_id, GROUP_CONCAT(user_name ORDER BY user_id SEPARATOR ',') AS user_list FROM employees GROUP BY department_id;

这个函数在做标签聚合、关联列表展示时特别好用,一条SQL就把分组的明细拼成一个字符串,省得应用层再循环拼接。

6. 常见问题与排查技巧

函数用多了,踩坑是必然的。这一节我把实际开发里遇到的问题整理成速查内容,既有报错排查,也有性能避坑,还有几条独家经验。

6.1 函数使用高频报错整理

报错信息或异常现象常见原因解决方案
SQL语法错误 near 'xxx'函数名拼错、括号不匹配、引号不成对逐个检查参数括号,本地先跑一遍
查询结果莫名变少STR_TO_DATE、CAST转换失败返回NULL先单独跑转换函数,用IS NULL筛选排查
ONLY_FULL_GROUP_BY相关报错GROUP BY没包含SELECT里的非聚合列补齐GROUP BY字段,或用ANY_VALUE包一层
中文字符统计长度不对误用LENGTH,按字节数统计了换成CHAR_LENGTH
金额对账差几分钱用了FLOAT/DOUBLE存金额统一改DECIMAL,计算时用ROUND包裹
随机抽取超时用了ORDER BY RAND()改成基于主键的随机区间方案

举一个我真实排查过的案例。某次报表按月统计订单量,发现某月数据明显偏少。我一开始以为是订单确实少,后来对比订单明细才发现,当月有一部分订单的create_time字段格式异常,进入了脏数据区。因为SQL里用了STR_TO_DATE预处理时间,异常数据被转成NULL后,WHERE条件直接过滤掉了。

排查方法是拆解SQL:先查原始数据,SELECT create_time FROM orders WHERE create_time LIKE '%/%',看到有斜杠格式的日期,再单独跑STR_TO_DATE验证结果确实是NULL。最终在写入端做了格式统一,报表恢复正常。这种事最好的避免方式就是:写函数前先看看原始数据长什么样,别想当然。

6.2 函数导致索引失效的问题

这是性能层面最大的坑。MySQL的B+树索引存储的是原始列值,一旦你在WHERE条件里对索引列应用了函数,优化器就无法利用索引做范围扫描,只能退化成全表扫描。

最典型的错误写法:

-- 这条SQL无法使用create_time上的索引 SELECT * FROM orders WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2025-01-15';

正确的写法是转成范围条件:

SELECT * FROM orders WHERE create_time >= '2025-01-15 00:00:00' AND create_time < '2025-01-16 00:00:00';

范围写法能让优化器直接走索引,尤其是在create_time数据量特别大的时候,性能差距可能是几十倍。

同理,对字符串列做LENGTH判断、对数值列做ABS运算,只要在索引列上套了函数,索引都会失效。我有个习惯性检查:写完WHERE条件后扫一眼,索引列上有没有函数包裹,有就重构写法。

如果你确实需要按日期格式化结果来过滤,又希望走索引,MySQL 5.7以上的版本可以用生成列(Generated Column)解决。先给表加一个虚拟列存格式化结果,再在这个虚拟列上建索引:

ALTER TABLE orders ADD COLUMN create_month VARCHAR(7) GENERATED ALWAYS AS (DATE_FORMAT(create_time, '%Y-%m')) STORED; CREATE INDEX idx_create_month ON orders(create_month);

之后直接按create_month过滤,索引就能用上。这个方案适合固定格式的查询,灵活度不高,但性能确实稳。

6.3 版本差异与兼容性注意

MySQL升级带来的函数行为变化,是老项目最容易踩的雷。

MySQL 5.7到8.0的升级,有几个点值得注意。首先是默认SQL_MODE变了,ONLY_FULL_GROUP_BY默认开启,老的宽松GROUP BY SQL直接报错。其次是字符集默认从utf8升级为utf8mb4,LENGTH()这类函数按字节统计的结果会变。再就是8.0移除了一些老函数和别名,比如PASSWORD()函数被去掉了,如果你还在用老语法,升级前就要排查。

还有一个容易被忽略的点:SQL_MODE里的严格模式会影响函数对非法输入的处理。非严格模式下,CAST('abc' AS UNSIGNED)返回0;严格模式下直接报错。同一个SQL在不同模式的库上跑出不同结果,这是很多环境差异问题的根源。

务实的建议是你维护一套函数测试用例。把常用的日期函数、字符串函数、数学函数各写一条查询,跑一遍记录结果,移数据库或升级版本时先跑测试用例,比上线后查数据错误要省事得多。

写在最后

我在实际项目里的体会是:日期函数和字符串函数几乎每天都在用,报表统计、数据清洗、接口联调都离不开;数学函数频率稍低,但金额计算和随机抽样的场景,一次用错就能造成大麻烦。把函数体系学清楚之后,写复杂SQL的思路会开阔很多,有些统计需求你第一反应会变成"这能不能用一条SQL搞定",而不是闷头写代码。

最后分享一个小技巧:遇到不熟的函数,先在本地MySQL跑一跑,看看返回结果,别急着往生产环境写。尤其是STR_TO_DATE、CAST、ROUND这些边界情况多的函数,提前验证一遍能省很多事。MySQL也提供了SELECT @@version;可以查看版本,不同版本之间函数行为有差异,本地验证时留意版本号。

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

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

立即咨询