做后端开发这几年,MySQL是我每天都要打交道的东西,而在排查过的慢查询和错误SQL里,至少有三成问题出在函数使用上。MySQL函数用好了能让SQL简洁高效,用不好轻则结果不对、重则让索引失效直接全表扫描。这篇博文我想系统梳理一下MySQL函数的分类、高频用法、性能陷阱和面试常考点,适合刚开始写SQL的在校生、后端新人,也适合准备跳槽想快速过一遍函数知识点的朋友。
我会把重点放在“怎么用”和“为什么这么用”上,遇到参数就讲参数,遇到坑就讲坑,尽量少说虚的。
1. MySQL函数体系与技术定位
1.1 函数到底在SQL里扮演什么角色
SQL是一种声明式语言,你告诉数据库“我要什么”,数据库自己决定“怎么算”。函数就是这中间最基础的计算单元——输入一个或几个值,经过处理返回一个结果。它可以出现在SELECT列表、WHERE条件、ORDER BY排序、GROUP BY分组、HAVING过滤,甚至JOIN的ON条件里。
举个例子,一条很普通的统计SQL:
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) AS order_cnt, ROUND(SUM(amount), 2) AS total_amount FROM orders WHERE status = 'PAID' AND create_time >= '2024-01-01' GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY day DESC;这里DATE_FORMAT负责把时间戳转成日期字符串,COUNT和SUM负责聚合,ROUND负责金额保留两位小数。没有函数,这种按天汇总的业务需求只能拉到应用层用Java或者Python慢慢算,效率天差地别。
但要提醒一句:函数是“计算单元”,不是“黑魔法”。它有没有副作用、是否确定、能不能走索引,直接决定了这条SQL在百万级数据上跑100毫秒还是跑10秒。
1.2 内置函数、存储过程和自定义函数的区别
很多人会把函数和存储过程搞混,面试也经常问。我当时整理过一张对比表,几乎能覆盖所有考点:
| 对比项 | 内置函数 | 自定义函数 | 存储过程 |
|---|---|---|---|
| 定义位置 | 数据库内置 | 用户CREATE FUNCTION | 用户CREATE PROCEDURE |
| 返回值 | 必须有返回值 | 必须有返回值 | 可以没有返回值,通过OUT参数返回 |
| 调用方式 | SELECT中直接调用 | SELECT中调用 | CALL proc_name() |
| 能否在SQL中嵌套 | 可以 | 可以 | 不可以 |
| 典型用途 | 字符串/日期/数值处理 | 封装复杂计算逻辑 | 封装多步骤业务流程 |
| 事务控制 | 不支持 | 不支持 | 支持 |
内置函数是MySQL自带的,像CONCAT、NOW、IFNULL这些,性能经过充分优化,能直接用就直接用。自定义函数适合封装一些特别通用的计算逻辑,比如“根据经纬度算距离”,但要注意函数里不能有SELECT查询副作用,也不能控制事务。存储过程则偏“流程”,适合批量数据处理或者复杂的业务逻辑编排。
1.3 函数确定性与非确定性
这也是一个容易被忽略的点。确定性函数指同样的输入一定得到同样的输出,比如CONCAT('a','b')永远返回'ab'。非确定性函数则相反,比如NOW()、RAND()、UUID(),每次调用结果可能不一样。
这个特性对主从复制和binlog影响很大。我在实际项目里就遇到过,主库执行了带UUID()的INSERT,从库回放时重新执行UUID(),生成的值和主库不一致,导致两边数据对不上。MySQL的binlog默认是STATEMENT格式时,复制依赖SQL重放,非确定性函数就是个定时炸弹。所以生产环境建议把binlog格式设为ROW,或者在关键业务里用应用层生成好的序列号而不是数据库函数。
2. 高频内置函数的分类拆解与实战示例
2.1 字符串函数:从拼接、截取到聚合
字符串函数是日常用得最多的,先说拼接。CONCAT可以把多个字段连起来,但有个坑:只要有一个参数是NULL,整个结果就是NULL。比如用户表里first_name和last_name,如果last_name允许为空,CONCAT出来就是空的,前端展示直接空白。
解决办法有两个:一是用IFNULL把NULL转成空字符串,二是直接用CONCAT_WS,这个函数专门处理带分隔符的拼接,而且会跳过NULL值:
SELECT CONCAT_WS(' ', first_name, last_name) AS full_name FROM users;截取字符串用SUBSTRING,注意位置从1开始,不是从0。配合CHAR_LENGTH可以处理中文,CHAR_LENGTH按字符计数,LENGTH按字节计数。utf8mb4下一个中文占3字节,用LENGTH('你好')得到6,用CHAR_LENGTH('你好')得到2,这个在分页、截断文本时特别容易踩坑。
字符串聚合要重点提一下GROUP_CONCAT。它可以把分组里的多个值拼成一个字符串,比如查每个用户的订单编号列表:
SELECT user_id, GROUP_CONCAT(order_no SEPARATOR ',') FROM orders GROUP BY user_id;默认拼接长度上限是1024字节,超出会被截断。处理大量数据时,先SET SESSION group_concat_max_len = 102400;。另外GROUP_CONCAT内部排序要用ORDER BY子句,不是写在GROUP BY后面:
SELECT user_id, GROUP_CONCAT(order_no ORDER BY create_time DESC SEPARATOR '|') FROM orders GROUP BY user_id;2.2 数值函数:四舍五入的边界问题
数值函数看起来简单,但钱相关的地方一定要小心。ROUND四舍五入,TRUNCATE直接截断,FLOOR向下取整,CEIL向上取整。做金额计算时,如果字段是DECIMAL类型,ROUND的结果精度可控;如果字段是FLOAT或DOUBLE,浮点误差叠加可能让你账目对不上。
我自己碰到过一个问题:对一批价格求和后再ROUND,和先ROUND每一条再求和,结果对不上。原因是浮点底层用二进制表示小数,0.1加0.2这类精度问题在MySQL里同样存在。解决方案是用DECIMAL(10,2)存价格,或者统一在最后一步保留精度。
MOD取模还能用来做分组抽样,比如按用户ID取模分片,MOD(user_id, 10)把用户均匀分成10片,分表分库、灰度发布时候很好用。
RAND()生成0到1之间的随机数,配合ORDER BY可以随机抽几条数据:
SELECT * FROM products ORDER BY RAND() LIMIT 5;但这条SQL在数据量大时性能很差,因为RAND()对每一行都会计算一次,而且ORDER BY无法用索引。真要随机抽一条,可以先SELECT COUNT(*)拿总数,再用LIMIT offset, 1跳过去。
2.3 日期时间函数:格式化、计算与时区
日期函数是最容易出错的类别,没有之一。先分清NOW()、CURDATE()、CURTIME()——NOW返回日期时间,CURDATE只返回日期,CURTIME只返回时间。还有一个CURRENT_TIMESTAMP,和NOW()等价,但语义更偏向“标准SQL”。
日期差计算有DATEDIFF和TIMESTAMPDIFF。DATEDIFF只算天数差,TIMESTAMPDIFF可以指定单位:
SELECT TIMESTAMPDIFF(MONTH, hire_date, NOW()) AS months_worked FROM employees;DATE_FORMAT控制日期显示格式,但格式符特别容易记混。%Y是四位年份,%y是两位;%m是月份01-12,%i是分钟,不是%M。我见过无数新人把分钟写成%M,%M实际是月份的英文名,January这种。完整日期转换用STR_TO_DATE,它是DATE_FORMAT的逆操作:
SELECT STR_TO_DATE('2024-08-15 14:30:00', '%Y-%m-%d %H:%i:%s');日期加减用DATE_ADD和DATE_SUB,interval是关键字:
SELECT DATE_SUB(NOW(), INTERVAL 7 DAY) AS last_week;还有一个特别实用的函数LAST_DAY,返回某月的最后一天。对账、报表跑批经常要算“当月剩余天数”:
SELECT LAST_DAY('2024-02-01'); -- 返回2024-02-29时区方面,NOW()返回的是当前会话时区的时间,受time_zone参数控制。如果你的数据库连接串或者会话没有统一时区,同一台服务器上不同客户端查NOW()可能看到不同结果。常用做法是连接串里加serverTimezone=Asia/Shanghai,或者数据库层面统一把time_zone设为+08:00。
2.4 条件函数与逻辑控制:IF、CASE WHEN、IFNULL
条件函数里CASE WHEN是真正的万能表达式,支持多分支和复杂条件,而且标准SQL都认它。IF函数适合简单二选一,嵌套多了可读性直线下降,不建议超过两层。
SELECT user_id, CASE WHEN total_amount >= 10000 THEN 'VIP' WHEN total_amount >= 1000 THEN '白银' ELSE '普通' END AS user_level FROM user_stats;IFNULL(a, b)表示a为NULL时返回b。COALESCE更灵活,可以传多个参数,返回第一个非NULL值:
SELECT COALESCE(phone, email, '无联系方式') FROM users;这里有三个典型的NULL判断误区:一是用= NULL判断,结果永远是NULL,永远不为TRUE,必须用IS NULL;二是用NULL和空字符串比较,NULL是“没有值”,空字符串是“长度为0的字符串”,业务上要分清;三是IFNULL的第二个参数如果是字符串类型,注意和第一个参数隐式转换,可能导致返回类型和预期不一致。
2.5 类型转换:CAST、CONVERT与隐式转换陷阱
类型转换函数CAST和CONVERT作用基本一样,语法略不同:
SELECT CAST('123' AS SIGNED); -- 转成整数 SELECT CONVERT('123', DECIMAL(10,2)); -- 转成小数真正的坑在于隐式转换。当字符串字段和数字比较时,MySQL会尝试把字符串转成数字,如果字段是索引列,这个转换会导致索引失效。举个例子,mobile字段是VARCHAR类型,存的是手机号,查询用WHERE mobile = 13800138000,MySQL会先把mobile列里的所有值转成数字再比较,走不了索引,直接全表扫描。应该写成WHERE mobile = '13800138000',让参数类型和字段类型一致。
反过来,数字字段和字符串参数比较影响小一些,因为参数可以转换成数字类型,转换的是常量而不是列。但最稳妥的写法永远是:字段类型是什么,参数就传什么。
2.6 常用函数速查表
| 类别 | 函数 | 关键点 |
|---|---|---|
| 字符串 | CONCAT / CONCAT_WS | CONCAT遇NULL返回NULL |
| 字符串 | SUBSTRING / CHAR_LENGTH | 位置从1开始,中文按字符数算 |
| 字符串 | GROUP_CONCAT | 长度上限1024,可用SEPARATOR指定分隔符 |
| 数值 | ROUND / TRUNCATE | ROUND四舍五入,TRUNCATE直接截断 |
| 日期 | DATE_FORMAT / STR_TO_DATE | %i分钟,%m月份,区分大小写格式符 |
| 日期 | TIMESTAMPDIFF / DATE_ADD | 日期差和日期加减的常用选择 |
| 条件 | IFNULL / COALESCE | COALESCE支持多参数,返回首个非NULL |
| 类型 | CAST / CONVERT | 隐式转换会导致索引失效,特别注意 |
| 聚合 | COUNT / SUM / AVG / MAX / MIN | COUNT(*)和COUNT(字段)语义不同 |
3. 窗口函数:MySQL 8.0里的进阶玩法
3.1 为什么窗口函数能解决“分组TopN”难题
MySQL 8.0之前,没有窗口函数,想查“每个品类销量前三的商品”非常痛苦,通常要用自连接、子查询或者用户变量。我记得5.7时代写过一段用变量模拟ROW_NUMBER的SQL,又长又绕,加个过滤条件还容易错。MySQL 8.0引入窗口函数之后,这类问题一行OVER子句就搞定了。
窗口函数的核心思想是:保留所有明细行,在每行旁边开一个“窗口”,对窗口内的行做计算。它和GROUP BY分组聚合最大的区别就是——不合并行。
3.2 四大类窗口函数
窗口函数分为四类:排名函数、聚合函数、取值函数、分布函数。日常最常用的是前两类。
排名函数有三个,区别要背清楚:
- ROW_NUMBER():从1开始连续编号,不重复
- RANK():有并列时跳号,1,1,3
- DENSE_RANK():有并列时不跳号,1,1,2
给一个经典例子,查每个品类销量前三:
SELECT category_id, product_id, sales, RANK() OVER (PARTITION BY category_id ORDER BY sales DESC) AS rk FROM product_sales;聚合函数做窗口,比如累计求和、移动平均。订单流水加累计金额:
SELECT order_id, amount, SUM(amount) OVER (ORDER BY order_id) AS running_total FROM orders;这里的ORDER BY不是排序,而是定义窗口的“滑动方向”——从第一行累加到当前行。要控制移动平均的范围,用ROWS BETWEEN:
SELECT day_id, revenue, AVG(revenue) OVER (ORDER BY day_id ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS avg_7d FROM daily_revenue;3.3 同比环比和TopN场景
环比和同比是数据分析的常客。LAG和LEAD可以取前后行的值:
SELECT month_id, revenue, LAG(revenue, 1) OVER (ORDER BY month_id) AS prev_month, ROUND((revenue - LAG(revenue, 1) OVER (ORDER BY month_id)) / LAG(revenue, 1) OVER (ORDER BY month_id) * 100, 2) AS mom_ratio FROM monthly_revenue;注意LAG在边界行返回NULL,环比计算那里会出现NULL,应用层要处理。
分组取最新一条记录,用ROW_NUMBER配合子查询:
SELECT user_id, order_id, create_time FROM ( SELECT user_id, order_id, create_time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) AS rn FROM orders ) t WHERE rn = 1;窗口函数性能上也有代价,如果PARTITION BY切分的窗口很多、每个窗口数据量很大,排序内存消耗会很高。数据量大的表建议先过滤再排序,缩小进入窗口函数的数据集。
3.4 MySQL 8.0与窗口函数的兼容性
窗口函数是MySQL 8.0的卖点,如果你还在用5.7,需要先升级再享受。升级前注意业务SQL兼容性,8.0默认字符集是utf8mb4,认证插件从mysql_native_password换成了caching_sha2_password,老客户端连接可能报错。这些都是升级前的功课,不展开,但心里要有数。
4. 函数使用中的性能陷阱与优化方案
4.1 索引列上套函数,优化器救不了你
这是函数使用里最严重的性能杀手:在WHERE条件里对索引列套函数,等于把索引列变成了“加工后的值”,B+树里按原始值排序的索引自然就失效了。
新人最常见的写法:
SELECT * FROM orders WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-01-15';这里create_time字段即使有索引也没用。正确的写法是转成范围查询:
SELECT * FROM orders WHERE create_time >= '2024-01-15 00:00:00' AND create_time < '2024-01-16 00:00:00';这样优化器可以用索引定位到区间,性能天壤之别。判断方法很简单,用EXPLAIN看一眼type和key,套函数时type通常是ALL,改范围查询后变成range,key也带上了索引名。
4.2 隐式类型转换:看不见的函数调用
前面提到过隐式类型转换,这里再敲一次黑板。MySQL在比较不同类型的值时,会隐式调用转换函数,转换列本身就让索引失效。
最典型的两个场景:
- VARCHAR列和数字比较,如WHERE mobile = 13800138000,mobile列被转成数字
- 字符串日期和日期类型比较,如WHERE create_time = '2024-01-15',create_time如果是DATETIME类型,字符串会被转成日期,这种常量转换反而不影响索引
调试技巧:EXPLAIN里看possible_keys和key。possible_keys有索引但key为NULL,基本可以断定是类型转换或者函数导致无法使用索引。
4.3 ORDER BY和GROUP BY里的函数
ORDER BY使用函数,比如ORDER BY DATE_FORMAT(create_time, '%Y-%m'),结果一定走filesort,数据量一大就慢。更隐蔽的是,MySQL 8.0之前在GROUP BY里用函数,也会失效。解决思路是冗余一个专门的排序列,比如订单表额外加一个month字段,写入时算好,查询直接排序走索引。
4.4 排序规则对函数结果的影响
字符串排序结果受COLLATION影响。MySQL里utf8mb4_general_ci和utf8mb4_unicode_ci对英文大小写、中文拼音的排序规则不一样。同样一个姓名列表,用不同排序规则可能出来不同顺序。如果你在函数里用了UPPER或LOWER来规避大小写问题,先检查列上有没有建索引,有索引的话套函数一样失效。
排序规则还影响GROUP BY的分组口径。比如大小写不同的邮箱地址,在utf8mb4_general_ci下会被当成一组,因为排序规则不区分大小写;如果想严格区分大小写,要用utf8mb4_bin。这类问题在用户表去重时尤其致命。
4.5 聚合函数和GROUP BY的常见坑
聚合函数COUNT、SUM、AVG、MAX、MIN配合GROUP BY用,有几点需要注意。COUNT(*)统计行数,COUNT(字段)统计该字段非NULL的个数,COUNT(DISTINCT 字段)做去重统计,三者语义完全不同。
AVG遇到NULL值自动忽略,如果业务需求要把NULL当成0算,要先IFNULL(x, 0)再AVG。SUM同理,全NULL时SUM返回NULL,前端展示要处理。
MySQL 5.7及以上默认开了ONLY_FULL_GROUP_BY,SELECT的列必须要么出现在GROUP BY里,要么包在聚合函数里。老项目迁移到新版本时特别容易踩这个错,报错信息是“which isn't in GROUP BY clause”。
4.6 用EXPLAIN定位函数问题
排查函数性能问题,EXPLAIN是第一个工具。重点看几个字段:
| 字段 | 含义 | 关注点 |
|---|---|---|
| type | 访问类型 | ALL最差,range/ref好,const最好 |
| possible_keys | 可能用到的索引 | 有值但key为空,说明用不了 |
| key | 实际使用的索引 | NULL说明没走索引 |
| rows | 预估扫描行数 | 行数越大性能越差 |
| Extra | 额外信息 | Using filesort、Using temporary要警惕 |
我的习惯是先看type是不是ALL,再看possible_keys有没有索引。如果possible_keys有值而key为NULL,九成是WHERE条件里套了函数或者做了隐式类型转换,按这个方向排查通常很快。
5. 常见报错与排查实录
5.1 “无法将‘mysql’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”
这个报错本质上不是MySQL函数的问题,而是Windows环境变量PATH没配好。系统找不到mysql.exe这个可执行文件,就在终端里报“无法识别”。很多人在装完MySQL后第一次打开命令行敲mysql,就撞上这个。
解决思路分三步走:第一,找到mysql.exe的安装路径,一般长这样D:\mysql-8.0.36-winx64\bin;第二,把这个路径加到系统环境变量PATH里;第三,关掉当前终端重新打开,让环境变量生效。加了PATH还不行,就在终端里用全路径调用验证:
D:\mysql-8.0.36-winx64\bin\mysql.exe -uroot -p这个排查思路对任何命令都通用。热搜里那一串“claude无法识别”“git无法识别”“npm无法识别”,基本是同一类问题:要么没安装,要么装了但不在PATH里。
5.2 ERROR 2002 (HY000): Can't connect to local MySQL server through socket
这个报错写得很直白:通过socket文件连不上本地MySQL服务器。常见原因很朴素——MySQL服务根本没启动。Linux下先确认服务状态:
systemctl status mysql sudo systemctl start mysql如果服务已经启动还是报这个错,可能是socket文件路径不对。MySQL的socket文件默认在/var/run/mysqld/mysqld.sock,配置文件里改了路径的话客户端和服务器要一致。还有一个排查技巧:强制走TCP/IP协议绕过socket:
mysql -h 127.0.0.1 -P 3306 -uroot -p加上-h 127.0.0.1会走TCP连接,不走socket文件。如果这个能连上,问题就锁定在socket配置上。
5.3 函数相关的SQL报错
报错“FUNCTION xxx.count doesn't exist”,通常是函数名写错或者函数不存在。MySQL函数名对大小写不敏感,但拼写错误不会自动纠正。遇到不熟悉的函数先查文档或者用SHOW FUNCTION STATUS看看。
报错“Invalid use of group function”,表示你把聚合函数用错位置了。典型场景是在WHERE条件里写COUNT(*) > 10,聚合函数的过滤必须用HAVING,WHERE是在分组之前执行的,聚合函数这时候还没算出来。这个报错在面试题里也经常出现,本质上还是对SQL执行顺序不熟。
5.4 函数报错速查表
| 报错信息 | 原因 | 处理方法 |
|---|---|---|
| FUNCTION xxx.count doesn't exist | 函数名拼写错误或不存在 | 核对函数名 |
| Invalid use of group function | 聚合函数出现在WHERE子句 | 改为HAVING |
| which isn't in GROUP BY clause | ONLY_FULL_GROUP_BY模式限制 | 把非聚合列加到GROUP BY或包进聚合函数 |
| Data truncation: Truncated incorrect value | 类型转换时数据截断 | 检查源数据格式,用CAST显式转换 |
| GROUP_CONAT ... truncated | group_concat_max_len超限 | 临时调大group_concat_max_len |
5.5 快速定位函数问题的排障思路
我排查函数相关问题有一套固定的流程,对新手比较友好:第一步,把函数表达式替换成常量,看SQL本身能不能查通。比如DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-01-15'改成create_time = '2024-01-15',结果正常说明SQL骨架没问题,问题出在函数处理上。第二步,单独执行函数,SELECT DATE_FORMAT('2024-01-15 12:00:00', '%Y-%m-%d'),确认函数本身返回是否符合预期。第三步,把数据量缩小,LIMIT 10看一眼实际值,确认是不是数据里混了脏数据导致函数报错。
这套方法帮我解决过很多“看起来莫名其妙”的问题,尤其是日期格式和NULL值。
6. 写在最后:函数使用的个人体会
函数这个东西,学的时候觉得API繁多记不住,用的时候又总觉得“再给我一个函数就能搞定”。我的经验是不要贪多,先把字符串拼接、日期格式化、条件判断、聚合统计、窗口排名这五类吃透,日常覆盖八成场景。剩下的用到再查文档,没必要死记硬背。
真正拉开水平差距的,不是记住了多少个函数,而是在写SQL的那一刻能不能意识到“这里用函数会有什么代价”。每次在条件列上准备套函数的时候多问自己一句:能不能改成范围查询?这个习惯帮我少踩了很多性能坑。最后一个小技巧:生产环境写SQL,尽量用确定性的、不依赖会话状态的函数,少用NOW()、RAND()这类非确定性函数做核心业务逻辑,灾备恢复和主从复制的时候会省很多麻烦。