GROUP BY + ROLLUP 是最直接的解法,它自动生成明细、分组小计和全局总计三类行,如 ROLLUP(a,b) 产生 (a,b)、(a,NULL)、(NULL,NULL),需用 GROUPING() 准确区分层级,避免 NULL 误判。

GROUP BY + ROLLUP 是最直接的解法
想在一条查询里同时拿到每行明细、按某维度分组的小计、以及全局总计,GROUP BY 配合 ROLLUP 就是标准答案。它不是额外加层视图或 UNION,而是 SQL 标准支持的分组扩展语法,MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都原生支持。
关键点在于:ROLLUP 会自动生成多级分组组合,包括(a,b,c)、(a,b)、(a)、(),最后一级就是空分组——对应总计行。
-
ROLLUP(a, b)生成分组:(a,b)、(a,NULL)、(NULL,NULL),注意第二级小计中b列值为NULL,需用GROUPING()或COALESCE()识别 - 明细行必须保留原始数据,不能只靠
ROLLUP;得用UNION ALL拼接原始明细 + 分组结果,或改用GROUPING SETS(更灵活但兼容性略差) - MySQL 8.0 之前不支持
ROLLUP的完整语义,旧版本只能手写UNION ALL多次查询,性能和维护成本明显上升
如何区分明细、小计、总计这三类行
光有 ROLLUP 不够,返回结果里所有行字段都一样,你得靠 GROUPING() 函数标记层级。它对当前分组中“被折叠”的列返回 1,否则返回 0。比如 GROUPING(dept) 在 dept 小计行返回 1,在明细行返回 0,在总计行也返回 1(因为 dept 和 name 都被折叠了)。
常用套路是加一列说明:
SELECT
COALESCE(dept, '总计') AS dept,
COALESCE(name, CASE WHEN GROUPING(name) = 1 AND GROUPING(dept) = 0 THEN '小计' ELSE '' END) AS name,
SUM(salary) AS total_salary,
CASE
WHEN GROUPING(dept) = 1 THEN '总计'
WHEN GROUPING(name) = 1 THEN '部门小计'
ELSE '明细'
END AS level
FROM employees
GROUP BY dept, name WITH ROLLUP;
注意:GROUPING() 不是所有数据库都支持(如 SQLite 就没有),此时得靠判断 dept IS NULL 和 name IS NULL 组合,但逻辑易错——比如 dept=NULL AND name=NULL 可能是总计,也可能是 dept 本身存了 NULL 值,必须配合业务约束或额外标识字段。
UNION ALL 手动拼接时最容易漏的细节
如果数据库不支持 ROLLUP,或者你需要更精确控制每类行的排序和字段内容(比如明细要显示 id,小计/总计不需要),就得用 UNION ALL 把三部分查出来再合并。这时候最容易出问题的是:
- 各子查询字段数、类型、顺序必须严格一致,否则报错
Operand should contain 3 column(s) - 明细查询里不能写
GROUP BY,否则自动去重,丢失重复记录;小计和总计才需要GROUP BY - 排序必须放在最后的
ORDER BY,不能在每个子查询里单独排——否则合并后顺序全乱 - 小计行的“部门名称”字段,要写成
dept AS dept_name,而明细行得写dept AS dept_name,不能一个写''一个写NULL,否则字符串比较时行为不一致
性能和可读性之间的实际取舍
ROLLUP 写起来短,但执行计划里通常会多一次哈希分组扫描,大数据量下比单次明细查询慢 2–5 倍;UNION ALL 方案可以给每部分单独加索引提示,但 SQL 长、嵌套深,后续加字段或改条件时容易漏改某一部分。
真正上线前建议做两件事:
- 用
EXPLAIN对比两种写法的扫描行数和临时表使用情况,尤其关注是否出现Using temporary; Using filesort - 把
dept换成高基数字段(比如order_id)试试——ROLLUP会生成巨量小计行,可能撑爆内存,这时宁可拆成应用层聚合
别指望一条 SQL 解决所有场景。明细+小计+总计看着是格式需求,背后常连着导出、分页、权限过滤——这些都会让“一条语句搞定”的想法迅速变得脆弱。

















