做数据分析这几年,我越来越确信一件事:SQL函数不是可有可无的语法糖,而是把数据从“能看”变成“能用”的核心工具。很多人写SQL时习惯把数据拉到本地再用代码处理,或者靠应用层写一堆冗长逻辑,结果效率低不说,维护起来也痛苦。其实MySQL内置的这批函数,就是数据库层最顺手的一套工具箱——从字段拼接、数值格式化,到日期窗口计算、分组聚合统计,几乎覆盖了日常取数和分析的完整链路。
这篇文章我想把自己平时用得最多、也踩过坑的函数整理成一份完整清单。目标不是让你背函数名,而是帮你理解每个函数的适用场景、注意事项,以及几个经常被忽略的性能细节。无论你是刚接触SQL的开发新手,还是已经写了一年半载查询的运营分析师,这份指南应该都能让你少走几次弯路。文中我会反复用一张模拟订单表 orders(字段:id、order_no、user_id、category、price、quantity、pay_time、status)和用户表 users(id、name、email、phone、created_at)来演示,所有SQL在 MySQL 8.0 上都可以直接跑。
1. 数学计算类函数:看似基础,用对场景才是关键
1.1 四舍五入与取整:ROUND、CEIL、FLOOR、TRUNCATE
数值处理函数最常用的是 ROUND,它把结果四舍五入到指定精度。比如统计订单均价的保留两位小数,或者把金额从分转成元,都离不开它。ROUND 有两个参数:第一个是原始数值,第二个是保留的小数位数。这里有个很多人没注意的点:第二个参数还可以传负数,ROUND(1234.56, -2) 会得到 1200,相当于在十位直接四舍五入。这在做大数据量金额概览时非常好用,但业务报表里要谨慎,因为会丢失明细精度。
CEIL 和 FLOOR 分别是向上取整和向下取整,名字很容易记混。我自己的记忆方式是:CEIL 是天花板,FLOOR 是地板。值得警惕的是负数场景——CEIL(-1.2) 的结果是 -1,而 FLOOR(-1.2) 的结果是 -2。如果你习惯用正数思维去理解“向上”,很容易在负数上栽跟头。TRUNCATE 则会直接截断指定精度后面的数字,不做任何四舍五入,TRUNCATE(123.456, 2) 得到 123.45。它和 ROUND 的差别只有在小数进位时才能看出来,建议在账单对账、积分计算等对精度敏感的场景,先想清楚业务到底要截断还是进位。
1.2 绝对值、取模、幂运算与符号判断
ABS 取绝对值基本没有歧义,在计算价格波动幅度、误差比例时很常用。MOD 是取模运算,等价于 % 运算符,常用于分组取数、负载分片、循环编号。比如运营活动要按用户ID尾号做流量切分,就可以用 MOD(user_id, 10) 来分桶,把用户均匀拆成10份。MOD 还有一个容易被忽略的点:当除数为0时,MySQL会返回 NULL,不会直接报错,这在 SQL 联调时容易掩盖逻辑问题,建议数据校验阶段加一个防御判断。
POWER(x, y) 做幂运算,SQRT(x) 开平方,SIGN(x) 返回数值的符号(负数-1、正数1、零0)。这几个函数单独用看起来都很简单,但它们组合起来能解决不少实际问题。比如计算复利场景下的余额增长,或者判断两个时间戳之间的正负方向,SIGN 能把隐式的方向判断变成一个显式字段,让后续的 WHERE 过滤写起来更清晰。
1.3 随机数与多列最值:RAND、LEAST、GREATEST
RAND() 返回 0 到 1 之间的随机小数,最典型的用途是抽样和随机排序。但我要说一个很多人踩过的坑:ORDER BY RAND() 在大表上是性能灾难,因为它会对所有记录生成随机数再排序,几乎无法走索引。如果是几万行以内凑合用,百万级以上建议先查出主键范围,再用主键随机抽样,或者干脆把随机逻辑放在应用层处理。RAND(N) 还支持传入一个种子,同一个种子生成的随机序列是固定的,这在做数据脱敏、测试数据准备时很有用,能保证每次执行结果一致,方便回归对比。
LEAST 和 GREATEST 用来从多个值里取最小值和最大值。很多人只把它们用在 SELECT 列表里,但其实它们可以处理多列字段的对比。比如用户表里有注册时间、最后登录时间、最后下单时间,想取最近的一次行为时间,用 GREATEST(created_at, last_login_at, last_order_at) 就比写三层嵌套判断简洁得多。需要注意 NULL 的传染性:LEAST(NULL, 1) 会返回 NULL,因为MySQL的比较运算对 NULL 不敏感。使用前最好用 COALESCE 或 IFNULL 把可能为空的字段兜底处理。
1.4 进制转换等冷门但实用的函数
CONV(n, from_base, to_base) 可以在不同进制之间转换数字,比如把十六进制的颜色值转成十进制,或者用二进制位标记状态时解析位运算结果。虽然日常开发用得少,但一旦碰到权限位、配置项拆解这类场景,它能省掉一长串程序代码。数学类函数整体不难,真正的难点在于“在恰当的场景想起来用它”,所以我在项目里整理过一份自己的函数速查笔记,遇到数值需求先翻一遍,避免重复造轮子。
2. 字符串处理函数:数据清洗的日常主力
2.1 拼接与截取:CONCAT、CONCAT_WS、SUBSTRING、SUBSTRING_INDEX
字符串拼接最常用的是 CONCAT(str1, str2, ...),地址拼接、昵称组合、报表标题生成都靠它。一个很容易踩的坑是:MySQL 的 CONCAT 只要有一个参数为 NULL,结果就整个变成 NULL。比如 CONCAT( province, city, district ) 中 district 为空,整条地址都丢了。解决方法是提前用 IFNULL 对每个字段做兜底,或者改用 CONCAT_WS。CONCAT_WS 是带分隔符的拼接,第一个参数指定分隔符,它会自动跳过 NULL 值,这在拼接不定长字段列表时非常稳。
SUBSTRING(str, pos) 和 SUBSTRING(str, pos, len) 用来截取子串,注意这里的起始位置从 1 开始,而不是从 0 开始,这是很多从编程语言转过来的开发最容易犯的错。LEFT(str, n) 和 RIGHT(str, n) 分别截取左侧和右侧的 n 个字符,比 SUBSTRING 语义更直白。SUBSTRING_INDEX(str, delim, count) 可以按分隔符分段截取,比如从邮箱里取 @ 前面的用户名、从 URL 里取域名部分,或者解析版本号中的主版本。它内部是按分隔符查找后再截取,效率不错,是我在日志分析里最常用的函数之一。
2.2 查找与替换:LOCATE、INSTR、REPLACE
LOCATE(substr, str) 用于查找子串在原字符串中的位置,找不到返回0。INSTR(str, substr) 的参数顺序恰好相反,是“原字符串在前,目标子串在后”,初学时容易写反。这两个函数常用在业务规则判断里,比如订单号所属业务线、商品编码包含某个前缀等。它们的返回值还可以继续配合 SUBSTRING 使用,实现“从固定标识开始向后截取”的效果,比硬编码位置灵活很多。
REPLACE(str, from, to) 做字符串替换,数据清洗时用途很广:清理多余空格、替换非法字符、脱敏手机号中间四位等。一次可以嵌套多个 REPLACE,比如把内容里的换行符和制表符全部替换掉,写成 REPLACE(REPLACE(content, '\r\n', ' '), '\t', ' ') 即可。关于 REPLACE 有一个细节:它是按从左到右的顺序扫描替换,不会递归替换,所以目标字符串里如果还包含 from 片段,也不会被二次处理,这一点和正则替换的逻辑不同。
2.3 去空格与大小写转换
TRIM 去掉字符串首尾的空格,LTRIM 和 RTRIM 分别是去掉左侧和右侧的空格。ETL 阶段最常见的问题就是用户提交的手机号、邮箱前后带着空格造成去重失败,所以统一入库前用 TRIM 清洗是标准动作。TRIM 还支持指定要去除的字符:TRIM(BOTH 'x' FROM 'xxxabcxxx') 会把首尾的 x 都去掉,不只是空格。
UPPER 和 LOWER 做大小写转换,常用于邮箱地址归一化、英文名统一大写等。这里有一个人口误级别的坑:有些程序语言里字符串比较是区分大小写的,MySQL 的默认排序规则在多数情况下也不区分,但为了避免歧义,统一转大写后再比较永远是最保险的。尤其在用户名登录校验时,大小写不统一非常容易导致手足无措的线上事故。
2.4 补位与其他字符串工具
LPAD(str, len, pad) 和 RPAD 在左侧或右侧补指定字符到目标长度。订单号自动补零、生成固定长度的测试数据,都用得上。比如把编号 12 补成 00012,写成 LPAD('12', 5, '0')。注意如果原字符串已经超过 len,它不会截断,而是直接返回原字符串,所以别指望它能兼顾截断功能。
REVERSE 反转字符串,配合 SUBSTRING_INDEX 可以取出最后一个分隔符后面的内容,这在解析路径、文件后缀时很巧妙。REPEAT(str, count) 重复字符串多次,适合生成测试数据或构造连续分隔符。这些函数单看很简单,但组合起来的“巧劲”是数据清洗能力的分水岭。
2.5 正则匹配与聚合拼接:REGEXP、GROUP_CONCAT
MySQL 从很早的版本就支持正则匹配,REGEXP 和 RLIKE 是同义词,用于判断字符串是否符合某个正则模式。比如筛选出格式合法的手机号、匹配特定开头的商品编码。正则写法很灵活,但性能不如 LIKE,更不如索引前缀匹配,所以在大表上做正则扫描要慎重。能用 LIKE 的先用 LIKE,正则只用来处理 LIKE 做不到的复杂模式,这是一个原则。
GROUP_CONCAT 是字符串与聚合交叉的神器:它把分组内多行的某个字段拼成一个字符串,常用于“一个用户的所有订单号列表”“一个分类下的所有商品名”。两个关键参数需要掌握:SEPARATOR 自定义分隔符,默认是逗号;最大长度受 group_concat_max_len 限制,默认为1024字节,数据量一旦超过会被静默截断。应对方案是执行前先 SET SESSION group_concat_max_len = 102400,或在配置文件里调大。这个问题我遇到过不止一次,报表里的名单明明有几千条,结果只显示了一部分,排查时很头疼。
3. 日期时间函数:数据分析的必备利器
3.1 获取当前时间:NOW、CURDATE、CURTIME、SYSDATE 的区别
NOW() 返回完整的日期时间,CURDATE() 只返回日期,CURTIME() 只返回时间,这是最基础的一层。真正容易踩坑的是 NOW() 和 SYSDATE() 的区别:NOW() 表示语句开始执行的时间点,在同一个长查询里无论执行到哪一步,NOW() 的值都是固定的;而 SYSDATE() 表示函数实际被执行那一刻的时间,在同一个长查询的不同阶段调用,它可能返回不同值。这在导入大数据量、需要为一批记录写入统一“操作时间”时非常关键:如果误用了 SYSDATE(),同一批数据的时间戳会参差不齐,后续对账可能出问题。
3.2 格式化与提取:DATE_FORMAT 与时间字段拆分
DATE_FORMAT(date, format) 是日期转字符串的核心函数,format 中的 %Y、%m、%d、%H、%i、%s 分别对应年、月、日、小时、分钟、秒。日报、月报的统计维度几乎都要靠它。比如按天统计订单金额:SELECT DATE_FORMAT(pay_time, '%Y-%m-%d') AS day, SUM(price * quantity) FROM orders GROUP BY day。这里要提醒一点:在 WHERE 条件里直接对日期字段使用 DATE_FORMAT,比如 WHERE DATE_FORMAT(pay_time, '%Y-%m-%d') = '2024-01-01',会严重阻碍索引使用,因为索引建立在原始字段上,对字段做函数变换后索引无法匹配。更好的写法是改成 pay_time >= '2024-01-01' AND pay_time < '2024-01-02',让数据库走范围扫描。
YEAR()、MONTH()、DAY() 分别提取日期各个部分,QUARTER() 取季度,MONTHNAME() 取月份英文名。DAYOFWEEK() 返回第几天,但它是从周日开始算的:周日是1,周一是2,一直到周六是7。这个和大部分中国人的直觉不一致,如果业务想统计周一至周日,建议用 WEEKDAY(),它返回0代表周一,6代表周日,正好对齐工作日习惯。WEEK(date) 和 WEEK(date, mode) 涉及周起始日和跨年处理,不同 mode 的算法差异较大,报表口径一旦定下来就不要轻易修改,否则周同比数据会前后对不上。
3.3 日期计算与差值:DATE_ADD、DATE_SUB、DATEDIFF、TIMESTAMPDIFF
DATE_ADD(date, INTERVAL expr unit) 和 DATE_SUB 用来做日期偏移,单位可以是 DAY、MONTH、YEAR、HOUR 等。比如最近30天活跃用户:WHERE last_login_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)。注意 INTERVAL 后的数字可以是负数,所以也可以用 DATE_ADD(date, INTERVAL -1 DAY) 实现减法效果。另外,对“月份”做偏移时,MySQL 会在结果超出当月天数时自动顺延,比如 1月31日加1个月会得到 2月28日,这个行为要和业务方提前对齐。
DATEDIFF(date1, date2) 返回两个日期之间相差的天数,第一个参数减第二个参数,结果有可能是负数。它只计算日期的天数差,会忽略时间部分,所以如果要精确到小时或分钟,就要用 TIMESTAMPDIFF(unit, start, end)。TIMESTAMPDIFF 的单位可以是 SECOND、MINUTE、HOUR、DAY、MONTH、YEAR,计算规则更灵活。一个常见的坑是:很多人以为 DATEDIFF 是“后面的减前面的”,结果把起止日期写反,得到负数,整条报表的环比数据全反了。我的习惯是统一写成 TIMESTAMPDIFF(DAY, 开始时间, 结束时间),语义上更接近“从开始到结束经过了多久”。
3.4 时间戳转换:UNIX_TIMESTAMP 与 FROM_UNIXTIME
UNIX_TIMESTAMP() 把日期时间转成秒级时间戳,FROM_UNIXTIME() 把时间戳转回日期时间。这两个函数在与前端、Java 后端对接时用得非常频繁。注意默认返回的是秒级,不是毫秒级,如果你用 Java 的 System.currentTimeMillis() 拿到的是毫秒,直接存进 MySQL 会导致时间放大1000倍。处理方式是先除以1000,或者应用层统一转成秒。另外,FROM_UNIXTIME 也可以带格式化参数,比如 FROM_UNIXTIME(1700000000, '%Y-%m-%d'),直接在SQL里输出需要的日期格式,省一次查询后再格式化。
4. 条件与逻辑函数:写出会思考的SQL
4.1 分支判断:IF、IFNULL、NULLIF、COALESCE
IF(expr, true_value, false_value) 是最直接的分支函数,适合简单的二选一场景。比如判断订单是否有效:IF(status = 1, '有效', '无效')。IFNULL(expr1, expr2) 专门处理空值兜底:如果第一个参数是 NULL,返回第二个参数。做报表时最常用的是对可能为 NULL 的金额、时长做默认值填充,避免页面出现空白或程序报空指针。NULLIF(expr1, expr2) 的作用相反:如果两个参数相等,返回 NULL,否则返回第一个参数。它最经典的使用场景是防止除零错误:写成 SUM(amount) / NULLIF(COUNT(*), 0),当分母为0时整个表达式变成 NULL 而不是报错,后续再用 IFNULL 包一层处理。
COALESCE(expr1, expr2, ...) 是更灵活的兜底函数,它会从左到右返回第一个非 NULL 的值。在“取用户首选联系方式,没有就取邮箱,再没有取手机号”这类多级回退场景下,COALESCE 一句话就能解决,比嵌套 IF 清爽得多。这类函数看起来不起眼,但它们是防止 SQL 出现大量诡异空值的最后防线。
4.2 灵活的条件表达式:CASE WHEN
CASE WHEN 是 SQL 里条件逻辑的“正主”,它的价值远远不止 SELECT 里的字段转换。CASE 有两种写法:简单 CASE 是 CASE 字段 WHEN 值 THEN 结果,搜索 CASE 是 CASE WHEN 条件 THEN 结果。搜索 CASE 可以在每个 WHEN 后面写完整条件表达式,比简单 CASE 灵活得多,几乎所有区间判断都推荐用这种写法。
它最常见的应用场景有三个。一是区间分组,比如把订单金额分成低中高三档:CASE WHEN price * quantity < 100 THEN '低' WHEN price * quantity < 500 THEN '中' ELSE '高' END。二是自定义排序,想让一个列表按“待支付、已支付、已取消”的顺序排序,可以在 ORDER BY 里用 CASE WHEN 把状态映射成 0、1、2。三是配合聚合函数实现统计透视,比如统计每个分类下各类状态的订单量:SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS paid_cnt。这比一次一次查询高效得多,也是我从“写多段SQL”走向“一条SQL出报表”的关键转折。
4.3 类型转换:CAST 与 CONVERT
CAST(expr AS type) 和 CONVERT(expr, type) 都把表达式转换成指定类型。type 可以是 SIGNED、UNSIGNED、CHAR、DATE、DATETIME、DECIMAL 等。最常见的场景是把字符串形式的数字转成数值后排序。比如手机号、单号如果误存成了字符串,直接 ORDER BY 会按字典序排列,出现 100 < 99 这种反直觉结果,这时先 CAST(phone AS UNSIGNED) 再排序就正常了。
另一个高频场景是隐式转换的陷阱。如果字段是字符串类型,你拿一个整数字面量去比较,MySQL 会自动把字段转成数值再比较,这个转换会让字段上的索引失效。反之,把数字字段和字符串字面量比较时同样会引发隐式转换。解决方法是 SQL 里写清楚 CAST,保证字段原始类型和比较值类型一致,避免数据库自己做“猜谜”。类型转换规则在数据量小的时候看不出问题,一旦上了千万级数据,一个隐式转换就能让你辛辛苦苦建的索引变成摆设。
5. 聚合函数与分组统计:数据分析的发动机
5.1 COUNT函数家族的细微差别
COUNT() 统计结果集行数,COUNT(1) 和它等价,在 InnoDB 引擎下性能基本相同。COUNT(字段) 只统计该字段非 NULL 的行数,这是最容易出错的地方:同一张表,COUNT() 和 COUNT(某列) 结果可能有差异,因为那一列里有 NULL 值。所以写“总数”时尽量不要用 COUNT(字段),除非你明确知道要排除空值。COUNT(DISTINCT 字段) 用于去重统计,比如 COUNT(DISTINCT user_id) 算独立用户数。它的数据量太大时性能会比较慢,通常需要配合近似算法或先做子查询去重,再在外面计数。
5.2 SUM、AVG、MAX、MIN 的使用技巧
SUM 求和,AVG 求平均,这两个函数都会自动忽略 NULL 值。需要注意 AVG 在“分母”上的含义:AVG(price) 是“有价格的记录的平均价格”,如果业务想表达“所有订单的平均价格”且没有价格的订单按0参与计算,那就应该写 AVG(IFNULL(price, 0))。这个细微差别在业务报表里可能直接导致客单价差出几块钱,一定要先对齐统计口径。
SUM 结合 IF 或 CASE 可以实现条件求和,这是我在做转化漏斗时最常用的技巧:把每个步骤是否完成转成0或1,SUM 之后就是完成人数。MAX 和 MIN 对数字、字符串、日期都有效,求最大日期、最早创建时间非常方便。字符串的 MAX 是按排序规则比较的,中文环境下排序规则不同结果也可能不同,涉及中文最大值时要结合业务理解判断,不能机械使用。
5.3 GROUP_CONCAT 与 GROUP BY WITH ROLLUP
GROUP_CONCAT 不止能拼接字符串,还能在拼接时排序和去重:GROUP_CONCAT(DISTINCT category ORDER BY category SEPARATOR ';'),这在生成标签列表、品类清单时非常好用。它的底层对结果长度有限制,前面提到的 group_concat_max_len 优化要提前做。
GROUP BY WITH ROLLUP 可以在分组统计结果的末尾追加一行总计,或者在每个分组层级追加小计。比如按分类和状态两个维度分组后,WITH ROLLUP 会生成分类小计和全量合计,非常方便做层级汇报表。要注意它和 ORDER BY 同时使用时会有一定限制:ROLLUP 生成的多余行和排序顺序可能互相冲突,MySQL 文档里明确推荐不要在带 WITH ROLLUP 的语句里直接使用 ORDER BY,可以把结果包一层子查询再排序。
5.4 HAVING与WHERE的本质区别
WHERE 在分组前过滤行,HAVING 在分组后过滤组。想在 SQL 里筛出“订单数量大于10的用户”,条件中涉及聚合结果 COUNT(*),这个条件只能写在 HAVING 里,写在 WHERE 里直接报错。从执行顺序上看,WHERE 先淘汰数据,传到 GROUP BY 的数据量已经缩小,所以能用 WHERE 提前过滤的要尽量提前过滤,HAVING 只是对聚合结果做补充约束。一个典型的性能优化点:先用 WHERE 过滤掉大部分数据,再做聚合,SQL 的执行效率会好得多。
还要注意 MySQL 的 ONLY_FULL_GROUP_BY 模式。默认开启时,SELECT 后面的非聚合列必须都出现在 GROUP BY 中,否则会报错。很多人初学时被这个报错困扰,其实它是在保护你,防止你从某一行取一个没有意义的字段值。如果确实需要组内某个值,要么把它加进 GROUP BY,要么用 MIN/MAX/ANY_VALUE 包一下。
6. 窗口函数:现代数据分析的进阶利器
6.1 排序窗口:ROW_NUMBER、RANK、DENSE_RANK
窗口函数是 MySQL 8.0 引入的,如果你还在用 5.7,需要先升级版本。窗口函数和聚合函数的本质区别是:聚合函数会把多行合并成一行,窗口函数不改变行数,只是在每行旁边多出一列计算值。对做数据分析的人来说,这就意味着可以在明细行上同时看到总数、排名、上一行值,应用场景一下宽了许多。
ROW_NUMBER() OVER(PARTITION BY ... ORDER BY ...) 为每一行生成一个唯一序号,是最常用的取“分组内前N条”的工具。比如查每个用户最近一笔订单:先按 user_id 分组、按 pay_time 倒序生成行号,再在外层筛选 rn = 1。RANK 和 DENSE_RANK 都用来处理并列排名:RANK 遇到并列会跳号,比如两个并列第二,下一个名次是第四;DENSE_RANK 不跳号,并列第二后下一个是第三。选择哪个取决于业务口径:比赛排名一般用 RANK,榜单序号一般用 DENSE_RANK,招聘里所谓的“排名不占位”就是 DENSE_RANK。
6.2 偏移窗口:LAG、LEAD 与环比分析
LAG(expr, offset, default) 取当前行往前偏移 offset 行的值,LEAD 往后偏移。这对计算环比、同比、上一单时间间隔特别有用。比如看每天的订单量比前一天涨了多少:先用 DATE_FORMAT 按天聚合,然后用 LAG(SUM(amount)) OVER(ORDER BY day) 拿到前一天的金额,当天金额减它就能得到增量。LAG 第三个参数是缺省值,当第一行没有前一行时用默认值,比如给0或当前值,避免计算结果整体为空。
FIRST_VALUE 和 LAST_VALUE 可以快速取窗口内第一个、最后一个值。LAST_VALUE 有一个框架范围的问题:默认窗口范围是到当前行为止,所以直接写 LAST_VALUE 可能得到的不是整个分区的最后一行,而是当前行本身。要得到真正的最后一行,需要显式指定 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。这个细节非常容易踩坑,我见过不止一次用 LAST_VALUE 查出和自己一样值的诡异结果。
6.3 聚合窗口:SUM OVER 与移动平均
SUM(amount) OVER(PARTITION BY user_id ORDER BY pay_time) 可以计算每个用户截至当前订单的累计消费额,这在RFM用户分析里很常用。和普通聚合相比,它保留了每一行明细,累计值也在逐行变化。AVG(amount) OVER(ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) 可以算近7日移动平均,用于平滑每日订单量的波动,比直接看单日数据能发现更稳定的趋势。
窗口函数在性能上一般优于“先聚合再自关联”的写法,因为它只需要扫描一次数据。但也别过度滥用:如果一屏SQL里写了五六个窗口函数,每个窗口范围还不一样,执行计划会变得很复杂。建议一个分析步骤只用一个窗口函数,复杂的多步分析可以拆成多个临时表或用 CTE 分层,既清晰又好维护。
7. 常见问题与排查实录
下面这些坑,是我在实际项目里遇到过真实案例并逐个排查过的,写成速查表方便你直接对照。
| 现象 | 常见原因 | 解决方案 |
|---|---|---|
| COUNT(某列) 和 COUNT(*) 结果不一致 | 该列存在 NULL 值,COUNT(字段) 不计 NULL | 明确业务口径,需要全量就用 COUNT(*) |
| 字符串数字排序结果反直觉 | 字段是字符串类型,ORDER BY 按字典序排列 | CAST 成数值再排序 |
| WHERE 里对日期字段用 DATE_FORMAT 后查询很慢 | 函数包字段导致索引失效 | 改为范围条件 BETWEEN |
| GROUP_CONCAT 结果截断 | group_concat_max_len 默认1024字节 | SET SESSION group_concat_max_len = 102400 |
| 窗口函数 LAST_VALUE 结果不是最后一行 | 默认窗口范围截止到当前行 | 显式指定 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
| 分组查询报错,提示非聚合列不在GROUP BY | ONLY_FULL_GROUP_BY 模式开启 | 将该列加入 GROUP BY 或使用 ANY_VALUE/MIN/MAX 包裹 |
| 时间戳比实际时间大很多 | 毫秒级时间戳误存成秒级 | 应用层统一除以1000 |
| NOW 和 SYSDATE 写入同一批数据的时间戳不一致 | SYSDATE 取的是实时时间 | 固定使用 NOW() 保证语句级一致性 |
这里额外分享一个排查经验:如果发现报表数据在某些条件下“对不上”,不要急着改 SQL 逻辑,先把涉及的函数单独拿出来跑一遍。比如 SELECT NOW(), SYSDATE(), DATEDIFF(NOW(), NOW()) 这种小测试,几秒就能定位是函数行为误解,还是数据本身有问题。排查速度往往决定了你在团队里是“背锅侠”还是“救火队员”。
8. 写在最后
盘点完这批函数,我最想说的反而不是哪个函数多强大,而是:函数一定要在真实场景里反复用,才能变成肌肉记忆。之前我带团队做SQL培训,总是让大家拿一份订单数据做练习:30分钟内完成“每天的高峰时段、Top商品的复购间隔、各价格带的订单占比”三个统计。做完这个练习,很多人会发现,原来自己不是不会函数,而是不知道函数和函数之间该怎么组合。
组合才是函数真正的价值。字符串截取加日期格式化可以做行为序列分析,条件聚合加窗口偏移可以做留存与转化漏斗,数学函数加分组统计可以做价格分布画像。一个单独的函数只是工具,当你能在一个真实业务问题里自然地把它们串起来,才算是真正掌握。
最后分享一个小建议:在你自己经常用的数据库客户端里,建一张“函数速查表”,把这次文章中提到的函数、参数、示例、适用场景都记上去,每踩一次坑就补一条。日积月累之后,你会发现查资料的时间越来越少,写出的SQL越来越稳。MySQL 的函数体系更新很快,窗口函数、JSON函数这些新特性值得持续关注,但先把常用函数吃透,永远是收益最高的一步。