4.3 CTE、Grouping Sets 与查询验证:把分析 SQL 当作程序
一张由五层 CTE 生成的报表通过了语法检查,总额却与账本对不上,预言厅决定把分析 SQL 当程序审查。
五层 CTE 能把复杂查询拆清楚,也能把同一个粒度错误传到五层之后。分析 SQL 和普通程序一样,需要接口、测试、执行计划和发布前对账。
本课目标
- 用 CTE 划分语义阶段而非堆叠别名;
- 使用
GROUPING SETS、ROLLUP和CUBE; - 正确连接慢变维度;
- 用不变量、对照查询和执行计划验证结果与成本。
1. 每个 CTE 只有一个职责
WITH terminal_missions AS (
-- Grain: one row per terminal mission
SELECT mission_id, fortress_id, terminal_status, completed_at
FROM missions
WHERE terminal_status IN ('completed', 'failed')
),
fortress_daily AS (
-- Grain: one row per fortress per UTC date
SELECT
fortress_id,
CAST(completed_at AS DATE) AS completed_date,
COUNT(*) AS attempts,
SUM(CASE WHEN terminal_status = 'completed' THEN 1 ELSE 0 END) AS successes
FROM terminal_missions
GROUP BY fortress_id, CAST(completed_at AS DATE)
),
scored AS (
SELECT
*,
1.0 * successes / NULLIF(attempts, 0) AS completion_rate
FROM fortress_daily
)
SELECT * FROM scored;CTE 名称描述业务结果,注释声明粒度,字段显式列出。step1、temp2 和层层 SELECT * 会让 schema 变化悄悄传播。
2. CTE 是否物化取决于数据库
有些优化器内联 CTE,有些版本或选项会物化,有些对递归/多次引用采用不同策略。CTE 首先是查询结构,不应假设它天然提高或降低性能。
查看实际执行计划与扫描字节、shuffle、spill 和运行时间。若中间结果需要多次复用或质量检查,可显式物化为版本化模型;同时承担存储、新鲜度与更新成本。
3. GROUPING SETS 一次表达多个粒度
SELECT
mission_type,
fortress_id,
SUM(cost) AS total_cost,
GROUPING(mission_type) AS all_mission_types,
GROUPING(fortress_id) AS all_fortresses
FROM mission_facts
GROUP BY GROUPING SETS (
(mission_type, fortress_id),
(mission_type),
(fortress_id),
()
);ROLLUP(a,b) 通常生成层级组合 (a,b)、(a)、();CUBE(a,b) 生成所有组合。具体语法和 GROUPING_ID 支持依方言而异。
汇总行用 NULL 占位时,必须用 GROUPING() 区分“所有类别”与源数据真实 NULL。直接 COALESCE(col,'ALL') 会把两者混在一起。
4. 版本化维度需要 as-of join
维度表:
fortress_id, region, valid_from, valid_to事实应按事件发生时的版本连接:
SELECT f.mission_id, d.region
FROM mission_facts AS f
JOIN fortress_history AS d
ON f.fortress_id = d.fortress_id
AND f.occurred_at >= d.valid_from
AND f.occurred_at < COALESCE(d.valid_to, TIMESTAMP '9999-12-31 00:00:00');维表有效区间必须对同一 key 不重叠,否则一条事实匹配多个版本。用测试检查 overlap 和 gap,并明确 valid_to 是包含还是不包含;半开区间更易拼接。
5. 查询测试从小型反例开始
构造最小 fixtures:
- 一个任务无标签;
- 一个任务两个标签;
- 两条同分排名;
- 某天缺分区;
- 分母为零;
- 维度边界恰在
valid_to; - NULL key;
- 迟到事件跨日期回补。
对每个案例手算期望结果。大数据集“数字看起来合理”不是测试。
6. 不变量与对照查询
可验证:
聚合前后总成本守恒
成功数 ≤ 尝试数
完成率位于 [0,1]
输出业务键唯一
inner join 丢失数等于明确拒绝数
按分组求和等于总计(无重叠分组时)为关键指标写一个慢而直白的参考查询,与优化版在样本和近期分区做差分。两者共享同一错误口径时仍可能同时错,因此还要与指标契约和源系统对账。
7. 读执行计划而不是猜性能
关注:
- 全表扫描还是分区裁剪;
- join 算法与 build/probe 侧;
- 实际与估计行数差异;
- shuffle 数据量;
- 排序与窗口 spill;
- 重复扫描同一大表;
- 过滤是否下推;
- 数据倾斜 key。
WHERE DATE(timestamp_col)=... 可能阻止某些系统利用原始时间分区或索引。使用半开范围通常更明确:
WHERE occurred_at >= TIMESTAMP '2026-08-01 00:00:00'
AND occurred_at < TIMESTAMP '2026-09-01 00:00:00'是否真正裁剪仍需看具体平台计划。
8. 成本优化不能改变口径
预聚合、近似 distinct、抽样和物化视图都能降成本,却改变新鲜度、精度或可下钻能力。报告应说明:
- 数据截至时间;
- 是否近似及误差保证;
- 刷新频率;
- 使用了哪个模型版本;
- 哪些维度不能继续拆分。
不要为了扫描更少字节,悄悄把用户级 distinct 换成事件计数。
9. SQL 发布清单
- 每个 CTE 的粒度已注明;
- join 基数已验证;
- 分子、分母和 NULL 语义已写入契约;
- 时间范围使用一致时区与半开边界;
- 窗口排序与 frame 明确;
- fixtures 和不变量通过;
- 与参考指标完成 reconciliation;
- 执行计划和资源在预算内;
- 输出 owner、新鲜度和版本可追踪。
常见误区
- CTE 会自动物化或自动优化:行为依数据库和版本。
- ROLLUP 中的 NULL 都表示总计:源数据 NULL 需用
GROUPING区分。 - 维表按 key 等值连接即可:历史事实需要时间版本。
- 查询更快就算优化成功:指标口径和精度不能悄悄改变。
练习
- 把一个五层 CTE 的每层粒度写出来,找出第一次变化的位置。
- 用
GROUPING SETS同时计算地区、类型和总计,并区分真实 NULL。 - 构造两个重叠维度版本,观察 as-of join 如何复制事实。
- 为完成率查询写最小 fixtures、守恒量和参考实现。
小结
分析 SQL 是可部署的数据程序。CTE 划分语义步骤,grouping sets 表达多粒度,as-of join 维护历史语义;fixtures、不变量、对账与执行计划共同证明它既算对,也算得起。
下一章进入统计推断:从样本差异判断总体差异时,需要把抽样过程、不确定性和决策阈值一起写进结论。