GROUPING / GROUPING_ID
用 GROUPING SETS / ROLLUP / CUBE 生成的汇总行里,被上卷掉的列显示为 NULL——这与「数据本身真的是 NULL」在结果中完全无法区分。GROUPING() 函数正是为解决这一歧义而生:它返回 1 表示该列在当前分组集中未参与分组(是汇总行),返回 0 表示参与分组。本节还会说明一个容易踩坑的点:PostgreSQL 没有独立的 GROUPING_ID() 函数,多参数 GROUPING(a, b, c) 就是它的等价物。学完本节,你将能用 GROUPING() 打标签、排序、过滤,直接产出可交付的层级报表。
前置知识
阅读本节前,建议先了解:CUBE
基础概念
GROUPING() 是本组语法的「元信息读出器」:它不读数据,只读「当前这行是由哪个分组集产生的」。
| 概念 | 术语 | 说明 |
|---|---|---|
| 分组标记 | GROUPING(col) | 单列形式,返回 0 或 1 的 integer |
| 分组位图 | Grouping Bitmap | 多列形式 GROUPING(a,b,c) 返回的整数 |
| 分组 ID | GROUPING_ID | 其他数据库的函数名,PostgreSQL 用多参数 GROUPING() 代替 |
| 歧义 | Ambiguity | 数据本身为 NULL 与汇总产生的 NULL 无法区分 |
| 明细行 | Detail Row | 所有 GROUPING() 均为 0 的行 |
| 汇总行 | Summary Row | 至少一列 GROUPING() 为 1 的行 |
为什么必须要有它——看一个真实的歧义场景:
-- 假设 city 列允许为空,且确实存在 city 为 NULL 的订单(如「海外直邮」未填城市)
CREATE TABLE sales (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
city TEXT, -- 允许 NULL
category TEXT NOT NULL,
amount NUMERIC(12, 2) NOT NULL
);
INSERT INTO sales (city, category, amount) VALUES
('北京', '手机', 1000.00),
('上海', '手机', 2000.00),
(NULL, '手机', 500.00); -- 真实存在的空城市数据
-- 下面这条查询的输出里,city IS NULL 的行有两类来源,无法区分
SELECT city, sum(amount) AS 销售额
FROM sales
GROUP BY ROLLUP (city)
ORDER BY city NULLS LAST; city | 销售额
-------+---------
上海 | 2000.00
北京 | 1000.00
| 500.00 <- 这是「city 真的为 NULL」的明细行
| 3500.00 <- 这是「全表总计」的汇总行两个 city 都是 NULL,语义完全不同。加上 GROUPING() 后立刻清晰:
SELECT
CASE WHEN GROUPING(city) = 1 THEN '全部城市' ELSE coalesce(city, '(未填写)') END AS 城市,
sum(amount) AS 销售额,
GROUPING(city) AS g
FROM sales
GROUP BY ROLLUP (city)
ORDER BY g, city NULLS LAST; 城市 | 销售额 | g
------------+---------+---
北京 | 1000.00 | 0
上海 | 2000.00 | 0
(未填写) | 500.00 | 0 <- 真实空值,g = 0
全部城市 | 3500.00 | 1 <- 汇总行,g = 1返回值规则:
| 表达式 | 返回值 | 类型 | 含义 |
|---|---|---|---|
GROUPING(col) | 0 | integer | col 参与了当前分组集 |
GROUPING(col) | 1 | integer | col 未参与,该列被汇总 |
GROUPING(a, b) | 0 ~ 3 | integer | 位图,a 为高位、b 为低位 |
GROUPING(a, b, c) | 0 ~ 7 | integer | 位图,a 为最高位、c 为最低位 |
多参数位图语义(以 GROUPING(a, b, c) 为例):
| 分组集 | GROUPING(a) | GROUPING(b) | GROUPING(c) | GROUPING(a,b,c) |
|---|---|---|---|---|
(a, b, c) | 0 | 0 | 0 | 000₂ = 0 |
(a, b) | 0 | 0 | 1 | 001₂ = 1 |
(a, c) | 0 | 1 | 0 | 010₂ = 2 |
(a) | 0 | 1 | 1 | 011₂ = 3 |
(b, c) | 1 | 0 | 0 | 100₂ = 4 |
(b) | 1 | 0 | 1 | 101₂ = 5 |
(c) | 1 | 1 | 0 | 110₂ = 6 |
() | 1 | 1 | 1 | 111₂ = 7 |
关于 GROUPING_ID 的重要澄清:SQL 标准与部分数据库(Oracle、SQL Server、Databricks)提供独立的 GROUPING_ID(...) 函数。PostgreSQL 没有这个函数,执行 SELECT GROUPING_ID(city) 会直接报错:
-- 错误!PostgreSQL 中不存在 GROUPING_ID 函数
SELECT GROUPING_ID(city, category), sum(amount)
FROM sales
GROUP BY CUBE (city, category);ERROR: function grouping_id(text, text) does not exist
LINE 1: SELECT GROUPING_ID(city, category), sum(amount)
^
HINT: No function matches the given name and argument types.正确写法是直接用多参数 GROUPING(),语义完全一致:
-- 正确:多参数 GROUPING() 就是 PostgreSQL 的 GROUPING_ID 等价写法
SELECT GROUPING(city, category) AS gid, sum(amount)
FROM sales
GROUP BY CUBE (city, category);语法与用法
-- 单列标记:0 = 参与分组,1 = 被汇总
GROUPING(列)
-- 多列位图:等价于其他数据库的 GROUPING_ID
GROUPING(列1, 列2, 列3)准备本节贯穿使用的示例表(在 demo 库中执行):
-- 销售明细表:城市可为空(用于演示 NULL 歧义),含渠道与时间
CREATE TABLE sales (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
city TEXT, -- 允许 NULL:代表「城市未填写」
category TEXT NOT NULL,
channel TEXT NOT NULL,
amount NUMERIC(12, 2) NOT NULL CHECK (amount >= 0),
sold_at DATE NOT NULL
);
-- 造数:5 城市 + 少量空城市,4 品类,3 渠道
INSERT INTO sales (city, category, channel, amount, sold_at)
SELECT
CASE WHEN i % 37 = 0 THEN NULL
ELSE (ARRAY['北京', '上海', '广州', '深圳', '杭州'])[1 + (i % 5)]
END,
(ARRAY['手机', '电脑', '家电', '配件'])[1 + (i % 4)],
(ARRAY['线上', '门店', '分销'])[1 + (i % 3)],
round((random() * 2000 + 100)::numeric, 2),
DATE '2026-01-01' + (i % 180)
FROM generate_series(1, 30000) AS i;
CREATE INDEX idx_sales_city_cat ON sales (city, category);
ANALYZE sales;最小可运行示例——给汇总行打上中文标签:
SELECT
CASE WHEN GROUPING(city) = 1 THEN '全部城市' ELSE coalesce(city, '(未填写)') END AS 城市,
CASE WHEN GROUPING(category) = 1 THEN '全部品类' ELSE category END AS 品类,
sum(amount) AS 销售额
FROM sales
GROUP BY ROLLUP (city, category)
ORDER BY GROUPING(city), city NULLS LAST,
GROUPING(category), category; 城市 | 品类 | 销售额
-----------+----------+-----------
(未填写) | 手机 | 8123.40
(未填写) | | 32201.55 <- 「未填写」城市的城市小计
上海 | 手机 | 88012.40
上海 | | 352011.77
全部城市 | |1489002.55 <- 全表总计详细说明
用 GROUPING() 打标签:区分「真 NULL」与「汇总 NULL」
这是 GROUPING() 最主要的用途。写法固定为 CASE WHEN GROUPING(x) = 1 THEN '全部xx' ELSE x END:
SELECT
CASE
WHEN GROUPING(city) = 1 THEN '【全部城市】'
ELSE coalesce(city, '【未填写】') -- 真 NULL 单独特判,不与汇总混淆
END AS 城市,
CASE
WHEN GROUPING(channel) = 1 THEN '【全部渠道】'
ELSE channel
END AS 渠道,
count(*) AS 订单数,
sum(amount) AS 销售额
FROM sales
GROUP BY ROLLUP (city, channel)
ORDER BY GROUPING(city), city NULLS LAST,
GROUPING(channel), channel;去掉 coalesce 会怎样?真 NULL 的行会与汇总行一样显示为 NULL,报表使用者无法判断——这正是必须先处理歧义的原因。
用 GROUPING() 排序:让汇总行紧跟其明细
PostgreSQL 中 NULL 默认排序为最大值,因此 ORDER BY city 会把「全部城市」(city IS NULL)排到最末,层级结构被打散。GROUPING() 在前做首要排序键即可修复:
-- 推荐排序:GROUPING() 优先,列值次之
ORDER BY GROUPING(city), city NULLS LAST,
GROUPING(category), category;若不写 GROUPING() 而指望 NULLS LAST,会得到「所有明细行聚在一起、所有汇总行沉底」的错误结构:
-- 不推荐:汇总行全部沉到最后,失去层级嵌套感
ORDER BY city NULLS LAST, category;还可用多参数位图整体排序,层级越细(gid 越小)越靠前:
SELECT
city, category, channel,
GROUPING(city, category, channel) AS gid,
sum(amount) AS 销售额
FROM sales
GROUP BY ROLLUP (city, category, channel)
ORDER BY gid,
GROUPING(city), city NULLS LAST,
GROUPING(category), category,
GROUPING(channel), channel;用 GROUPING() 过滤:只能在 HAVING 或外层查询
这是最容易出错的一点。WHERE 在分组聚合之前求值,此时 GROUPING() 所依赖的分组集信息尚不存在,因此聚合函数与 GROUPING() 都不能出现在 WHERE 中:
-- 错误!ERROR: aggregate functions are not allowed in WHERE
SELECT city, sum(amount)
FROM sales
WHERE GROUPING(city) = 1
GROUP BY ROLLUP (city);ERROR: aggregate functions are not allowed in WHERE
LINE 3: WHERE GROUPING(city) = 1
^
HINT: Use HAVING or a subquery.正确写法一:用 HAVING(HAVING 在分组之后、聚合结果之上过滤):
-- 只保留总计行
SELECT city, sum(amount) AS 销售额
FROM sales
GROUP BY ROLLUP (city)
HAVING GROUPING(city) = 1;-- 保留小计与总计(丢掉明细行)
SELECT city, category, sum(amount) AS 销售额
FROM sales
GROUP BY ROLLUP (city, category)
HAVING GROUPING(category) = 1
ORDER BY GROUPING(city), city NULLS LAST;正确写法二:外层子查询过滤(当需要用别名字段或做更复杂条件时更灵活):
SELECT *
FROM (
SELECT
city,
category,
GROUPING(city) AS g_city,
GROUPING(category) AS g_cat,
sum(amount) AS 销售额
FROM sales
GROUP BY ROLLUP (city, category)
) t
WHERE g_cat = 1 -- 直接引用别名
ORDER BY g_city, city NULLS LAST;权衡:HAVING GROUPING(...) 少一层子查询、计划更简单;外层子查询能用别名、条件组合更自由。二者结果等价。
与 FILTER 配合:汇总行也能做条件聚合
GROUPING() 在 SELECT 列表里,count(*) FILTER (...) 也在 SELECT 列表里,两者可以自由组合,实现「同一层级下按条件统计多套指标」:
SELECT
CASE WHEN GROUPING(channel) = 1 THEN '全部渠道' ELSE channel END AS 渠道,
count(*) AS 订单数,
count(*) FILTER (WHERE category = '手机') AS 手机订单数,
sum(amount) AS 销售额,
sum(amount) FILTER (WHERE category = '手机') AS 手机销售额,
round(100.0 * sum(amount) FILTER (WHERE category = '手机')
/ NULLIF(sum(amount), 0), 2) AS 手机占比
FROM sales
GROUP BY ROLLUP (channel)
ORDER BY GROUPING(channel), channel; 渠道 | 订单数 | 手机订单数 | 销售额 | 手机销售额 | 手机占比
----------+--------+------------+-----------+------------+----------
分销 | 9993 | 2503 | 4953350.20 | 1242811.55 | 25.09
线上 | 9995 | 2491 | 4961233.11 | 1235102.40 | 24.89
门店 | 10012 | 2506 | 4974419.24 | 1248111.02 | 25.09
全部渠道 | 30000 | 7500 |14889002.55 | 3726024.97 | 25.03位图整数的实用价值
多参数 GROUPING() 返回的是位图整数,它把「哪些列被汇总」压缩成一个数字,非常适合:
- 去重:不关心具体层级、只想按层级分组统计时;
- 排序:层级顺序天然由数值大小决定(0 最细、全 1 最粗);
- 映射:在应用层用字典把
0~7映射成中文层级名。
-- 例:统计每个层级各有多少行,快速确认 CUBE 是否真的算出了 8 层
SELECT
GROUPING(city, category, channel) AS gid,
(ARRAY['城市×品类×渠道', '城市×品类', '城市×渠道', '城市',
'品类×渠道', '品类', '渠道', '总计'])[1 + GROUPING(city, category, channel)] AS 层级,
count(*) AS 行数
FROM (
SELECT city, category, channel, sum(amount)
FROM sales
GROUP BY CUBE (city, category, channel)
) t
GROUP BY GROUPING(city, category, channel)
ORDER BY gid; gid | 层级 | 行数
-----+---------------+------
0 | 城市×品类×渠道 | 89
1 | 城市×品类 | 24
2 | 城市×渠道 | 18
3 | 城市 | 6
4 | 品类×渠道 | 12
5 | 品类 | 4
6 | 渠道 | 3
7 | 总计 | 1注意:上面这个查询里,外层又用了一次 GROUPING(),但外层的 GROUPING() 是对子查询结果列求值——子查询输出里 city / category / channel 是普通列(值可能为 NULL),外层 GROUP BY GROUPING(...) 没有聚合基础,写法上是无效的。正确做法是由子查询把 gid 算好并输出,外层直接 GROUP BY gid:
-- 正确:子查询负责算 gid,外层只按 gid 分组
SELECT
gid,
(ARRAY['城市×品类×渠道', '城市×品类', '城市×渠道', '城市',
'品类×渠道', '品类', '渠道', '总计'])[1 + gid] AS 层级,
count(*) AS 行数
FROM (
SELECT
GROUPING(city, category, channel) AS gid,
sum(amount) AS 销售额
FROM sales
GROUP BY CUBE (city, category, channel)
) t
GROUP BY gid
ORDER BY gid;执行计划与开销
GROUPING() 本身是零成本的:它只是读取聚合节点已经算好的分组集标记位,不触发任何额外扫描或计算。相比不用 GROUPING() 的同一查询,计划完全一致:
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT
city,
GROUPING(city) AS g,
sum(amount)
FROM sales
GROUP BY ROLLUP (city); QUERY PLAN
--------------------------
GroupAggregate
Group Key: city
Group Key: ()
-> Index Scan using idx_sales_city_cat on sales
Buffers: shared hit=214
Execution Time: 22.118 ms个位数的 GROUPING() 调用不会出现在计划里,也不增加 Buffers。因此不要因为「怕开销」而省略 GROUPING()——它是让结果可读的必要成本,而且成本几乎为零。
GROUPING() 返回类型与可用位置汇总
| 问题 | 答案 |
|---|---|
| 返回类型 | integer |
可用于 SELECT 列表 | ✅ 可以 |
可用于 ORDER BY | ✅ 可以 |
可用于 HAVING | ✅ 可以 |
可用于 WHERE | ❌ 不可以(报 aggregate functions are not allowed in WHERE) |
可用于 GROUP BY | ❌ 不可以(对未聚合结果求值无意义) |
可用于 JOIN ... ON | ❌ 不可以 |
| 参数要求 | 参数必须是当前 GROUP BY 中出现的分组列(或其表达式) |
-- 错误!GROUPING() 的参数必须是分组列
SELECT GROUPING(sold_at), sum(amount)
FROM sales
GROUP BY ROLLUP (city);ERROR: arguments to GROUPING must be grouping expressions of the associated query level实战示例
步骤 1:可直接交付的带层级中文报表
-- 城市 × 品类层级报表:明细 / 城市小计 / 品类小计 / 总计,附中文标签
SELECT
CASE WHEN GROUPING(city) = 1 THEN '【全部城市】'
ELSE coalesce(city, '【未填写】') END AS 城市,
CASE WHEN GROUPING(category) = 1 THEN '【全部品类】'
ELSE category END AS 品类,
count(*) AS 订单数,
sum(amount) AS 销售额,
round(avg(amount), 2) AS 客单价,
GROUPING(city, category) AS gid
FROM sales
GROUP BY CUBE (city, category)
ORDER BY gid,
GROUPING(city), city NULLS LAST,
GROUPING(category), category; 城市 | 品类 | 订单数 | 销售额 | 客单价 | gid
--------------+----------+--------+-----------+----------+-----
上海 | 手机 | 331 | 415221.40 | 1254.44 | 0
上海 | 电脑 | 322 | 402118.55 | 1248.81 | 0
...
上海 | 【全部品类】| 1332 | 1655112.30 | 1242.58 | 1
【未填写】 | 手机 | 21 | 26211.40 | 1248.16 | 0
【全部城市】 | 手机 | 7500 | 3726024.97 | 496.80 | 2
【全部城市】 | 【全部品类】| 30000 |14889002.55 | 496.30 | 3注意 gid = 2 那一层(city 被汇总、category 参与分组)——这是 CUBE 特有的「按品类」汇总层,ROLLUP (city, category) 不会产出它。gid 直接反映了层级,0 最细、3 最粗。
步骤 2:用 GROUPING() 剔除明细,只交付汇总层
-- HAVING 过滤:只保留三列中至少两列被汇总的「粗粒度」行
SELECT
CASE WHEN GROUPING(city) = 1 THEN '【全部城市】' ELSE coalesce(city, '【未填写】') END AS 城市,
CASE WHEN GROUPING(category) = 1 THEN '【全部品类】' ELSE category END AS 品类,
CASE WHEN GROUPING(channel) = 1 THEN '【全部渠道】' ELSE channel END AS 渠道,
sum(amount) AS 销售额,
GROUPING(city, category, channel) AS gid
FROM sales
GROUP BY CUBE (city, category, channel)
HAVING GROUPING(city, category, channel) >= 3 -- 至少两列被汇总
ORDER BY gid,
GROUPING(city), city NULLS LAST,
GROUPING(category), category,
GROUPING(channel), channel; 城市 | 品类 | 渠道 | 销售额 | gid
--------------+------------+------------+-----------+-----
【全部城市】 | 手机 | 【全部渠道】| 3726024.97 | 2
【全部城市】 | 【全部品类】| 分销 | 4953350.20 | 4
【全部城市】 | 【全部品类】| 线上 | 4961233.11 | 4
【全部城市】 | 【全部品类】| 门店 | 4974419.24 | 4
【全部城市】 | 【全部品类】| 【全部渠道】|14889002.55| 7HAVING GROUPING(a, b, c) >= 3 比写三个 GROUPING(x) = 1 的 OR 组合简洁得多,这是位图整数的典型用法。
步骤 3:验证 GROUPING_ID 不可用、多参数 GROUPING() 可用的对照
-- 错误示例:确认 PostgreSQL 无 GROUPING_ID
SELECT GROUPING_ID(city, category, channel)
FROM sales
GROUP BY CUBE (city, category, channel)
LIMIT 1;ERROR: function grouping_id(text, text, text) does not exist
HINT: No function matches the given name and argument types.-- 正确写法:多参数 GROUPING(),并用 CASE 映射成中文层级名
SELECT DISTINCT
GROUPING(city, category, channel) AS gid,
CASE GROUPING(city, category, channel)
WHEN 0 THEN '城市×品类×渠道'
WHEN 1 THEN '城市×品类'
WHEN 2 THEN '城市×渠道'
WHEN 3 THEN '城市'
WHEN 4 THEN '品类×渠道'
WHEN 5 THEN '品类'
WHEN 6 THEN '渠道'
WHEN 7 THEN '全表总计'
END AS 层级名称
FROM sales
GROUP BY CUBE (city, category, channel)
ORDER BY gid; gid | 层级名称
-----+--------------
0 | 城市×品类×渠道
1 | 城市×品类
2 | 城市×渠道
3 | 城市
4 | 品类×渠道
5 | 品类
6 | 渠道
7 | 全表总计步骤 4:GROUPING() + FILTER 的完整可运行报表 SQL
-- 可直接交付的月度渠道报表:含总计行、手机占比、环比基数
SELECT
CASE WHEN GROUPING(channel) = 1 THEN '【全部渠道】' ELSE channel END AS 渠道,
count(*) AS 订单总数,
count(*) FILTER (WHERE category = '手机') AS 手机订单,
count(*) FILTER (WHERE category = '电脑') AS 电脑订单,
sum(amount) AS 销售额,
sum(amount) FILTER (WHERE city IS NULL) AS 未填写城市销售额,
round(100.0 * count(*) FILTER (WHERE category = '手机')
/ NULLIF(count(*), 0), 2) AS 手机订单占比,
GROUPING(channel) AS g_channel
FROM sales
GROUP BY ROLLUP (channel)
ORDER BY g_channel, channel; 渠道 | 订单总数 | 手机订单 | 电脑订单 | 销售额 | 未填写城市销售额 | 手机订单占比 | g_channel
------------+----------+----------+----------+------------+------------------+--------------+-----------
分销 | 9993 | 2503 | 2496 | 4953350.20 | 2140.50 | 25.05 | 0
线上 | 9995 | 2491 | 2501 | 4961233.11 | 1980.20 | 24.92 | 0
门店 | 10012 | 2506 | 2502 | 4974419.24 | 2261.10 | 25.03 | 0
【全部渠道】| 30000 | 7500 | 7499 |14889002.55 | 6381.80 | 25.00 | 1步骤 5:性能验证——GROUPING() 不增加开销
-- 不带 GROUPING()
EXPLAIN (ANALYZE, BUFFERS)
SELECT city, sum(amount)
FROM sales
GROUP BY ROLLUP (city, category); GroupAggregate (cost=0.29..2891.00 rows=32 width=...) (actual time=0.031..22.114 rows=32 loops=1)
Group Key: city, category
Group Key: city
Group Key: ()
Buffers: shared hit=214
Execution Time: 22.114 ms-- 带 GROUPING()
EXPLAIN (ANALYZE, BUFFERS)
SELECT city, category, GROUPING(city), GROUPING(category), GROUPING(city, category), sum(amount)
FROM sales
GROUP BY ROLLUP (city, category); GroupAggregate (cost=0.29..2891.00 rows=32 width=...) (actual time=0.032..22.201 rows=32 loops=1)
Group Key: city, category
Group Key: city
Group Key: ()
Buffers: shared hit=214
Execution Time: 22.201 ms两者计划节点、Buffers、耗时完全一致(差异在测量噪声范围内),证明 GROUPING() 是零开销的元数据读取。
注意事项
- PostgreSQL 没有
GROUPING_ID()函数,SELECT GROUPING_ID(a, b)会报function does not exist;用GROUPING(a, b)代替。 GROUPING()不能出现在WHERE里,会报aggregate functions are not allowed in WHERE:sql-- 错误! SELECT city, sum(amount) FROM sales WHERE GROUPING(city) = 1 GROUP BY ROLLUP (city); -- 正确:用 HAVING SELECT city, sum(amount) FROM sales GROUP BY ROLLUP (city) HAVING GROUPING(city) = 1;- 不要用
IS NULL代替GROUPING()判断汇总行,原始数据本身含NULL时会产生歧义;打标签时对真 NULL 单独coalesce。 GROUPING()的参数必须是当前查询的分组列,换别的列会报arguments to GROUPING must be grouping expressions:sql-- 错误!sold_at 不在 GROUP BY 中 SELECT GROUPING(sold_at), sum(amount) FROM sales GROUP BY ROLLUP (city);- 多参数位图的高低位顺序易记反:
GROUPING(a, b, c)中a是最高位,()全汇总时返回2^N - 1;三列时总计行是7而非1。 - 排序必须写
ORDER BY GROUPING(x), x,只写ORDER BY x NULLS LAST会把所有汇总行沉底,失去层级结构。 GROUPING()不能写在GROUP BY里,它读的正是分组结果本身,逻辑上循环依赖。- 位图整数只适合做层级去重与排序,不要拿它当业务数值参与运算。
GROUPING()与GROUPING SETS家族同版本引入(PostgreSQL 9.5+),且必须与GROUP BY GROUPING SETS/ROLLUP/CUBE搭配使用,普通GROUP BY中用它恒为0。
最佳实践
- 凡是用了
ROLLUP/CUBE/GROUPING SETS的查询,一律配GROUPING()——它是把「不可读的 NULL 汇总行」变成「可交付报表」的关键一步。 - 标签写法统一为
CASE WHEN GROUPING(x) = 1 THEN '【全部xx】' ELSE coalesce(x, '【未填写】') END,兼顾汇总与真空值两种情况。 - 需要多个层级标记时优先用多参数位图(
GROUPING(a, b, c)),比多个单列GROUPING()的OR组合更简洁、更易排序与过滤。 - 过滤汇总层用
HAVING GROUPING(...);若条件复杂或用到了别名,就包一层子查询在外层过滤。 - 排序固定模板:
ORDER BY GROUPING(a), a NULLS LAST, GROUPING(b), b ...,保证小计紧跟明细、总计在最后。 GROUPING()不产生性能开销,不要为了「省算力」而省略它——省略的代价是结果不可用。- 多位图整数在应用层建映射表(
0 → 城市×品类×渠道)而非在 SQL 里堆 8 个WHEN,便于维护。 - 改写 SQL 后用
EXPLAIN (ANALYZE, BUFFERS)复核:确认计划节点与Buffers与不带GROUPING()时一致,避免误以为它引入了额外扫描。 - 从其他数据库迁移时,把所有
GROUPING_ID(...)机械替换成GROUPING(...),语义一一对应。
下一节
继续学习:SELECT 写法优化