这道题来自京东商业分析部的数据分析师面试。每周一的经营分析会上,品类运营和区域经理各需要一份报表——品类运营想看"数码在全国各区域分别卖了多少,合计多少",区域经理想看"华北区各品类分别卖了多少,合计多少"。
如果写两条 SQL 分别出报表,不仅麻烦而且数据可能不一致。有没有一条 SQL 同时满足两个视角?这就是多维聚合(Multi-Dimensional Aggregation)要解决的问题。
多维分析场景
品类运营视角: 区域经理视角:
数码 华北
├── 华北 7000 ├── 数码 7000
├── 华东 3000 ├── 家电 8000
├── 华南 3500 ├── 服饰 1000
└── 合计 13500 └── 合计 16000
一条 SQL 同时出两张表的数据 → GROUPING SETS 或 CUBE。
现有一张订单明细表 t5_jd_order_detail,记录了每笔订单的品类、地区和销售额。请统计:
t5_jd_order_detail 表
+-----------+-----------+---------+--------+
| order_id | category | region | sales |
+-----------+-----------+---------+--------+
| 1 | 数码 | 华北 | 5000 |
| 2 | 数码 | 华东 | 3000 |
| 3 | 数码 | 华北 | 2000 |
| 4 | 家电 | 华北 | 8000 |
| 5 | 家电 | 华东 | 6000 |
| 6 | 家电 | 华南 | 4000 |
| 7 | 服饰 | 华东 | 1500 |
| 8 | 服饰 | 华南 | 2500 |
| 9 | 服饰 | 华北 | 1000 |
| 10 | 数码 | 华南 | 3500 |
+-----------+-----------+---------+--------+
10 rows selected (0.911 seconds)(dwsql.com)
本题核心是选择正确的多维聚合语法。先把三个选项理清:
ROLLUP(a, b):
层级上卷 — (a,b) → (a) → ()
✅ 有品类小计
❌ 没有地区小计
CUBE(a, b):
全交叉 — (a,b) → (a) → (b) → ()
✅ 有品类小计
✅ 有地区小计
⚠️ 生成所有组合,数据量大时慢
GROUPING SETS:
精确指定 — 只生成你要的组合
✅ 灵活
✅ 性能最好(只算需要的)
本题需要同时有品类小计和地区小计,所以 ROLLUP 不满足,需要 CUBE 或 GROUPING SETS。
维度 | 评分 |
|---|---|
题目难度 | ⭐️⭐️⭐️ |
题目清晰度 | ⭐️⭐️⭐️⭐️⭐️ |
业务常见度 | ⭐️⭐️⭐️⭐️ |
SELECT
COALESCE(category, '全部品类') AS category,
COALESCE(region, '全部地区') AS region,
SUM(sales) AS gmv
FROM t5_jd_order_detail
GROUP BY CUBE(category, region)
ORDER BY
category NULLS LAST,
region NULLS LAST;
执行结果(完整 16 行):
+-----------+---------+--------+
| category | region | gmv |
+-----------+---------+--------+
| 全部品类 | 全部地区 | 36500 |
| 全部品类 | 华东 | 10500 |
| 全部品类 | 华北 | 16000 |
| 全部品类 | 华南 | 10000 |
| 家电 | 全部地区 | 18000 |
| 家电 | 华东 | 6000 |
| 家电 | 华北 | 8000 |
| 家电 | 华南 | 4000 |
| 数码 | 全部地区 | 13500 |
| 数码 | 华东 | 3000 |
| 数码 | 华北 | 7000 |
| 数码 | 华南 | 3500 |
| 服饰 | 全部地区 | 5000 |
| 服饰 | 华东 | 1500 |
| 服饰 | 华北 | 1000 |
| 服饰 | 华南 | 2500 |
+-----------+---------+--------+
16 rows selected (9.624 seconds)(dwsql.com)
CUBE 虽然简洁,但生成了所有 2³=8 种维度组合(含重复)。当维度多时,CUBE 的计算量指数增长。GROUPING SETS 只算你指定的组合,性能更好。
SELECT
COALESCE(category, '全部品类') AS category,
COALESCE(region, '全部地区') AS region,
SUM(sales) AS gmv
FROM t5_jd_order_detail
GROUP BY category, region
GROUPING SETS (
(category, region), -- 明细
(category), -- 品类小计
(region), -- 地区小计
() -- 总计
)
ORDER BY
category asc NULLS LAST,
region asc NULLS LAST;
结果与 CUBE 完全一致,但执行计划更高效——只扫描 4 次而非 8 次。
⚠️ NULL vs '全部品类' 聚合行中,不参与分组的列会被置为 NULL。一定要用 COALESCE 替换,否则报表中全是 NULL,完全看不懂。
-- ❌ 不处理 NULL,报表不可读
SELECT category, region, SUM(sales)
FROM t5_jd_order_detail GROUP BY CUBE(category, region);
-- ✅ COALESCE 替换
SELECT COALESCE(category, '全部品类'), COALESCE(region, '全部地区'), SUM(sales)
我们上面执行 添加 ORDER BY category asc NULLS LAST,region asc NULLS LAST语句,是希望能将小计都放到最后,方便查看。但是实际执行效果是,汇总行排在最前面,而不是最后。这里涉及到了SQL语句的执行顺序,FROM → WHERE → GROUP BY (含 CUBE) → HAVING → SELECT → ORDER BY 因为是先 GROUP BY 后 ORDER BY,在执行完成之后,select 的别名已经替换为实际值,所以排序是根据实际值进行的,这应该属于是新的一列了。而不是原来的列。 验证语句如下
SELECT
COALESCE(category, '全部品类') AS category1,
COALESCE(region, '全部地区') AS region1,
SUM(sales) AS gmv
FROM t5_jd_order_detail t
GROUP BY CUBE(category, region)
ORDER BY
category,
region;
该语句会提示,没有category和region这两个列;
想要实现真正的排序,需要增加排序列,这里可以使用grouping函数。
SELECT
COALESCE(cat, '全部品类') AS category,
COALESCE(reg, '全部地区') AS region,
gmv
FROM (
SELECT
category AS cat,
region AS reg,
SUM(sales) AS gmv,
GROUPING(category) AS cat_grp,
GROUPING(region) AS reg_grp
FROM t5_jd_order_detail
GROUP BY CUBE(category, region)
) t
ORDER BY
cat_grp,
cat,
reg_grp,
reg;
执行结果
+-----------+---------+--------+
| category | region | gmv |
+-----------+---------+--------+
| 家电 | 华东 | 6000 |
| 家电 | 华北 | 8000 |
| 家电 | 华南 | 4000 |
| 家电 | 全部地区 | 18000 |
| 数码 | 华东 | 3000 |
| 数码 | 华北 | 7000 |
| 数码 | 华南 | 3500 |
| 数码 | 全部地区 | 13500 |
| 服饰 | 华东 | 1500 |
| 服饰 | 华北 | 1000 |
| 服饰 | 华南 | 2500 |
| 服饰 | 全部地区 | 5000 |
| 全部品类 | 华东 | 10500 |
| 全部品类 | 华北 | 16000 |
| 全部品类 | 华南 | 10000 |
| 全部品类 | 全部地区 | 36500 |
+-----------+---------+--------+
16 rows selected (0.441 seconds)
month 维度,GROUPING SETS((category, region, month), (category, month), (region, month), (month)) 可以看各月趋势和各月小计GROUPING(category) ——返回 1 表示该行是 category 维度的聚合行SUM(CASE WHEN region='华北' THEN sales END) 的方式手动做行列转换💡 选择指南
需求 | 用哪个 |
|---|---|
只需要层级汇总(品类→总计) | ROLLUP |
需要全部交叉组合(维度少) | CUBE |
需要部分组合(维度多/性能敏感) | GROUPING SETS |
不确定 → 默认选 | GROUPING SETS |
考点 | 说明 |
|---|---|
ROLLUP / CUBE / GROUPING SETS | 多维聚合的三种语法,越往后越灵活 |
COALESCE | 将聚合行的 NULL 替换为可读标签 |
NULLS LAST | 汇总行排在明细行之后 |
GROUPING SETS 性能 | 只算指定组合,避免 CUBE 的计算爆炸 |
CREATE TABLE IF NOT EXISTS t5_jd_order_detail (
order_id INT,
category STRING,
region STRING,
sales BIGINT
);
INSERT INTO t5_jd_order_detail VALUES
(1, '数码', '华北', 5000),
(2, '数码', '华东', 3000),
(3, '数码', '华北', 2000),
(4, '家电', '华北', 8000),
(5, '家电', '华东', 6000),
(6, '家电', '华南', 4000),
(7, '服饰', '华东', 1500),
(8, '服饰', '华南', 2500),
(9, '服饰', '华北', 1000),
(10, '数码', '华南', 3500);