MySQL中GROUP_CONCAT的用法详解:语法、示例与常见坑
- 发布时间:2026-05-09 06:02:29
- 本文热度:浏览 12 赞 0 评论 0
- 文章标签: MySQL SQL GROUP_CONCAT
- 全文共1字,阅读约需1分钟
GROUP_CONCAT 是什么,解决什么问题
GROUP_CONCAT 是 MySQL 中常用的聚合函数,用来把同一组内的多行值拼接成一个字符串。它最适合处理这类需求:
- 按部门汇总员工姓名列表
- 按订单汇总商品名称
- 按用户汇总角色集合
- 按分类汇总标签列表
- 把一对多关系压缩成一行展示
和 COUNT、SUM、AVG 一样,GROUP_CONCAT 也是聚合函数,但它聚合的不是数值,而是字符串。
先看一个最直观的例子
假设有一个用户和角色的关系表,一个用户可以对应多个角色,现在希望查询结果中每个用户只占一行,并把角色名拼起来。
建表 SQL
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL
);
DROP TABLE IF EXISTS role_info;
CREATE TABLE role_info (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_name VARCHAR(50) NOT NULL
);
DROP TABLE IF EXISTS user_role;
CREATE TABLE user_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL
);
初始化数据
INSERT INTO user_info (username) VALUES
('alice'),
('bob'),
('charlie');
INSERT INTO role_info (role_name) VALUES
('admin'),
('developer'),
('tester'),
('operator');
INSERT INTO user_role (user_id, role_id) VALUES
(1, 1),
(1, 2),
(1, 4),
(2, 2),
(2, 3),
(3, 4);
普通关联查询
SELECT
u.id,
u.username,
r.role_name
FROM user_info u
JOIN user_role ur ON u.id = ur.user_id
JOIN role_info r ON ur.role_id = r.id
ORDER BY u.id, r.id;
查询结果大致如下:
| id | username | role_name |
|---|---|---|
| 1 | alice | admin |
| 1 | alice | developer |
| 1 | alice | operator |
| 2 | bob | developer |
| 2 | bob | tester |
| 3 | charlie | operator |
如果希望一行只展示一个用户,就可以使用 GROUP_CONCAT。
使用 GROUP_CONCAT 聚合角色
SELECT
u.id,
u.username,
GROUP_CONCAT(r.role_name) AS roles
FROM user_info u
JOIN user_role ur ON u.id = ur.user_id
JOIN role_info r ON ur.role_id = r.id
GROUP BY u.id, u.username
ORDER BY u.id;
结果类似:
| id | username | roles |
|---|---|---|
| 1 | alice | admin,developer,operator |
| 2 | bob | developer,tester |
| 3 | charlie | operator |
这就是 GROUP_CONCAT 的核心作用:把组内多行拼成一个字段。
GROUP_CONCAT 的基本语法
GROUP_CONCAT([DISTINCT] expr
[ORDER BY {unsigned_integer | col_name | expr} [ASC | DESC]]
[SEPARATOR str_val])
可以拆成三部分理解:
expr:要拼接的字段或表达式DISTINCT:去重ORDER BY:控制拼接顺序SEPARATOR:指定分隔符
最常见的几种用法
1. 最基础用法
SELECT
user_id,
GROUP_CONCAT(role_id) AS role_ids
FROM user_role
GROUP BY user_id;
效果:按 user_id 分组,把对应的 role_id 拼成逗号分隔的字符串。
2. 配合 DISTINCT 去重
如果关联查询导致重复值,或者原始数据本身就可能重复,可以加 DISTINCT。
SELECT
user_id,
GROUP_CONCAT(DISTINCT role_id) AS role_ids
FROM user_role
GROUP BY user_id;
例如某个用户因为脏数据出现多条相同角色记录时,DISTINCT 可以避免结果里重复出现。
3. 配合 ORDER BY 控制拼接顺序
很多人第一次使用 GROUP_CONCAT 时会忽略顺序问题。如果不显式指定 ORDER BY,拼接顺序不应被认为是稳定的。
SELECT
u.id,
u.username,
GROUP_CONCAT(r.role_name ORDER BY r.role_name ASC) AS roles
FROM user_info u
JOIN user_role ur ON u.id = ur.user_id
JOIN role_info r ON ur.role_id = r.id
GROUP BY u.id, u.username;
这样结果会按角色名字母顺序拼接。
也可以按业务字段排序,例如按角色 ID:
GROUP_CONCAT(r.role_name ORDER BY r.id ASC)
4. 使用 SEPARATOR 指定分隔符
默认分隔符是英文逗号 ,。如果业务需要其他格式,可以自定义。
SELECT
u.id,
u.username,
GROUP_CONCAT(r.role_name SEPARATOR ' | ') AS roles
FROM user_info u
JOIN user_role ur ON u.id = ur.user_id
JOIN role_info r ON ur.role_id = r.id
GROUP BY u.id, u.username;
结果类似:
admin | developer | operator
还可以拼成更适合前端展示的格式:
GROUP_CONCAT(r.role_name SEPARATOR '、')
结果:
admin、developer、operator
5. 拼接多个字段
GROUP_CONCAT 不只能拼一个字段,也可以先用 CONCAT 把多个字段组装起来,再整体聚合。
假设希望展示 “角色ID:角色名”:
SELECT
u.id,
u.username,
GROUP_CONCAT(CONCAT(r.id, ':', r.role_name) ORDER BY r.id SEPARATOR '; ') AS role_detail
FROM user_info u
JOIN user_role ur ON u.id = ur.user_id
JOIN role_info r ON ur.role_id = r.id
GROUP BY u.id, u.username;
结果类似:
1:admin; 2:developer; 4:operator
这在导出报表、拼装展示字段时非常实用。
GROUP BY 和 GROUP_CONCAT 的关系
GROUP_CONCAT 几乎总是和 GROUP BY 一起出现,因为它本质上是对每一组做字符串聚合。
例如:
SELECT
user_id,
GROUP_CONCAT(role_id)
FROM user_role
GROUP BY user_id;
含义是:
- 先按
user_id分组 - 再把每个组中的
role_id拼接起来
如果没有 GROUP BY,那么整张结果集会被当成一组:
SELECT GROUP_CONCAT(username) AS all_usernames
FROM user_info;
结果会把整张表的用户名拼成一行。
实战场景 1:订单下的商品清单汇总
建表 SQL
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL
);
DROP TABLE IF EXISTS order_item;
CREATE TABLE order_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_name VARCHAR(100) NOT NULL
);
初始化数据
INSERT INTO orders (order_no) VALUES
('ORD20260417001'),
('ORD20260417002');
INSERT INTO order_item (order_id, product_name) VALUES
(1, 'iPhone'),
(1, 'AirPods'),
(1, 'Apple Watch'),
(2, 'Mechanical Keyboard'),
(2, 'Mouse');
查询每个订单的商品列表
SELECT
o.id,
o.order_no,
GROUP_CONCAT(oi.product_name ORDER BY oi.id SEPARATOR ', ') AS product_list
FROM orders o
JOIN order_item oi ON o.id = oi.order_id
GROUP BY o.id, o.order_no
ORDER BY o.id;
这个写法非常适合报表查询、后台列表页和导出场景。
实战场景 2:按部门汇总员工名单
建表 SQL
DROP TABLE IF EXISTS department;
CREATE TABLE department (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(50) NOT NULL
);
DROP TABLE IF EXISTS employee;
CREATE TABLE employee (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(50) NOT NULL,
dept_id BIGINT NOT NULL
);
初始化数据
INSERT INTO department (dept_name) VALUES
('研发部'),
('测试部'),
('运维部');
INSERT INTO employee (emp_name, dept_id) VALUES
('张三', 1),
('李四', 1),
('王五', 2),
('赵六', 3),
('孙七', 3);
查询每个部门的员工列表
SELECT
d.id,
d.dept_name,
GROUP_CONCAT(e.emp_name ORDER BY e.id SEPARATOR '、') AS emp_names
FROM department d
LEFT JOIN employee e ON d.id = e.dept_id
GROUP BY d.id, d.dept_name
ORDER BY d.id;
这里使用 LEFT JOIN 的目的是:即使部门下没有员工,也能把部门查出来。
NULL 值对 GROUP_CONCAT 的影响
GROUP_CONCAT 会自动忽略 NULL 值。
例如:
SELECT GROUP_CONCAT(NULL, 'A', 'B');
这种写法本身不标准,但可以理解为:参与聚合时,NULL 项不会被拼进去。
如果某一组内所有值都是 NULL,结果通常是 NULL,而不是空字符串。
例如:
SELECT
dept_id,
GROUP_CONCAT(emp_name) AS emp_names
FROM employee
GROUP BY dept_id;
如果某组的 emp_name 全部为空,那么该组结果就是 NULL。
如果你希望返回空字符串,可以结合 IFNULL 或 COALESCE:
SELECT
d.id,
d.dept_name,
IFNULL(GROUP_CONCAT(e.emp_name ORDER BY e.id SEPARATOR '、'), '') AS emp_names
FROM department d
LEFT JOIN employee e ON d.id = e.dept_id
GROUP BY d.id, d.dept_name;
GROUP_CONCAT 的结果长度限制
这是 GROUP_CONCAT 最容易踩的坑之一。
GROUP_CONCAT 的返回结果受系统变量 group_concat_max_len 限制。如果拼接结果超过这个长度,会被截断。
查看当前限制
SHOW VARIABLES LIKE 'group_concat_max_len';
临时修改当前会话
SET SESSION group_concat_max_len = 10240;
修改全局值
SET GLOBAL group_concat_max_len = 10240;
如果你的拼接结果可能很长,比如:
- 一个订单下商品很多
- 一个用户下标签很多
- 导出时拼接整段说明文字
那就一定要检查这个参数,否则结果可能被悄悄截断,导致数据展示不完整。
常见坑 1:没有写 ORDER BY,结果顺序不稳定
很多人看到测试环境下查询结果一直一样,就以为 GROUP_CONCAT 默认顺序固定。实际上不是。
下面这种写法:
GROUP_CONCAT(r.role_name)
虽然经常看起来“像是按插入顺序”,但 SQL 层面并没有保证。执行计划变化、索引变化、Join 顺序变化,都可能让拼接顺序发生变化。
更稳妥的写法是:
GROUP_CONCAT(r.role_name ORDER BY r.id ASC)
只要结果需要稳定顺序,就显式写出来。
常见坑 2:忘记 DISTINCT,结果重复
一对多、多对多联表时,很容易因为关联路径导致重复。
例如:
SELECT
u.id,
GROUP_CONCAT(r.role_name) AS roles
FROM user_info u
JOIN user_role ur ON u.id = ur.user_id
JOIN role_info r ON ur.role_id = r.id
GROUP BY u.id;
如果 user_role 表里有重复记录,或者联表过程中额外引入重复行,最终 roles 会重复。
可以改成:
GROUP_CONCAT(DISTINCT r.role_name ORDER BY r.id)
常见坑 3:只按主表 ID 分组,却选了其他非聚合字段
例如:
SELECT
u.id,
u.username,
GROUP_CONCAT(r.role_name) AS roles
FROM user_info u
JOIN user_role ur ON u.id = ur.user_id
JOIN role_info r ON ur.role_id = r.id
GROUP BY u.id;
在某些 SQL 模式下,这种写法可能报错;在宽松模式下,虽然能执行,但非聚合字段的值来源可能不严谨。
更规范的写法是:
SELECT
u.id,
u.username,
GROUP_CONCAT(r.role_name ORDER BY r.id) AS roles
FROM user_info u
JOIN user_role ur ON u.id = ur.user_id
JOIN role_info r ON ur.role_id = r.id
GROUP BY u.id, u.username;
原则很简单:非聚合字段要么进入 GROUP BY,要么明确通过聚合函数处理。
常见坑 4:把 GROUP_CONCAT 当成存储结构
GROUP_CONCAT 适合查询结果展示,不适合拿来替代数据库建模。
错误思路:
- 一个用户多个角色,直接把角色 ID 存成
1,2,3 - 一个订单多个商品,直接把商品名存成一个逗号字符串
这样做的问题包括:
- 难以做精确查询
- 难以建立索引
- 难以保证数据一致性
- 更新和删除成本高
- SQL 维护复杂
正确做法是:
- 底层仍然使用规范化表结构
- 在查询结果层面用
GROUP_CONCAT做聚合展示
也就是说,GROUP_CONCAT 应该用于查询输出层,而不是存储建模层。
常见坑 5:结果过长被截断却没发现
这一点在测试数据量小时通常看不出来,到了生产环境才暴露。
例如一个用户挂了几百个标签,或者一个报表字段拼接了大量文本,最后前端看到的字符串不完整,但 SQL 又没有明显报错,这通常就是 group_concat_max_len 限制导致的。
排查思路:
- 检查
group_concat_max_len - 用实际生产数据测试
- 对导出类接口做长度验证
- 必要时改用更适合的数据输出方式,而不是无限制拼接长字符串
GROUP_CONCAT 与 CONCAT、CONCAT_WS 的区别
这几个函数名字很像,但作用完全不同。
CONCAT
CONCAT 是把同一行中的多个字段拼成一个字符串。
SELECT CONCAT('role-', id, '-', role_name) FROM role_info;
它处理的是“列到列”的拼接。
CONCAT_WS
CONCAT_WS 是带分隔符的 CONCAT。
SELECT CONCAT_WS(':', id, role_name) FROM role_info;
相当于:
id:role_name
GROUP_CONCAT
GROUP_CONCAT 是把多行记录聚合成一个字符串。
SELECT GROUP_CONCAT(role_name) FROM role_info;
它处理的是“行到一行”的聚合。
一句话区分:
CONCAT:拼字段CONCAT_WS:按分隔符拼字段GROUP_CONCAT:拼多行
实战建议:什么时候适合用 GROUP_CONCAT
适合使用的场景:
- 后台列表页展示聚合信息
- 报表导出
- 汇总字段展示
- 多标签、多角色、多商品清单压缩展示
- 临时统计查询
不太适合的场景:
- 需要再对聚合结果继续做精细过滤
- 拼接结果非常大
- 需要前端结构化处理
- 想把它当成持久化存储格式
如果前端最终需要的是数组结构,而不是字符串,那么很多时候更推荐在应用层做二次组装,或者使用更适合结构化输出的方案。
一个更完整的查询写法
下面给一个相对规范的写法,包含排序、去重和空值处理:
SELECT
u.id,
u.username,
COALESCE(
GROUP_CONCAT(DISTINCT r.role_name ORDER BY r.id ASC SEPARATOR ', '),
''
) AS roles
FROM user_info u
LEFT JOIN user_role ur ON u.id = ur.user_id
LEFT JOIN role_info r ON ur.role_id = r.id
GROUP BY u.id, u.username
ORDER BY u.id;
这个版本解决了几个问题:
- 用
LEFT JOIN保证主表数据不丢 - 用
DISTINCT避免重复 - 用
ORDER BY保证顺序稳定 - 用
SEPARATOR控制展示格式 - 用
COALESCE处理空结果
如果你在线上业务里要真正使用,通常这个版本比最简写法更可靠。
总结
GROUP_CONCAT 的本质是把分组后的多行值压缩成一个字符串,常用于报表、列表页和聚合展示场景。
使用时重点记住这几件事:
- 它通常和
GROUP BY一起使用 - 默认分隔符是逗号,可以通过
SEPARATOR自定义 - 需要去重时加
DISTINCT - 需要稳定顺序时一定要写
ORDER BY - 结果长度受
group_concat_max_len限制,超长会被截断 - 它适合查询展示,不适合替代关系型建模
如果只是记一条经验,那就是:线上使用 GROUP_CONCAT 时,几乎总要考虑排序、去重和长度限制。