SQL GROUPING SETS 与 ROLLUP/CUBE 的多维汇总效率对比

2次阅读

grouping sets 不一定比 rollup 快,性能取决于执行计划而非语法;postgresql 和 sql server 通常生成相同计划,oracle 12c+ 在统计信息缺失时可能更慢;mysql 8.0 不支持 grouping sets,rollup 为伪实现。

SQL GROUPING SETS 与 ROLLUP/CUBE 的多维汇总效率对比

GROUPING SETS 比 ROLLUP 快吗?看执行计划,不是看写法

快不快,取决于数据库实际生成的执行计划,而不是你写了 GROUPING SETS 还是 ROLLUP。PostgreSQL 和 SQL Server 通常会对等价的 ROLLUP(a,b)GROUPING SETS((a,b),(a),()) 生成完全一样的计划;但 Oracle 12c+ 在某些统计信息缺失时,可能对 GROUPING SETS 多做一次哈希重分区,反而慢 10%~20%。

实操建议:

  • 先用 EXPLAIN (ANALYZE, BUFFERS)(PostgreSQL)或 SET STATISTICS xml ON(SQL Server)比对真实执行路径,别信语法“看起来更直接”
  • 如果字段顺序敏感(比如 a 高基数、b 低基数),显式写 GROUPING SETS((a,b),(a),()) 可能比 ROLLUP(a,b) 更容易触发索引跳扫(尤其在 PostgreSQL 中配合 INDEX ON t(a,b)
  • CUBE(a,b) 等价于 GROUPING SETS((a,b),(a),(b),()),但多数引擎会把 CUBE 当作独立算子优化——Oracle 会尝试位图转换,而 DuckDB 目前仍拆成多个 union ALL,性能差距可达 3×

GROUPING SETS 的 NULL 值陷阱:不是数据为空,是分组标记

很多人看到结果里某列出现 NULL,就以为原始数据丢了,其实那是 GROUPING() 函数的标记机制在起作用——NULL 表示该维度被“折叠”,不是原始值为 NULL

常见错误现象:

  • WHERE col IS NULL 过滤汇总行,误删真实数据中的 NULL 记录
  • 没加 GROUPING() 判断,直接在应用层把所有 NULL 当“总计行”处理,导致明细和汇总混在一起
  • ORDER BY 里只排字段,不按 GROUPING(col) 分层,导致小计行插在明细中间

正确做法是显式识别:例如 select a, b, SUM(x), GROUPING(a) AS ga, GROUPING(b) AS gb FROM t GROUP BY GROUPING SETS((a,b),(a),()),再用 ga=1 AND gb=1 定位全汇总行。

ROLLUP/CUBE 在 MySQL 8.0+ 是伪实现,GROUPING SETS 不支持

MySQL 8.0 虽然加了 ROLLUP 语法,但它底层是改写成多个 UNION ALL + 单层 GROUP BY,没有真正的多维聚合算子。这意味着:

  • 每组 GROUP BY 都要全表扫描一遍,4 维 ROLLUP 就是 2⁴ = 16 次扫描(实际优化后略少,但仍是 O(2ⁿ))
  • CUBE 在 MySQL 中根本不可用,会报错 Error 1235 (42000): this version of MySQL doesn't yet support 'CUBE'
  • 想用 GROUPING SETS?不行。MySQL 至今(8.0.33)仍不支持该语法,必须手写 UNION ALL 或换引擎(如 clickhouse、DuckDB)

如果你在 MySQL 里硬要模拟 GROUPING SETS((a),(b),()),最稳的写法是:(SELECT a, NULL AS b, SUM(x) FROM t GROUP BY a) UNION ALL (SELECT NULL, b, SUM(x) FROM t GROUP BY b) UNION ALL (SELECT NULL, NULL, SUM(x) FROM t),注意加括号和字段对齐。

大数据量下,预聚合比任何 GROUPING 方式都管用

当单表超千万行、维度超 3 个时,无论 GROUPING SETS 还是 ROLLUP,都会面临内存溢出或临时磁盘爆满。这时候语法差异已经不重要了,关键是你有没有提前建好物化聚合表。

实操建议:

  • 用定时任务(如 pg_cron)每天凌晨跑:INSERT INTO agg_daily_a_b SELECT a, b, count(*), SUM(revenue) FROM fact_sales WHERE dt = CURRENT_DATE - INTERVAL '1 day' GROUP BY a, b
  • 对高频查询的组合(如 (region, product_category)),单独建索引:CREATE INDEX idx_agg_region_cat ON agg_daily_a_b(region, product_category)
  • 避免在大宽表上直接跑 GROUPING SETS——即使引擎支持,也大概率触发 HashAgg: Rehashing due to memory limit(PostgreSQL)或 Warning: Hash aggregate spilled to disk(SQL Server)

真正卡住性能的,从来不是语法选错,而是忘了数据规模已经超出单次聚合的合理边界。

text=ZqhQzanResources