PostgreSQL GROUPING SETS 多维汇总:一次扫描替代 UNION ALL

举报
yd_250998817 发表于 2026/11/02 10:34:40 2026/11/02
【摘要】 用 PostgreSQL 的 GROUPING SETS 在订单表上一次性生成地区、品类、地区×品类和总计四种粒度汇总,并用 GROUPING() 区分真实 NULL 与汇总 NULL,替代多次 UNION ALL 扫描。

在报表开发中,经常需要同一张订单表按多个粒度汇总:地区×品类、地区、品类和总计。常见写法是用四条聚合 SQL 加 UNION ALL 拼接。它直观,但每条分支都会独立访问订单表,过滤条件重复,维护成本高,而且多个分支可能读到不同快照。PostgreSQL 的 GROUPING SETS 允许在一个查询中声明多组分组键,通常只需对基表扫描一次;再配合 GROUPING() 函数,可以区分真实 NULL 和汇总行产生的 NULL。

问题场景与表结构

下面用一张简化的订单表演示。金额使用 numeric,日期用于限定报表周期。样例数据中特意保留 region 或 category 为 NULL 的行,用来验证 GROUPING() 的区分能力。

CREATE TABLE orders (
  id bigserial PRIMARY KEY,
  region text,
  category text,
  amount numeric(12,2) NOT NULL,
  created_at date NOT NULL
);

INSERT INTO orders (region, category, amount, created_at) VALUES
  ('north', 'book', 100, DATE '2024-01-01'),
  ('north', 'book', 50,  DATE '2024-01-02'),
  ('north', 'toy', 30,   DATE '2024-01-03'),
  ('south', 'book', 80,  DATE '2024-01-04'),
  (NULL, 'book', 20,     DATE '2024-01-05'),
  ('north', NULL, 40,    DATE '2024-01-06');

一条 SQL 生成四种粒度

查询使用 GROUPING SETS 声明四组分组键:(region, category)、(region)、(category) 和 ()。空集 () 表示总计。SELECT 中的 GROUPING(region) 和 GROUPING(category) 返回 0 或 1:0 表示该列参与了当前分组,1 表示该列被汇总掉了。

SELECT
  region,
  category,
  GROUPING(region) AS g_region,
  GROUPING(category) AS g_category,
  SUM(amount) AS total_amount
FROM orders
WHERE created_at >= DATE '2024-01-01'
  AND created_at < DATE '2024-02-01'
GROUP BY GROUPING SETS (
  (region, category),
  (region),
  (category),
  ()
)
ORDER BY
  CASE
    WHEN GROUPING(region)=0 AND GROUPING(category)=0 THEN 1
    WHEN GROUPING(region)=0 AND GROUPING(category)=1 THEN 2
    WHEN GROUPING(region)=1 AND GROUPING(category)=0 THEN 3
    ELSE 4
  END,
  region NULLS LAST,
  category NULLS LAST;

这里的关键不是把 NULL 当成“汇总”标记,而是用 g_region、g_category 标记行的层级。真实 region 为 NULL 的明细行是 region IS NULL AND g_region=0;地区汇总行的 region 也可能是 NULL,但 g_region=0 AND g_category=1,含义完全不同。

运行方法

将建表、插入和查询语句保存为 demo.sql,在测试库执行:

psql -h <主机> -p 5432 -d <数据库> -U <用户> -f demo.sql

也可以进入 psql 后使用 \i demo.sql。执行前确认连接的是测试环境,示例表名和字段名按实际报表调整。

结果与排序示例

按上面的 ORDER BY,输出会先列明细,再列地区小计、品类小计,最后是总计。金额结果如下:

region category g_region g_category total_amount
north book 0 0 150.00
north toy 0 0 30.00
north NULL 0 0 40.00
south book 0 0 80.00
NULL book 0 0 20.00
north NULL 0 1 220.00
south NULL 0 1 80.00
NULL NULL 0 1 20.00
NULL book 1 0 250.00
NULL toy 1 0 30.00
NULL NULL 1 0 40.00
NULL NULL 1 1 320.00

其中 north NULL 0 0 40.00 是真实 category 为 NULL 的明细;NULL book 0 0 20.00 是真实 region 为 NULL 的明细。两个标志都为 1 的行才是总计。展示层可以根据标志生成“明细/地区小计/品类小计/总计”,不要直接用 COALESCE 把汇总 NULL 改成 0 或“未知”。

EXPLAIN 对比扫描次数

先看等效的 UNION ALL 写法。四个分支的过滤条件相同,但每个分支各自聚合:

EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT region, category, 0 AS g_region, 0 AS g_category, SUM(amount) AS total_amount
FROM orders
WHERE created_at >= DATE '2024-01-01' AND created_at < DATE '2024-02-01'
GROUP BY region, category
UNION ALL
SELECT region, NULL::text, 0, 1, SUM(amount)
FROM orders
WHERE created_at >= DATE '2024-01-01' AND created_at < DATE '2024-02-01'
GROUP BY region
UNION ALL
SELECT NULL::text, category, 1, 0, SUM(amount)
FROM orders
WHERE created_at >= DATE '2024-01-01' AND created_at < DATE '2024-02-01'
GROUP BY category
UNION ALL
SELECT NULL::text, NULL::text, 1, 1, SUM(amount)
FROM orders
WHERE created_at >= DATE '2024-01-01' AND created_at < DATE '2024-02-01';

再用 GROUPING SETS 观察计划,可暂时去掉 ORDER BY 以突出扫描和聚合节点:

EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT
  region,
  category,
  GROUPING(region) AS g_region,
  GROUPING(category) AS g_category,
  SUM(amount) AS total_amount
FROM orders
WHERE created_at >= DATE '2024-01-01'
  AND created_at < DATE '2024-02-01'
GROUP BY GROUPING SETS ((region, category), (region), (category), ());

读计划时重点看 orders 的扫描节点。UNION ALL 计划中,orders 的扫描节点会按分支重复出现;GROUPING SETS 计划中通常只出现一个 orders 扫描节点,聚合部分可能表现为 MixedAggregate 或拆成多个 GroupAggregate。若表是分区表,Append 下每个分区仍可能出现子扫描,不能只数节点个数。实际计划受版本、统计信息、索引和工作负载影响,应使用 EXPLAIN (ANALYZE, BUFFERS) 在目标环境验证,不要只凭成本估算下结论。

边界与限制

  • GROUPING SETS 和 GROUPING() 在较新的 PostgreSQL 版本中可用;若目标环境版本较旧,需要改用 UNION ALL 或外层查询。
  • GROUPING() 的参数应来自 GROUP BY 中的分组表达式,不能随意用于未分组的列。
  • HAVING 会作用于所有粒度。若在 HAVING 中过滤金额,可能连小计和总计一起过滤;需要保留层级时,把过滤放到外层查询。
  • 不同粒度需要不同 WHERE 条件时,GROUPING SETS 不能直接表达,应使用条件聚合或 UNION ALL。
  • 四种粒度会输出更多行,排序和聚合可能使用临时文件;数据量大时关注 work_mem 和计划中的 Sort、HashAggregate 节点。
  • 它减少的是重复扫描和重复过滤,不替代索引设计;查询仍要读取满足条件的行,性能应以实际执行计划为准。

小结

GROUPING SETS 把多粒度报表收敛到一个查询里,配合 GROUPING() 保留 NULL 的语义,适合地区、品类、总计这类固定维度的汇总。先用小数据验证层级和 NULL 标记,再用 EXPLAIN 对比实际计划,比直接堆 UNION ALL 更易维护。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。