还在写多个SQL拼报表?试试这个一句SQL搞定所有层级汇总的“外挂”。
90%的人只会用GROUP BY做基础分组
日常工作中,我们经常需要对数据做分组统计:

-- 按部门统计销售额SELECT department, SUM(sales_amount) AS total_salesFROM sales_recordsGROUP BY department;输出:
department | total_sales |
技术部 | 100000 |
市场部 | 150000 |
销售部 | 200000 |
但如果老板要的报表是这样的:
- 每个部门的汇总
- 所有部门的合计
- 甚至每个部门下再分产品线的汇总
传统做法:写3个SQL,UNION ALL拼起来,或者导出Excel手工做。代码臃肿,性能差,维护成本高。
WITH ROLLUP:一句SQL搞定N层汇总
WITH ROLLUP 是GROUP BY的扩展,可以在分组结果后自动追加“小计”和“总计”行。
语法极简: 在GROUP BY后面加个 WITH ROLLUP 就行。
案例1:单维度汇总 + 总计
SELECT department, SUM(sales_amount) AS total_salesFROM sales_recordsGROUP BY department WITH ROLLUP;输出:
department | total_sales | |
技术部 | 100000 | |
市场部 | 150000 | |
销售部 | 200000 | |
NULL | 450000 | ← 总计行,NULL代表全部部门的汇总 |
一行NULL代表总计,报表直接可用。
案例2:多维度钻取汇总
这是ROLLUP真正强大的地方。按 部门 -> 产品线 两级分组,自动生成:产品线小计 + 部门小计 + 总计。
SELECT department, product_line, SUM(sales_amount) AS total_salesFROM sales_recordsGROUP BY department, product_line WITH ROLLUP;输出:
department | product_line | total_sales | |
技术部 | 软件 | 60000 | |
技术部 | 硬件 | 40000 | |
技术部 | NULL | 100000 | ← 技术部小计 |
市场部 | 广告 | 80000 | |
市场部 | 活动 | 70000 | |
市场部 | NULL | 150000 | ← 市场部小计 |
销售部 | 直销 | 120000 | |
销售部 | 渠道 | 80000 | |
销售部 | NULL | 200000 | ← 销售部小计 |
NULL | NULL | 450000 | ← 总计 |
注意输出顺序: ROLLUP按分组字段从右向左逐层汇总,NULL代表当前维度的小计。
案例3:三维度报表
业务场景:按 年份 -> 季度 -> 区域 统计销售额。
SELECT YEAR(order_date) AS year, QUARTER(order_date) AS quarter, region, SUM(amount) AS total_amount, COUNT(*) AS order_countFROM ordersGROUP BY year, quarter, region WITH ROLLUP;一句SQL生成:
- 每个区域在该季度的销售额
- 每个季度的小计(跨区域)
- 每年的小计(跨季度)
- 总计(跨年)
报表系统需要的就是这种多层级钻取数据。
让报表更优雅:处理NULL值
NULL在报表里不好看,用 COALESCE 或 IFNULL 替换成更友好的文字:
SELECT COALESCE(department, '【全公司合计】') AS department, COALESCE(product_line, '【部门小计】') AS product_line, SUM(sales_amount) AS total_salesFROM sales_recordsGROUP BY department, product_line WITH ROLLUP;输出:
department | product_line | total_sales |
技术部 | 软件 | 60000 |
技术部 | 硬件 | 40000 |
技术部 | 【部门小计】 | 100000 |
市场部 | 广告 | 80000 |
市场部 | 活动 | 70000 |
市场部 | 【部门小计】 | 150000 |
销售部 | 直销 | 120000 |
销售部 | 渠道 | 80000 |
销售部 | 【部门小计】 | 200000 |
【全公司合计】 | 【部门小计】 | 450000 |
这样导出的Excel直接可读,无需二次处理。
性能优化:ROLLUP的执行计划
ROLLUP并不是多次GROUP BY再UNION,而是通过一次扫描完成所有层级的聚合。执行计划中会看到 Using temporary; Using filesort,这是正常的。
优化建议:
- 分组字段顺序:把基数低的字段放前面(如部门、年份),基数高的放后面(如产品、月份)
- 只SELECT必要字段
- 配合WHERE条件先过滤数据
-- 好的写法:先过滤,再汇总SELECT department, product_line, SUM(sales_amount) AS total_salesFROM sales_recordsWHERE order_date >= '2024-01-01' -- 先过滤GROUP BY department, product_line WITH ROLLUP;避坑指南
- ROLLUP与ORDER BY的配合:ROLLUP产生的汇总行在结果集末尾,如果用了ORDER BY会打乱层级顺序。建议在应用层排序,或者用ORDER BY IS NULL, department这种方式让汇总行保持最后。
- MySQL版本:MySQL 5.5+支持,5.7+优化更佳。PG用GROUP BY ROLLUP(...),SQL Server用WITH ROLLUP,语法略有差异。
- 识别汇总行:通过分组字段是否为NULL判断当前行是哪一层汇总。多维度时,从右向左为NULL的字段就是汇总维度。
实际应用场景
- BI报表导出:用户在前端勾选多层级,后端一句SQL返回带钻取的数据结构
- 财务对账:按机构->部门->科目逐级汇总
- 销售看板:按区域->城市->门店层级统计
- 数据核对:快速验证明细和汇总是否一致(总计行和明细SUM对不上就是数据问题)
总结
WITH ROLLUP的威力在于:
- 一句SQL替代多句SQL+UNION
- 一次扫描完成多层级聚合,性能优于多次查询
- 层级可扩展:GROUP BY几个字段就自动生成几层汇总
下次写报表统计时,别再用UNION ALL拼合计了。GROUP BY ... WITH ROLLUP,干净利落。