原创

MySQL中GROUP_CONCAT的用法详解:语法、示例与常见坑

GROUP_CONCAT 是什么,解决什么问题

GROUP_CONCAT 是 MySQL 中常用的聚合函数,用来把同一组内的多行值拼接成一个字符串。它最适合处理这类需求:

  • 按部门汇总员工姓名列表
  • 按订单汇总商品名称
  • 按用户汇总角色集合
  • 按分类汇总标签列表
  • 把一对多关系压缩成一行展示

COUNTSUMAVG 一样,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

如果你希望返回空字符串,可以结合 IFNULLCOALESCE

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 限制导致的。

排查思路:

  1. 检查 group_concat_max_len
  2. 用实际生产数据测试
  3. 对导出类接口做长度验证
  4. 必要时改用更适合的数据输出方式,而不是无限制拼接长字符串

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 时,几乎总要考虑排序、去重和长度限制。

正文到此结束
评论插件初始化中...
Loading...