# 没人动过代码，上季度的数字为什么变了

> Kimball 的《数据仓库工具箱》早已点明其中机制，dbt snapshots 至今仍在实现他的 Type 2 方案。

原文: https://fdetimes.net/zh/books-courses/kimball-data-warehouse-toolkit-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 多维数据集都必须重新计算。

看看那个仪表盘背后的查询：

```sql
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，接下来的每个问题都会被托付给他。

**本周动手试试:**

- 在当前项目中选一张事实表，用“每行是……”开头的一句话写下它的粒度，发给业务方确认。
- 列出客户维度表中的所有属性，标出那些正在被覆盖（Type 1）、但历史报表又需要旧值的列。
- 为一张小型源表建一个 dbt snapshot，修改其中一个值，检查它生成的新行和有效期列。

## 信源

- [The Data Warehouse Toolkit, 3rd Edition (Kimball Group)](https://www.kimballgroup.com/data-warehouse-business-intelligence-resources/books/data-warehouse-dw-toolkit/)

- [Grain | Kimball Dimensional Modeling Techniques](https://www.kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/grain/)

- [Type 1: Overwrite | Kimball Dimensional Modeling Techniques](https://www.kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/type-1/)

- [Type 2: Add New Row | Kimball Dimensional Modeling Techniques](https://www.kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/type-2/)

- [Conformed Dimensions | Kimball Dimensional Modeling Techniques](https://www.kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/conformed-dimension/)

- [Slowly Changing Dimensions, Part 2](https://www.kimballgroup.com/2008/09/slowly-changing-dimensions-part-2/)

- [Add snapshots to your DAG | dbt Developer Hub](https://docs.getdbt.com/docs/build/snapshots)
