使用GROUP BY ... WITH ROLLUP可生成小计和总计,但必须用GROUPING()区分汇总行与真实NULL值;字段顺序决定汇总层级,排序和HAVING需基于GROUPING()控制逻辑。

直接用 GROUP BY ... WITH ROLLUP 就能生成带小计和总计的分组结果,但必须配合 GROUPING() 判断 NULL 是数据本身为空,还是 ROLLUP 自动生成的汇总行——否则“小计”和“真正为空的数据”会混在一起。
WITH ROLLUP 的字段顺序决定汇总层级
ROLLUP 按 GROUP BY 后字段从右向左逐级上卷。比如 GROUP BY region, city WITH ROLLUP 会产出四层结果:
- 每组
region + city的明细(GROUPING(region)=0,GROUPING(city)=0) - 每个
region下所有city的小计(city列为NULL,且GROUPING(city)=1) - 全表总计(
region和city都为NULL,且GROUPING(region)=1,GROUPING(city)=1)
如果写成 GROUP BY city, region WITH ROLLUP,小计就变成“每个 city 下所有 region”,逻辑完全颠倒。顺序错了,报表就错。
用 GROUPING() 函数区分汇总行和空值
只靠 IS NULL 判断会出错。例如 city 字段本身允许为空,那 WHERE city IS NULL 会把真实空数据和小计行一起过滤掉。
正确做法是用 GROUPING():
-
GROUPING(city) = 1→ 这行是 ROLLUP 生成的,city被上卷了 -
GROUPING(city) = 0→ 这行是原始数据,city值哪怕为NULL也是业务数据 - 多列时组合判断更安全:比如
GROUPING(region)=0 AND GROUPING(city)=1表示“该region小计”
典型写法:
SELECT
CASE WHEN GROUPING(region) = 1 THEN '总计'
WHEN GROUPING(city) = 1 THEN region + ' 小计'
ELSE region END AS region_label,
CASE WHEN GROUPING(city) = 1 THEN NULL ELSE city END AS city,
SUM(sales) AS total_sales
FROM sales_data
GROUP BY region, city WITH ROLLUP
ORDER BY 必须考虑 GROUPING() 才能控制汇总行位置
ROLLUP 生成的汇总行默认排在对应分组末尾,但数据库不保证绝对顺序。如果想让“小计”紧贴它所属的 region 组后面、“总计”固定在最底,就得显式排序:
- 先按
GROUPING(region) DESC把非汇总行(0)排前面 - 再按
region正序,保证同 region 数据连续 - 再按
GROUPING(city) DESC,让该 region 内的小计排在明细之后 - 最后按
city正序
完整排序子句:ORDER BY GROUPING(region) DESC, region, GROUPING(city) DESC, city
HAVING 子句容易误删汇总行
常见错误是写 HAVING COUNT(*) > 0 或 HAVING city IS NOT NULL —— 这些条件会在聚合后执行,而汇总行的 city 是 NULL、某些聚合值可能为 0,结果小计和总计全被干掉。
如真要过滤,必须基于 GROUPING():
- 只保留明细行:
HAVING GROUPING(region) = 0 AND GROUPING(city) = 0 - 保留明细 + 小计,不要总计:
HAVING GROUPING(region) = 0 - 只要总计:
HAVING GROUPING(region) = 1 AND GROUPING(city) = 1
最容易被忽略的是:ROLLUP 行的聚合计算逻辑和明细行一致,但语义不同。比如 COUNT(*) 在小计行统计的是“该 region 下有多少个 city”,不是“该 region 总订单数”——要后者得用 SUM(COUNT(*)) OVER (PARTITION BY region) 或提前聚合。

















