没人动过代码,上季度的数字为什么变了
Kimball 的《数据仓库工具箱》早已点明其中机制,dbt snapshots 至今仍在实现他的 Type 2 方案。

要点速览
- Ralph Kimball 与 Margy Ross 合著的这本书(Wiley,2013 年)是维度建模的标准参考书,可以直接用在今天的 dbt 技术栈上。
- SCD Type 1 覆盖旧值,历史随之消失;Type 2 新增一行并分配独立的代理键,每条事实都指向事件发生时有效的那个版本。
- 值得带走的三点:先定粒度,有意识地为每个属性选择 SCD 类型,一致性维度只定义一次、所有人共用。
设想一个周一早上。客户的销售负责人打开仪表盘,发现北区第一季度的收入比上周少了。没人改过数据管道,也没人删过订单。只是一个大客户把总部迁到了南区,系统就悄无声息地改写了过去。
如果你是负责这份数据的前线部署工程师(FDE),解释原因的人就是你。
这正是《数据仓库工具箱(第 3 版)》(The Data Warehouse Toolkit, 3rd Edition)以及 Kimball Group 发布的维度建模技术页面至今仍值得一读的原因。它们点明了让旧数字发生变化的机制,也说明了如何设计表结构,让这种变化只在你有意为之时才发生。
一本 2013 年的书,仍在描述今天的技术栈
Ralph Kimball 和 Margy Ross 合著了这本维度建模经典指南的第三版。维度建模是 Kimball 开创的方法,该书由 Wiley 于 2013 年出版。
书虽然不新,但 dbt 的官方文档至今仍把 snapshots 描述为在可能变化的源表上实现 SCD Type 2 的方式。Type 2 是 Kimball 提出的概念,dbt snapshots 只是把这一思路落成代码的一种方式。
所以,读 Kimball 让你明白 snapshots 解决的是什么问题,而不只是怎么配置。这些资料培养的 FDE 能力,是把模糊的业务问题转化为可以验证的数据设计。其中有三个思路,放到任何项目里都用得上。
画表之前,先定粒度
Kimball Group 的技术页面把粒度定义为事实表中一行数据代表什么。这是必须最先敲定的决策。听起来简单,但许多关于数字的争执正是从这里开始的。
比较“每行是一笔订单”和“每行是订单中的一件商品”。如果有人按行计数,两种粒度算出的“订单数”并不相同。在第二种粒度下,一笔包含三件商品的订单会被计为三次。
因此,到了客户现场,第一件事不是打开 SQL 编辑器,而是用业务语言把粒度写成一句话,请客户方的负责人确认。
覆盖旧值是一个决策,不是默认选项
回到周一早上的问题。在 SCD Type 1 下,新值直接替换维度行中的旧值。Kimball Group 的技术页面明确指出,这会抹掉历史。同一页面还提醒,所有受影响的聚合事实表和 OLAP 多维数据集都必须重新计算。
看看那个仪表盘背后的查询:
SELECT c.region, SUM(f.amount) AS revenue
FROM fact_orders f
JOIN dim_customer c ON f.customer_key = c.customer_key
WHERE f.order_date BETWEEN '2025-01-01' AND '2025-03-31'
GROUP BY c.region;
(revenue 即“收入”;下表中的“北区”“南区”分别对应 North 和 South。)
假设上周北区的数字是 1,000,其中 300 来自客户 KH-07。执行一条 Type 1 语句,例如 UPDATE dim_customer SET region = 'South' 之后,同一个查询会让北区变成 700,那 300 挪到了南区。如果按区域汇总的聚合表没有重算,它仍显示 1,000,两个仪表盘就对不上了。
Type 2 的做法不同。它在维度表中新增一行,写入更新后的值,并分配一个新的代理键,事实表随后用这个键作为外键。Kimball Group 建议至少增加三列:生效日期、失效日期和当前行标志。示例中的客户维度表如下:
| customer_key | customer_id | region | 生效日期 | 失效日期 | 当前 |
|---|---|---|---|---|---|
| 101 | KH-07 | 北区 | 2024-01-01 | 2025-06-30 | N |
| 245 | KH-07 | 南区 | 2025-07-01 | 9999-12-31 | Y |
再跑一遍同样的 SQL。第一季度的订单仍然关联到键 101,北区依旧是 1,000。7 月以后的订单则指向键 245。
Kimball 在 2008 年的一篇文章里拿自己举例:按照 Type 2,应当为 Ralph Kimball 新建一条员工记录,自 2008 年 7 月 18 日起生效,而不是修改旧记录。
并非每一列都需要 Type 2。修正拼错的客户名称时,覆盖旧值完全合理。你要做的是逐个过一遍属性,问客户:过去的报表应该显示当时的值,还是现在的值?
定义一次,处处复用
如果销售和财务各自维护一张“客户”表,双方的仪表盘很少能对得上。Kimball Group 给出的答案是一致性维度:与公司负责数据治理的人一起定义一次,然后在各个事实表之间复用。数字因此保持一致,后续开发成本也更低。
判断两个维度是否一致,有明确的标准:它们的属性必须使用相同的列名和相同的取值域。一张表写“North”、另一张写“N”,就不算一致。
按问题读,不必从头读到尾
可以试试这样一条路径:打开 Kimball Group 的 Dimensional Modeling Techniques 页面,依次阅读 Grain、Type 1、Type 2 和 Conformed Dimensions 这几节。之后遇到实际案例,再用书深入对应的主题。最后打开 dbt snapshots 文档,把理论和代码对上。
这些资料适合 SQL 写得不错、却从未对高管据以决策的数字负过责的开发者。
简历上写一句“用 dbt snapshots 将客户维度改为 SCD Type 2,客户更换区域时历史报表保持不变”,远比“精通 SQL”有分量。
客户很少问你用的是 Type 1 还是 Type 2,他们问的是:为什么昨天的数字和今天不一样?能在五分钟内讲清楚这一点的 FDE,接下来的每个问题都会被托付给他。
这篇文章对你有帮助吗?
感谢你的反馈!
7 个信源
- The Data Warehouse Toolkit, 3rd Edition (Kimball Group)
- Grain | Kimball Dimensional Modeling Techniques
- Type 1: Overwrite | Kimball Dimensional Modeling Techniques
- Type 2: Add New Row | Kimball Dimensional Modeling Techniques
- Conformed Dimensions | Kimball Dimensional Modeling Techniques
- Slowly Changing Dimensions, Part 2 · 2008-09-22
- Add snapshots to your DAG | dbt Developer Hub