写这篇MySQL内置函数的分享,起因是上周帮一个学弟排查一个数据统计的问题:他写了一大段业务代码,从数据库里取出关联数据再在Java里循环做字符串拼接和日期格式化,代码又长又慢,优化之后换成数据库函数一条SQL就搞定了。这个经历让我觉得很有必要把MySQL内置函数的玩法系统整理一下。内置函数用得好,很多本来要在应用层花十几行代码处理的事,在SQL层面一个函数调用就解决了,不管你是做后端开发、数据分析还是日常提数,这都是一项绕不开的基本功。
1. 内容整体设计与思路拆解
1.1 为什么数据库函数值得专门花时间学
很多程序员在项目里习惯了“数据库只负责存数据,所有逻辑都写在代码里”的玩法。这个思路在小型系统里面没什么问题,但一旦数据量上来或者查询逻辑复杂了,你会发现把所有逻辑都拉到应用层处理,性能和开发效率都很吃亏。
举个例子,你想查一条订单数据的“下单日期是星期几”,如果在Java代码里处理,你得先把datetime字段查出来,再new一个SimpleDateFormat,然后做格式化,再判断星期几。如果列表里有几百条数据,这套循环逻辑就是几百次的重复代码。但MySQL里一行DAYOFWEEK(create_time)就搞定了,还能直接在WHERE条件里做筛选,比如“只看周一和周五下的单”,一条SQL完事。
内置函数的价值不在于让你显得技术很炫,而在于它把高频使用的数据处理操作封装成了通用能力,让数据库在返回结果之前就帮你完成数据的清洗、转换、聚合。这样应用层的代码更短、更清晰,网络传输的数据量也更少。
1.2 MySQL内置函数的整体分类体系
MySQL官方文档里的函数非常多,但如果拆开来看,日常开发真正高频用到的是这么几类:
| 函数类别 | 代表函数 | 典型使用场景 |
|---|---|---|
| 字符串函数 | CONCAT、SUBSTRING、REPLACE、LENGTH、CHAR_LENGTH | 字符串拼接、截取、替换、长度统计 |
| 数值函数 | ROUND、CEIL、FLOOR、ABS、MOD | 数值计算、取整、求余 |
| 日期时间函数 | NOW、DATE_FORMAT、DATEDIFF、DATE_ADD | 取当前时间、格式化日期、日期加减计算 |
| 流程控制函数 | IF、IFNULL、CASE WHEN | 条件判断、空值处理 |
| 聚合函数 | COUNT、SUM、AVG、MAX、MIN、GROUP_CONCAT | 统计汇总、分组拼串 |
| 加密函数 | MD5、SHA2、AES_ENCRYPT | 密码加密、数据脱敏 |
| 系统函数 | VERSION、DATABASE、USER、UUID | 获取环境信息、生成唯一标识 |
这个分类不是官方文档的目录,是实战场景导向的划分。我下面的内容也是按照这个维度来讲,而不是照着官方文档一个个函数罗列——罗列式的文档你随时可以查,但哪个场景应该用哪个函数,这个经验和判断才是真正值钱的东西。
1.3 学习内置函数的最佳路径
我的建议是不要死记硬背函数列表,而是按“场景驱动”的方式掌握。你先想清楚自己在开发中最常遇到的数据处理需求是什么,然后针对性去看这个场景里有哪些函数可用。比如你经常做报表,那就先啃日期函数和聚合函数的组合玩法;你经常处理用户输入的内容,那就把字符串函数吃透。
每个函数都建议在本地装一个MySQL,建个临时表自己敲一遍。光看不练很快就忘了,而且MySQL的版本差异会导致某些函数的行为不一样,自己动手跑一遍印象才深刻。下文中的所有SQL示例,我都建议你亲手在命令行或客户端里跑一遍。
2. 字符串函数的核心细节与实操要点
2.1 拼接函数CONCAT和CONCAT_WS的差别
字符串拼接是日常开发中用到最多的操作之一。CONCAT(str1, str2, ...)会把参数里的内容按顺序拼接成一个字符串。有一个很多人踩过的坑:如果任何一个参数为NULL,CONCAT的结果就是NULL,而不是忽略空值继续拼接。
比如你拼接用户的省市区地址,如果某个字段是NULL,整个地址就变成NULL了。解决方案有两个:一是用IFNULL把可能为空的字段包一层,二是换用CONCAT_WS(separator, str1, str2, ...)。CONCAT_WS的第一个参数是分隔符,它的好处是自动跳过NULL值,但数字0不会被跳过,这点要留意。
-- CONCAT遇到NULL直接返回NULL SELECT CONCAT('广东省', NULL, '深圳市'); -- 结果是NULL -- CONCAT_WS自动跳过NULL,结果:广东省,深圳市 SELECT CONCAT_WS(',', '广东省', NULL, '深圳市'); -- 数字0不会被跳过的坑 SELECT CONCAT_WS(',', '数量', 0); -- 结果:数量,02.2 截取函数SUBSTRING和LEFT、RIGHT的搭配使用
截取字符串的需求很常见,比如提取手机号中间四位、截取身份证号的出生日期。SUBSTRING(str, pos, len)从指定位置开始截取指定长度的字符,有几点细节要注意。
第一,MySQL的字符串位置是从1开始数的,不是从0开始,这和很多编程语言不一样。第二,SUBSTRING支持负数位置,表示从字符串末尾倒数。第三,LEFT和RIGHT是SUBSTRING的语法糖,分别从左边和右边截取固定长度的字符。
实际应用中,这三个函数经常配合着用。比如提取手机号中间四位,可以用SUBSTRING(phone, 4, 4),意思是第4位开始截4个字符;提取身份证出生日期,通常先用RIGHT把最后一位验证码去掉,再LEFT取前8位。
SELECT SUBSTRING('广东省深圳市南山区', 4, 3); -- 结果:深圳市 SELECT SUBSTRING('广东省深圳市南山区', -3, 2); -- 结果:南山 SELECT LEFT('MySQL内置函数讲解', 5); -- 结果:MySQL SELECT RIGHT('MySQL内置函数讲解', 2); -- 结果:讲解2.3 字符串长度函数LENGTH和CHAR_LENGTH的本质区别
这个坑几乎每个新手都会踩。查询字符串长度的函数有两个:LENGTH(str)和CHAR_LENGTH(str)。如果存的是纯英文字符,两个函数结果一样,但存了中文之后结果就不同了。
LENGTH返回的是字符串的字节数,而CHAR_LENGTH返回的是字符数。MySQL的utf8mb4编码下,一个中文字符占3个字节,所以LENGTH('内置函数')的结果是12(4个中文×3字节),而CHAR_LENGTH('内置函数')的结果是4。
明白了这个区别,很多问题就能解释通了。之前有人在设计表时把用户名设置为VARCHAR(6),意思是存6个字符,但代码里用LENGTH去判断用户名的长度是否合法,结果明明是6个“国”字的用户名,LENGTH算出18,被判定为超长。这种问题就是函数用错了。
SELECT LENGTH('内置函数'); -- 结果:12 SELECT CHAR_LENGTH('内置函数'); -- 结果:4 SELECT LENGTH('MySQL'); -- 结果:5 SELECT CHAR_LENGTH('MySQL'); -- 结果:52.4 替换与去空格:REPLACE和TRIM系列函数
数据清洗时最常用的就是替换和去空格。REPLACE(str, from_str, to_str)做的是全局替换,也就是说只要匹配到from_str就会替换,不是只替换第一个匹配项。
去空格有三个方向:LTRIM只去掉左侧空格,RTRIM只去掉右侧空格,TRIM同时去掉两侧空格。注意TRIM只能去掉字符串首尾的空格,字符串中间的空格它管不了。如果想把字符串中间的多余空格也压缩成单个空格,就得用REPLACE把两个连续空格替换成一个,多执行几次直到没有连续空格为止。
MySQL 8.0以上版本还有一个TRIM(BOTH 'x' FROM str)的扩展用法,可以去掉指定的字符,比如去掉字符串两侧的星号。这个用法稍冷门,但在处理一些特定格式的数据时非常好用。
SELECT REPLACE('a-b-c-d', '-', ''); -- 结果:abcd SELECT TRIM(' 前后都有空格的字符串 '); -- 去掉首尾空格 SELECT TRIM(BOTH '*' FROM '**重要信息**'); -- 结果:重要信息3. 数值与日期时间函数的实战用法
3.1 取整函数的差异与选择
数值计算中最容易搞混的就是几个取整函数。ROUND(x, d)是四舍五入,CEIL(x)或CEILING(x)向上取整,FLOOR(x)向下取整。这个差异在金额计算里非常关键。
举一个电商场景的例子:商品单价是9.9元,数量是3件,按金额计算逻辑,如果先算ROUND(9.9 * 3, 0)结果是30(四舍五入),但用CEIL(9.9 * 3)结果是30,用FLOOR(9.9 * 3)结果是29。而如果单价9.9元买了1件,ROUND(9.9, 0)是10,CEIL(9.9)是10,FLOOR(9.9)是9。
另一个容易忽略的点是ROUND的第二个参数如果省略,默认取整到0位。如果d是负数,表示小数点左侧取整,比如ROUND(1234.56, -2)的结果是1200。这个负数的用法知道的人不多,但在做千位级别的分组汇总时很实用。
SELECT ROUND(9.9), CEIL(9.9), FLOOR(9.9); -- 结果:10, 10, 9 SELECT ROUND(1234.56, -2); -- 结果:12003.2 日期时间获取函数NOW、CURDATE、CURTIME的取舍
开发中经常需要获取当前时间,MySQL提供了一组函数:NOW()返回当前的日期和时间,CURDATE()只返回日期,CURTIME()只返回时间,还有SYSDATE()也是返回日期时间。
大多数场景推荐使用NOW(),因为它在语句执行时返回一个固定的时间,整条SQL的一致性有保障。而SYSDATE()是函数执行到哪个时间点就返回哪个时刻,在一条SQL里多次调用可能得到不同的时间值。这个微妙的差异在长查询或复杂存储过程中可能引发问题。
如果只需要日期部分,用CURDATE(),比如统计当天下单数量,WHERE order_date = CURDATE()就非常直观。如果只需要时间部分,比如判断当前时间是否在允许操作的时间窗口内,用CURTIME()。
SELECT NOW(), CURDATE(), CURTIME(); -- 示例结果:2024-06-01 14:23:45, 2024-06-01, 14:23:453.3 日期格式化DATE_FORMAT的格式符详解
DATE_FORMAT是使用频率最高的日期函数,可以把日期时间转换成任意你想要的字符串格式。它支持非常多的格式符,最常用的是这些:
| 格式符 | 含义 | 示例 |
|---|---|---|
| %Y | 四位年份 | 2024 |
| %y | 两位年份 | 24 |
| %m | 两位月份 | 06 |
| %c | 月份(1-12) | 6 |
| %d | 两位日 | 01 |
| %e | 日(1-31) | 1 |
| %H | 24小时制小时 | 14 |
| %i | 分钟 | 23 |
| %s | 秒 | 45 |
| %W | 星期几的英文全称 | Monday |
| %a | 星期几的英文缩写 | Mon |
| %M | 月份的英文全称 | June |
组合使用能实现非常多样的输出,比如把时间转成2024年06月01日这种人类友好的格式,或者转成2024-06这种月份分组键。做报表按月统计时,经常用DATE_FORMAT(create_time, '%Y-%m')作为GROUP BY的维度。
3.4 日期计算的函数组合:DATE_ADD、DATEDIFF、TIMESTAMPDIFF
日期的加减计算在业务中太常见了。要查“最近7天注册的用户数”,本质就是找出注册时间在今天往前推7天之后的所有用户。DATE_ADD(date, INTERVAL expr unit)就是干这个用的。
-- 查询最近7天注册的用户 SELECT * FROM users WHERE register_time >= DATE_ADD(CURDATE(), INTERVAL -7 DAY);DATE_ADD可以加减DAY、MONTH、YEAR、HOUR、MINUTE、SECOND等时间单位。注意写法上INTERVAL后面的单位是单数形式,必须写DAY而不是DAYS。
计算两个日期之间差多少天,用DATEDIFF(date1, date2),结果等于date1减去date2的天数。如果需要更细粒度地计算差值,比如两个时间差多少小时、多少分钟,就用TIMESTAMPDIFF(unit, datetime1, datetime2)。注意DATEDIFF只算日期不看时间,TIMESTAMPDIFF看完整的时间。
-- 两个时间相差多少小时 SELECT TIMESTAMPDIFF(HOUR, '2024-06-01 08:00:00', '2024-06-01 20:30:00'); -- 结果:12,不足一小时的部分舍去4. 流程控制与聚合分析函数的进阶用法
4.1 IF函数和IFNULL函数的区别
IF(expr, val1, val2)是MySQL里的三目运算符,如果expr为真返回val1,为假返回val2。这个函数在SELECT列表里做字段级判断非常方便。比如查订单时,根据状态值直接输出可读文本:
SELECT order_id, IF(status = 1, '待支付', '已支付') AS status_text FROM orders;IFNULL(expr1, expr2)则更专注,它只做一件事:如果expr1是NULL,返回expr2,否则返回expr1。很多人在处理可空字段时经常用COALESCE,但其实如果只有两个参数,IFNULL的语义更直白。COALESCE是标准SQL里的函数,支持多个参数,返回第一个非NULL的值,能力覆盖IFNULL,但IFNULL在MySQL里的执行上有时更直观。
实际开发里,IFNULL最常见的用途是配合聚合函数。比如统计平均分时,如果某个班级没有学生,AVG返回NULL,界面显示就会是空的,用IFNULL包一层返回0,体验就好很多。
SELECT IFNULL(AVG(score), 0) AS avg_score FROM student_scores WHERE class_id = 101;4.2 CASE WHEN实现复杂条件判断
CASE WHEN是SQL里实现复杂分支逻辑的核心语法,它不是严格意义上的函数,但在功能上承担了流程控制职责。常见的写法有两种:简单CASE表达式和搜索CASE表达式。
简单CASE适合做等值判断,搜索CASE适合做范围判断。这两种写法在业务中都很常见。尤其是在做数据分箱(比如按年龄段分组、按金额大小档位归类)时,CASE WHEN几乎是唯一解。
SELECT order_id, amount, CASE WHEN amount < 100 THEN '小额订单' WHEN amount < 1000 THEN '中额订单' ELSE '大额订单' END AS order_level FROM orders;CASE WHEN还可以直接嵌套在聚合函数里面,实现“按条件统计”的效果。比如统计一个班各科成绩及格人数,可以一条SQL搞定:
SELECT SUM(CASE WHEN math_score >= 60 THEN 1 ELSE 0 END) AS math_pass, SUM(CASE WHEN english_score >= 60 THEN 1 ELSE 0 END) AS english_pass FROM student_scores;4.3 聚合函数COUNT、SUM、AVG的隐藏规则
聚合函数是数据分析的基石,但有几个隐藏规则值得反复强调。COUNT(*)和COUNT(1)没有实质区别,都会统计所有行,包括NULL值行。但COUNT(column)只统计该列非NULL的行数。这是面试高频考点,也是日常开发容易踩的坑。
SUM函数在遇到全NULL时返回NULL而不是0,AVG自动忽略NULL值计算平均值,MAX和MIN也会忽略NULL值。这些规则在做数据报告时必须心里有数,否则报表里会出现莫名其妙的空白。
GROUP_CONCAT是很多新手没见过但非常实用的聚合函数,它能把分组内多个值拼接成一行字符串。比如查一个用户的所有标签,用GROUP_CONCAT可以把标签名拼成逗号分隔的一串文本。行转列操作里它也是常客。
SELECT user_id, GROUP_CONCAT(tag_name ORDER BY tag_id SEPARATOR '、') FROM user_tags GROUP BY user_id;4.4 利用GROUP_CONCAT实现行列转换
GROUP_CONCAT默认的分隔符是逗号,可以通过SEPARATOR指定其他分隔符。需要注意的一个参数是group_concat_max_len,默认值是1024字节,也就是说拼出来的字符串超过1024字节会被截断。查大量数据拼接时容易踩到这个限制,遇到就调整系统变量。
-- 查看当前长度上限 SHOW VARIABLES LIKE 'group_concat_max_len'; -- 设置更长(当前会话生效) SET SESSION group_concat_max_len = 1000000;下面是一个基于GROUP_CONCAT做简单行转列的经典例子:把每个学生的各科成绩转成一行展示。虽然真正的动态行转列要配合存储过程或拼接SQL实现,但静态列数的场景用GROUP_CONCAT加CASE WHEN就够了。
SELECT student_name, MAX(CASE WHEN course = '语文' THEN score END) AS 语文, MAX(CASE WHEN course = '数学' THEN score END) AS 数学, MAX(CASE WHEN course = '英语' THEN score END) AS 英语 FROM student_scores GROUP BY student_name;5. 加密、系统函数与空值处理技巧
5.1 密码存储与数据加密的MD5和SHA2
MySQL内置了MD5和SHA2系列加密函数,常用于密码的哈希存储。MD5(str)返回32位的十六进制字符串,SHA2(str, hash_length)支持224、256、384和512位。
需要注意,MD5在安全性上已经不适合单独用于密码存储了,因为彩虹表攻击很容易破解普通密码的MD5值。更稳妥的实践是使用加盐(salt)的方式,把一个随机字符串拼接在密码后面再做哈希。不过这个拼接和哈希的过程放在数据库里做还是应用层做,要看你团队的架构约定,从安全角度推荐在应用层做完整处理,Database函数可以作为辅助。
另一个容易理解错的应用是数据脱敏。比如日志表里存了手机号,想在查询时只显示前3位和后4位,可以结合字符串函数实现CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4))。这种处理就是典型的利用函数在展示层做脱敏。
SELECT MD5('abc123'); -- 结果:e99a18c428cb38d5f260853678922e03 SELECT SHA2('abc123', 256); -- 结果:6ca13d52ca70c883e0f0bb101e425a89e8624de51db2d2392593af6a841180905.2 UUID函数生成全局唯一标识
UUID()返回一个36位的字符串,格式是8-4-4-4-12的十六进制,包含连字符。它是根据时间和机器特征生成的全局唯一标识,适合作为分布式环境下的业务主键,或者记录消息的唯一标识。
但直接拿UUID当表的主键有一个问题:它是无序的字符串,InnoDB的聚簇索引在插入时会频繁引起页分裂和随机IO,性能比自增整数主键差。常见做法是去掉连字符存成CHAR(32),或者把UUID转成BINARY(16)存储,也可以单独建一列存UUID作为业务标识,表主键仍然用自增整型。
SELECT UUID(); -- 示例结果:550e8400-e29b-41d4-a716-446655440000 SELECT REPLACE(UUID(), '-', ''); -- 去掉连字符后的32位字符串5.3 系统信息函数与调试辅助
系统信息函数虽然不像字符串或日期函数那样高频,但在调试和运维时很实用。VERSION()返回MySQL版本号,DATABASE()返回当前数据库名,USER()返回当前连接的用户名和主机名,CONNECTION_ID()返回当前连接的ID。
这些函数在排查问题时非常有用。比如你在写一个长时间运行的存储过程时,可以用CONNECTION_ID()来确认当前执行的是哪个会话,方便在另一个会话里做监控或终止。在一条SQL里把版本、当前库、当前用户都查出来,也算是一个快速诊断环境的小工具。
SELECT VERSION(), DATABASE(), USER(); -- 示例结果:8.0.36, test_db, user@localhost5.4 空值处理的最佳实践
NULL值的处理是SQL开发里的一个永恒主题。我之前带项目时见过不少因为NULL导致计算结果出错的案例。除了前面提到的IFNULL,还要掌握COALESCE和NULLIF。
NULLIF(expr1, expr2)的逻辑是:如果expr1等于expr2,返回NULL,否则返回expr1。这个函数最经典的应用是做除法时的防零处理,比如计算某个比例的增长率,除数为0时结果为NULL,再配合IFNULL转成0。
-- 计算增长率,当上次数值为0时返回0 SELECT IFNULL( (current_value - last_value) / NULLIF(last_value, 0), 0 ) AS growth_rate FROM metrics;另一个容易被忽略的是,NULL和空字符串是不一样的。判断一个字段“为空”要区分两种场景:如果业务上把空字符串视为无效数据,就得WHERE column IS NULL OR column = ''。如果只判断NULL,漏掉了空字符串的情况,统计结果就会有偏差。
6. 实操过程:用一个综合案例打通内置函数的组合使用
6.1 场景设定与建表
讲完各类函数,用一个综合案例来演示函数的组合使用。假设我们有一个用户订单表,需求是按月统计每个用户的订单概况,输出每个用户的当月下单次数、订单总金额、首单时间、末单时间,以及一个“订单活跃度”评级。
先建一张简单的订单表作为演示环境:
CREATE TABLE user_orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, amount DECIMAL(10, 2) NOT NULL, order_time DATETIME NOT NULL, remark VARCHAR(255) ); INSERT INTO user_orders (user_id, amount, order_time, remark) VALUES (1, 199.00, '2024-05-01 10:30:00', '会员日下单'), (1, 59.90, '2024-05-15 14:20:00', NULL), (1, 299.00, '2024-06-02 09:00:00', '618预售'), (2, 89.00, '2024-05-03 11:00:00', '普通购买'), (2, 159.00, '2024-06-15 20:15:00', NULL), (3, 399.00, '2024-06-01 08:30:00', '大额订单');这是一个很常见的数据分布:同一个用户在不同月份有多笔订单,有备注也有空值备注。
6.2 用字符串和日期函数做字段处理
第一步先处理原始字段。订单时间直接展示不友好,备注字段为NULL时需要显示为“无备注”,金额需要格式化成统一的样子。此时字符串函数和日期函数上场。
SELECT user_id, DATE_FORMAT(order_time, '%Y-%m-%d %H:%i') AS order_time_text, CONCAT('¥', FORMAT(amount, 2)) AS amount_text, IFNULL(remark, '无备注') AS remark_text FROM user_orders;FORMAT函数可以把数字格式化为带千位分隔符的字符串,FORMAT(199.00, 2)结果是“199.00”。加上¥前缀是为了展示层面的美观,但要注意拼接后的字段已经变成字符串类型,不能参与后续的数值计算。这也是一个实战中的常见提醒:展示和计算要分开处理,不要在一个字段上既做格式化又做聚合。
6.3 用流程控制函数做业务评级
对这个需求,我想根据每月的订单情况给出一个评级:订单金额累计大于500元的为“高价值”,100到500之间的为“普通”,低于100的为“低价值”。这个逻辑用CASE WHEN可以很自然地表达出来。
还可以把上个月有没有下过单作为一个维度,这就用到了LAG窗口函数,但MySQL 8.0才支持窗口函数。如果还在用5.7或更老的版本,就需要通过自连接实现。这里直接用CASE WHEN做金额分档,逻辑清晰而且兼容性好。
SELECT user_id, DATE_FORMAT(order_time, '%Y-%m') AS order_month, SUM(amount) AS month_amount, COUNT(*) AS order_count, CASE WHEN SUM(amount) >= 500 THEN '高价值' WHEN SUM(amount) >= 100 THEN '普通' ELSE '低价值' END AS value_level FROM user_orders GROUP BY user_id, DATE_FORMAT(order_time, '%Y-%m') ORDER BY user_id, order_month;这条SQL同时用了日期函数做分组、聚合函数做统计、流程控制函数做评级。GROUP BY后面直接用DATE_FORMAT的表达式,意味着查询结果里也必须包含这个表达式或者用别名方式处理。ORDER BY这里用了别名,MySQL是允许的,这一点比较友好。
6.4 聚合与拼串:汇总用户备注信息
如果产品经理还想看每个用户每个月在订单备注里都写了什么,可以用GROUP_CONCAT做汇总。备注为空的跳过,非空的拼接在一起。再结合CONCAT做一段可读的汇总文案。
SELECT user_id, DATE_FORMAT(order_time, '%Y-%m') AS order_month, GROUP_CONCAT( IFNULL(remark, '未填写') ORDER BY order_time SEPARATOR ';' ) AS remark_summary FROM user_orders GROUP BY user_id, DATE_FORMAT(order_time, '%Y-%m');这条SQL展示了字符串函数、流程控制函数和聚合函数三层嵌套的组合用法。IFNULL把空备注填成“未填写”,ORDER BY在GROUP_CONCAT内部排序,SEPARATOR指定用中文分号拼接。嵌套的层次很多,但只要理解了每个函数做的事情,整条SQL的逻辑还是很好读的。
7. 常见问题与排查技巧实录
7.1 字符串比较中的大小写问题
MySQL在默认的排序规则(collation)下,字符串比较是不区分大小写的,所以WHERE name = 'abc'可以匹配到名为“ABC”的记录。但有些业务场景需要区分大小写,这就需要在SQL里显式声明。
方法论上,推荐在表设计阶段就明确字段的排序规则。如果必须临时区分大小写,可以在查询时给字段加BINARY关键字,或者用STRCMP函数做精确比较。STRCMP(str1, str2)如果两个字符串相同返回0,第一个小于第二个返回负数,否则返回正数。
-- 区分大小写比较 SELECT * FROM users WHERE BINARY username = 'Admin'; -- 使用STRCMP SELECT STRCMP('abc', 'abc'); -- 结果:0 SELECT STRCMP('abc', 'abd'); -- 结果:-17.2 日期函数传参格式不正确导致的隐式转换问题
很多人在使用STR_TO_DATE或日期比较时,直接传字符串进去,MySQL做了隐式类型转换,多数时候能正常工作,但性能可能受到影响。典型的例子是WHERE order_time > '2024-06-01',当order_time是datetime类型时,这个字符串会被转换为日期时间再比较,转换本身没问题。
真正容易出问题的是用STR_TO_DATE解析字符串时的格式不匹配。STR_TO_DATE要求格式符与实际字符串严格对应,比如字符串是“2024/06/01”,格式必须写成'%Y/%m/%d',如果写成年月日加连字符的格式,解析就会失败返回NULL。
SELECT STR_TO_DATE('2024/06/01', '%Y/%m/%d'); -- 正常返回2024-06-01 SELECT STR_TO_DATE('2024/06/01', '%Y-%m-%d'); -- 结果:NULL,格式不匹配7.3 隐式类型转换导致的函数行为意外
MySQL会自动把不同类型的值做隐式转换,这在函数调用时经常会引发一些意想不到的结果。比如字符串和数字比较时,MySQL会把字符串开头的数字部分转成数字,如果开头不是数字就转成0:
SELECT '5abc' + 0; -- 结果:5 SELECT 'abc' + 0; -- 结果:0这个问题在内部函数传参时也同样存在。比如IF函数里,如果返回值一个是字符串一个是数字,MySQL会根据规则做类型转换,导致原本期望的字符串“001”被转成了数字1。遇到这类问题,排查思路是先用SELECT单独跑一下函数表达式,确认输出类型和值是否符合预期,再放进复杂的业务SQL里。
7.4 内置函数在索引使用上的注意事项
一个重要原则需要反复强调:在WHERE条件里对索引列使用内置函数,通常会导致索引失效。比如WHERE DATE(create_time) = CURDATE()这种写法,看似很优雅,但MySQL在大多数情况下无法直接使用create_time上的索引,因为它需要先对每行的create_time做DATE函数计算,再进行等值比较。
优化方式是把函数写在条件值那侧,而不是写在索引列上。上面的查询可以改写成范围查询:
-- 不推荐:对索引列使用函数 SELECT * FROM orders WHERE DATE(create_time) = CURDATE(); -- 推荐:使用范围条件 SELECT * FROM orders WHERE create_time >= CURDATE() AND create_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY);如果确实需要经常按日期维度查询,另一个方案是额外冗余一个日期字段或使用生成列(MySQL 5.7及以上支持),让查询可以走索引。这两种方案在实际项目中我都用过,根据业务场景选一个就好。
7.5 排查函数问题的通用套路
最后分享一个排查函数相关问题的通用方法。遇到函数结果和预期不一致时,不要直接在大SQL里反复试错,而是单独把函数表达式拿出来,在最简环境下验证。
-- 第一步:单独验证函数返回值 SELECT DATE_FORMAT(NOW(), '%Y-%m-%d'); -- 第二步:验证嵌套组合 SELECT STR_TO_DATE(DATE_FORMAT(NOW(), '%Y-%m-%d'), '%Y-%m-%d'); -- 第三步:再放回业务SQL中验证这个套路看起来基础,但效率极高。很多复杂问题其实是多个函数组合在一起后数据类型变化引起的连锁反应,单独验证每个函数都能跑对,组合起来却错了。一层层拆开跑一遍,问题基本就暴露了。
MySQL内置函数这个知识面看似散,其实非常有逻辑。先把字符串、数值、日期这三类高频函数吃透,再掌握流程控制和聚合函数的组合用法,最后在前面加一点加密、系统函数的了解,日常开发中90%以上的数据处理场景都是可以覆盖的。纸上得来终觉浅,最关键的还是回到你的业务里去用。下次遇到要写循环拼接的代码时,停下来想一想:这个需求,是不是一条SQL函数就能完成的?