03-MySQL 函数专题:CONCAT、CONCAT_WS 与 GROUP_CONCAT
📝 笔记摘要:MySQL 的字符串拼接三件套与常用函数速查
字符串拼接看着简单,实际是 MySQL 里最容易出静默错误的一类函数:CONCAT 遇到 NULL 直接把整条结果变成 NULL,GROUP_CONCAT 默认只给你 1024 个字符,超了直接截断还不报错。这篇把三个拼接函数讲透,再把常用函数按类别整理成能直接查的速查表。
MySQL 系列 共 5 篇,这是第 3 篇
01 MySQL 基础 类型、建表与查询
02 MySQL 事务、锁与索引 ACID、隔离级别、锁层级与索引失效
03-MySQL 函数专题 CONCAT、CONCAT_WS 与 GROUP_CONCAT ← 本篇
04 MySQL 报表实战 同环比、日期序列与多表连接
05 MySQL 优化与版本细节 优化清单、版本行为差异与转义
这篇的结构
1. CONCAT:最简单,也最容易翻车
2. CONCAT_WS:会跳过 NULL 的那个
3. GROUP_CONCAT:把多行拼成一行
4. 三个函数的差别在哪
5. GROUP_CONCAT 的两个坑
6. 实战:三种拼接场景
7. 字符串函数速查
8. 数值与时间函数速查
9. 聚合函数与 NULL
表结构和查询写法在 MySQL 基础 那篇,报表场景里的拼接实战在 MySQL 报表实战。
1. CONCAT:最简单,也最容易翻车
CONCAT(str1, str2, ...) 把参数首尾相连接成一个字符串。
SELECT CONCAT('My', 'S', 'QL'); -- 'MySQL'
SELECT CONCAT(14.3); -- '14.3',数字会转成字符串
SELECT CONCAT('My', NULL, 'QL'); -- NULL
参数可以有任意多个 不限两个
数字参数会转成字符串CONCAT(14.3)返回字符串'14.3',不是数字
只要有一个参数是 NULL,整个结果就是 NULL 这是最容易翻车的地方
参数里有二进制字符串,结果也是二进制 全是非二进制字符串时结果也是非二进制的
「一个 NULL 就全变 NULL」在实践中意味着:用 CONCAT 拼字段时,只要有一个字段可能为空,结果就整列都是 NULL。而 NULL 在后续的排序、比较、显示里都不是你能预期的行为——NULL 排在一起时升序和降序的顺序是反的。
-- 有风险:phone 有一行为 NULL,这一行的 result 就是 NULL
SELECT name, CONCAT(phone, ' / ', address) AS contact FROM customer;
-- 稳:用 CONCAT_WS,它会跳过 NULL
2. CONCAT_WS:会跳过 NULL 的那个
CONCAT_WS(separator, str1, str2, ...) 是 CONCAT 的特殊形式:第一个参数是分隔符,剩下的字符串用它连起来。名字就是 Concatenate With Separator。
SELECT CONCAT_WS(',', 'First name', 'Second name', 'Last Name');
-- 'First name,Second name,Last Name'
SELECT CONCAT_WS(',', 'First name', NULL, 'Last Name');
-- 'First name,Last Name' NULL 被跳过了
分隔符为 NULL,结果就是 NULL 这是唯一一个 NULL 会「传染」的情况
分隔符后面的 NULL 会被跳过,不会传染
空字符串不会被跳过CONCAT_WS(',', 'a', '', 'b')得到'a,,b',中间那个空串照样占位
分隔符不一定是一个字符 多字符也行,CONCAT_WS(' - ', 'a', 'b')得到'a - b'
所以 CONCAT_WS 是拼接可能为空的字段时的默认选择:CONCAT_WS(' / ', phone, address) 里 phone 为空时,结果是 ' / 地址'(有个多余分隔符),要完全干净还得配 NULLIF 或 IFNULL 处理,但至少不会整列变 NULL。
3. GROUP_CONCAT:把多行拼成一行
前两个函数处理的是「一行里的多个字段」,GROUP_CONCAT 处理的是「多行聚成一行」——它是聚合函数,配合 GROUP BY 用。
GROUP_CONCAT([DISTINCT] expr [, expr ...]
[ORDER BY {unsigned_integer | col_name | expr} [ASC | DESC] [, col_name ...]]
[SEPARATOR str_val])
官方给的例子:
SELECT student_name, GROUP_CONCAT(test_score)
FROM student
GROUP BY student_name;
SELECT student_name,
GROUP_CONCAT(DISTINCT test_score ORDER BY test_score DESC SEPARATOR ' ')
FROM student
GROUP BY student_name;
只拼非 NULL 的值 聚合函数默认忽略 NULL。如果一组里全是 NULL,结果就是 NULL
DISTINCT 去重 去掉重复值
ORDER BY 排序 默认升序,加DESC变降序
SEPARATOR 换分隔符 默认是逗号,SEPARATOR ''可以整个去掉分隔符
4. 三个函数的差别在哪
CONCAT 参数之间不加分隔符。任意一个参数是 NULL,结果就是 NULL
CONCAT_WS 参数之间加分隔符,分隔符自己指定。分隔符之后的 NULL 会被跳过
GROUP_CONCAT 聚合函数,把一个分组里的多行值拼成一个字符串,可以去重、排序、换分隔符,结果受group_concat_max_len限制
一句话区分:前两个是「把几个字段接起来」,第三个是「把几行收起来」。
5. GROUP_CONCAT 的两个坑
坑一:默认只给 1024 个字符
这是 GROUP_CONCAT 最著名的坑。默认的 group_concat_max_len 是 1024,超出的部分直接被截断,而且不报错——你只会看到结果莫名其妙少了一截。
-- 先看看你的限制是多少
SHOW VARIABLES LIKE 'group_concat_max_len';
-- 按会话调大
SET SESSION group_concat_max_len = 1024000;
调大它有代价 返回值的实际长度还受
max_allowed_packet约束(默认 64 MB)。组里的行数多、字段长时,内存和网络上要传的东西都不小
别用 GROUP_CONCAT 拉大结果集当接口返回 一次拼几十万行,内存直接爆。真的要把明细拉出来,分页查出来自己在应用层拼
默认值是 1024 这个数字从 5.x 到现在都没变过
坑二:顺序不确定
不加 ORDER BY 时,GROUP_CONCAT 的拼接顺序取决于执行计划——优化器换了扫描方式,顺序就变了。要稳定输出就必须显式写 ORDER BY。
-- 不稳定:同样的数据,不同时候跑出来的顺序可能不同
SELECT GROUP_CONCAT(name) FROM orders WHERE user_id = 1;
-- 稳定
SELECT GROUP_CONCAT(name ORDER BY created_at ASC SEPARATOR ', ')
FROM orders WHERE user_id = 1;
6. 实战:三种拼接场景
场景一:把一张表的多个字段拼成展示文本
SELECT CONCAT_WS(' / ', province, city, district, address) AS full_address
FROM customer;
省份、地址这些字段经常是空的,用 CONCAT_WS 而不是 CONCAT,空的那个直接跳过,不会把整行变成 NULL。
场景二:把一行的多个标签拼成一个字符串
SELECT user_id,
GROUP_CONCAT(DISTINCT tag ORDER BY tag SEPARATOR ', ') AS tags
FROM user_tags
GROUP BY user_id;
标签表是一行一个标签,要展示成一行就要 GROUP_CONCAT。这里三个修饰符都用上了:DISTINCT 去重、ORDER BY 保证顺序、SEPARATOR 换掉默认逗号。
场景三:按分组拼出一行汇总
SELECT dept_id,
COUNT(*) AS emp_count,
GROUP_CONCAT(CONCAT_WS(':', emp_name, salary) ORDER BY emp_id SEPARATOR ' | ') AS staff
FROM employee
GROUP BY dept_id;
注意这里是嵌套:内层 CONCAT_WS 先把一个员工的多个字段拼成 姓名:工资,外层 GROUP_CONCAT 再把这一组的员工拼成整行。GROUP_CONCAT 里面可以放表达式,表达式里可以再调函数。
7. 字符串函数速查
CHAR_LENGTH(str) 字符个数(不是字节数)。
LENGTH(str)是字节数,utf8mb4 下两者不等
CONCAT / CONCAT_WS 拼接,见上
LOWER(str) / UPPER(str) 转小写 / 转大写。老资料把这两个写反了——LOWER是变小写
TRIM(str) 去首尾空格。LTRIM/RTRIM只去一边
LEFT(str, n) / RIGHT(str, n) 取左边 / 右边 n 个字符
SUBSTRING(str, pos, len) 从第 pos 个字符起取 len 个。老写法SUBSTR(str FROM pos FOR len)等价
REPLACE(str, from, to) 把 str 里所有出现的 from 换成 to。不是只换第一个
INSERT(str, pos, len, newstr) 从第 pos 个字符开始,把 len 个字符替换成 newstr
INSTR(str, substr) 返回 substr 在 str 里第一次出现的位置(从 1 开始)。LOCATE是同一个函数,参数顺序相反
LPAD(str, len, pad) / RPAD 左侧 / 右侧补齐到 len 长度
REVERSE(str) 反转
FIELD(str, s1, s2, ...) 返回 str 在后面列表里的序号,找不到返回 0
ELT(n, s1, s2, ...) 返回第 n 个字符串,n 从 1 开始
FORMAT(num, decimals) 格式化成带千分位和固定小数位的字符串(结果是字符串,不是数字)
HEX(str) / UNHEX(hex) 转十六进制 / 转回来
MD5(str) 32 位十六进制摘要。别用来存密码,用 bcrypt 或 Argon2
8. 数值与时间函数速查
ABS(x) 绝对值
CEILING(x) / FLOOR(x) 向上 / 向下取整
ROUND(x, d) 四舍五入到 d 位小数
TRUNCATE(x, d) 直接截断到 d 位小数,不做四舍五入。金额场景两者结果不同
RAND() 0 到 1 之间的随机数
SIGN(x) 符号:正数 1、负数 -1、零 0
POW(a, b) / MOD(a, b) 幂 / 取模
GREATEST(a, b, ...) / LEAST(a, b, ...) 取最大 / 最小。任一参数为 NULL 时返回 NULL
NOW() 当前日期和时间
CURDATE() / CURTIME() 当前日期 / 当前时间
DATE_ADD(d, INTERVAL n unit) / DATE_SUB(d, INTERVAL n unit) 日期加减,unit 可以是 DAY、WEEK、MONTH、YEAR、HOUR 等
DATEDIFF(d1, d2) 相差的天数(d1 - d2,参数顺序容易记反)
TIMESTAMPDIFF(unit, d1, d2) 按指定单位算时间差,参数顺序和 DATEDIFF 相反
DATE_FORMAT(d, format) 格式化日期,'%Y-%m-%d'这种写法
EXTRACT(unit FROM d) 从日期里取某一部分,EXTRACT(YEAR FROM d)
LAST_DAY(d) 当月最后一天
时间函数里 DATEDIFF 和 TIMESTAMPDIFF 的参数顺序相反,是最容易记混的一对:
SELECT DATEDIFF('2026-10-20', '2026-10-01'); -- 19
SELECT TIMESTAMPDIFF(DAY, '2026-10-01', '2026-10-20'); -- 19
9. 聚合函数与 NULL
所有聚合函数(COUNT、SUM、AVG、MAX、MIN、GROUP_CONCAT)默认忽略 NULL,除非官方文档另有说明。
COUNT(*) 和 COUNT(列) 不一样
COUNT(*)数的是行数,一行一个;COUNT(列名)数的是该列非 NULL 的行数。列里有 NULL 时两者结果差很大
SUM 和 AVG 会跳过 NULL 所以AVG算的是「非 NULL 值的平均」,不是「把所有值加起来除以总行数」
空集的行为SUM和COUNT对空集返回 0,AVG、MAX、MIN、GROUP_CONCAT返回 NULL
💡 补充:几个容易记错的点。COUNT(1) 和 COUNT(*) 在现在的 InnoDB 上生成同样的执行计划,没有可测量的性能差异,统一写 COUNT(*)。CONCAT 遇 NULL 返回 NULL,CONCAT_WS 跳过 NULL(但分隔符为 NULL 时结果为 NULL)——这两个函数的差异在拼可能为空的字段时是决定性的。函数名在 MySQL 里不区分大小写,但 lower / upper 的说明常被写反,lower 是转小写。
🔥 一句话总结:拼字符串记住三句话——CONCAT 遇 NULL 就全完,CONCAT_WS 会跳过 NULL 但分隔符不能为 NULL,GROUP_CONCAT 负责多行收成一行、默认只给 1024 字符且顺序不保证;再用 IFNULL / NULLIF 把空值在进函数之前处理掉,这三个函数就够用了。
