grouping sets 是真正减少表扫描次数的多维聚合方案,一次全表扫描即可输出多个粒度结果;其核心是显式列出分组组合,未参与分组字段置为占位 null,须用 grouping() 或 grouping_id() 区分占位与真实 null,且仅 grouping sets 能精确控制所需维度组合,避免 rollup/cube 的冗余计算。

GROUPING SETS 不是语法糖,而是真正减少表扫描次数的多维聚合方案——一次全表扫描,输出多个粒度结果。
GROUPING SETS 基本写法与字段 NULL 的含义
GROUPING SETS 的核心是显式列出所有需要的分组组合,每个元组对应一种聚合粒度。未参与当前分组的字段在结果中自动为 NULL,这不是数据缺失,而是 SQL 引擎生成的占位符。
-
GROUPING SETS ((region, product), (region), ())会产出三类行:带具体region和product值的明细聚合、仅带region值的地区汇总、以及region和product全为NULL的全局总计 - 字段为
NULL时,不能直接用WHERE region IS NULL过滤“全局行”,因为原始数据里也可能真有NULL;必须配合GROUPING(region)判断是否为聚合占位 - 如果想把
NULL替换成业务可读值(如“全国”“全部渠道”),得用COALESCE(region, '全国'),但注意:替换后就无法再用GROUPING()函数区分了
如何用 GROUPING() 和 GROUPING_ID() 区分真实 NULL 与聚合占位
只靠字段值是否为 NULL 无法判断它是原始数据还是聚合产生的占位符。这时候必须用 GROUPING() 或 GROUPING_ID()。
-
GROUPING(region)返回 1 表示该行中region是因未参与当前分组而被置为NULL;返回 0 表示该NULL来自原始数据 -
GROUPING_ID(region, product)把每个字段的GROUPING()结果当二进制位拼起来:比如(region, product)分组下,GROUPING_ID= 0;仅按region分组时,product占位 →GROUPING_ID= 1(二进制01);全不参与分组时为 3(11) - 实际过滤时推荐写法:
HAVING GROUPING_ID(region, product) = 0只取最细粒度,“= 1”取仅按 region 聚合的行,“= 3”取全局总计
GROUPING SETS vs ROLLUP / CUBE:别无脑替换
ROLLUP(a, b, c) 和 CUBE(a, b) 看似更简洁,但它们是预设模式,不能跳过中间粒度。一旦需求要“只要 a+b 和全局,不要单独的 a”,就必须用 GROUPING SETS 显式声明。
-
ROLLUP(region, product)等价于GROUPING SETS ((region, product), (region), ())—— 它强制包含 (region) 这一层 -
CUBE(region, product)等价于GROUPING SETS ((region, product), (region), (product), ())—— 多出 (product) 单独聚合,可能根本不需要 - 真正灵活的场景(比如只聚合 (region, channel) 和 (),跳过单维度)只能靠
GROUPING SETS手动列出来,否则要么多算、要么漏算 - 某些引擎(如旧版 Hive)对
CUBE支持不全,但GROUPING SETS兼容性更稳
性能关键:它真能省掉多次扫描吗?
能,但前提是底层引擎支持——Spark 3.0+、Hive 2.0+、PostgreSQL 9.5+、SQL Server 2008+ 都已实现单次扫描多路聚合。MySQL 直到 8.0.12 才支持,且早期版本优化不足。
- 传统
UNION ALL写法:3 个GROUP BY→ 至少 3 次全表扫描(除非有物化视图或缓存) -
GROUPING SETS:1 次扫描 + 1 次哈希分组,内部按不同 key 组合分流聚合,CPU 和 IO 开销明显更低 - 实测在 Spark on Parquet 上,5 个分组集比 5 个 UNION 查询快 2.3–3.1 倍;但在小表(
- 注意 shuffle 压力:分组集越多,reduce 端 key 组合越爆炸,
GROUPING SETS ((a,b,c), (a,b), (a), (b), (c), ())实际会产生大量稀疏 key,不如拆成两个查询
最容易被忽略的是:GROUPING SETS 输出的 NULL 是语义化的,不是错误;没配 GROUPING() 就直接 WHERE 过滤,大概率漏数据或误删真实 NULL 行。











