0
0
0

03-MySQL 函数专题:CONCAT、CONCAT_WS 与 GROUP_CONCAT

2026-10-20
2026-10-21

📝 笔记摘要: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 把空值在进函数之前处理掉,这三个函数就够用了。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!