2. 数据仓库中的时间维度:缓慢变化维度(SCD)类型、有效时间与事务时间
聊 Point-in-Time Join 之前,咱们得先把时间维度这件事掰扯清楚。我见过不少团队,PIT Join 写得挺溜,结果数据一跑出来全是错的——问题根源往往不在 Join 语法上,而在对时间语义的理解出了偏差。
说白了,数据仓库里的时间不是只有一种。你想想看,一个客户今天改了地址,明天又改回来,那这条记录到底哪个时间才是“对的”?这就引出了我们今天要聊的两个核心概念:有效时间和事务时间,以及它们背后的实现机制——缓慢变化维度。
2.1 缓慢变化维度(SCD)的三种主流类型
SCD 这个概念,最早是 Kimball 老爷子提出来的。我刚开始做数仓那会儿,觉得这东西太理论了,直到有一次上线后业务方跑来质问:“为什么上个月报表里的客户地址和这个月不一样?”——嗯,从那以后我再也不敢小看 SCD 了。
SCD 的核心问题很简单:维度属性变了,历史数据怎么办?
常见的做法有三种:
| 类型 | 处理方式 | 典型场景 |
|---|---|---|
| SCD Type 1 | 直接覆盖,不保留历史 | 客户邮箱、联系电话(不重要属性) |
| SCD Type 2 | 新增记录,保留完整历史 | 客户地址、产品分类(重要属性) |
| SCD Type 3 | 增加字段,保留有限历史 | 客户等级变化(仅需保留上一次) |
Type 1 最简单,但也最危险。我见过有人把客户主地址也做成 Type 1,结果审计的时候发现上个月的报表和这个月对不上,查了半天才发现是地址被覆盖了。Type 1 适合那些“错了就错了,不需要追溯”的属性。
Type 2 是 PIT Join 最常打交道的类型。每次属性变化,就插入一条新记录,同时带上生效时间和失效时间。这样你就能回答“某年某月某日,这个客户的信息是什么”。
Type 3 用得少一些,但某些场景很实用。比如客户等级,你只需要知道“当前等级”和“上一个等级”,Type 3 加两个字段就够了,不用像 Type 2 那样整行复制。
核心原则:PIT Join 主要解决的是 Type 2 SCD 与事实表之间的关联问题。如果你的维度全是 Type 1,那根本不需要 PIT Join——直接关联主键就行。
2.2 有效时间 vs 事务时间:两个容易搞混的概念
这两个概念,我当年花了整整一个项目周期才真正搞明白。先给个定义:
- 有效时间(Valid Time):现实世界中,这个事实“真正发生”的时间。
- 事务时间(Transaction Time):数据被记录到数据库中的时间。
举个例子你就懂了。假设今天是 2024-03-15,你收到一份销售单据,上面写着“2024-03-10 成交”。那么:
- 有效时间 = 2024-03-10(实际成交日)
- 事务时间 = 2024-03-15(录入系统的日期)
为什么这个区分这么重要?因为 PIT Join 里,你选择用哪个时间做关联,结果可能完全不同。
注意:很多新手在做 PIT Join 时,默认用事务时间关联维度。这在大部分场景下是对的,但如果你遇到“补录数据”或“延迟上报”的情况,事务时间关联会导致维度快照取错。我踩过这个坑,补录的数据关联到了错误的维度版本,整整花了三天排查。
2.3 时间维度的建模方式
实际项目中,时间维度的建模通常有两种思路:
方式一:在维度表中直接加时间字段
-- Type 2 SCD 维度表示例
CREATE TABLE dim_customer (
customer_sk BIGINT PRIMARY KEY, -- 代理键
customer_id VARCHAR(20), -- 业务主键
customer_name VARCHAR(100),
customer_address VARCHAR(200),
valid_from DATE, -- 有效开始时间
valid_to DATE, -- 有效结束时间(NULL 表示当前有效)
is_current BOOLEAN, -- 是否当前版本
created_at TIMESTAMP, -- 事务时间(记录创建时间)
updated_at TIMESTAMP -- 事务时间(记录更新时间)
);
方式二:单独维护时间维度表
有些团队喜欢把时间单独抽出来,做成一张时间维度表。这样做的好处是统一管理,但会增加 Join 的复杂度。
我的建议:对于大多数场景,直接在维度表中维护 valid_from / valid_to 就够了。单独抽时间维度表,除非你的业务时间逻辑极其复杂(比如多个时区、多个日历体系),否则没必要增加复杂度。
2.4 时间维度与 PIT Join 的关系
到这里,你应该能感受到:PIT Join 本质上就是在解决“有效时间”和“事务时间”之间的映射问题。
事实表里记录的是业务事件的发生时间(有效时间),而维度表里记录的是属性变化的时间区间。PIT Join 要做的,就是把事实表的有效时间,映射到维度表对应的那个版本上。
我画了一张图,帮你理清这几个概念之间的关系:
2.5 实际项目中的避坑指南
最后,分享几个我在项目中踩过的坑:
- 坑一:有效时间用 DATE 类型,事务时间用 TIMESTAMP。 别混用。我见过有人把有效时间也存成 TIMESTAMP,结果关联的时候精度对不上,白白浪费半天排查。
- 坑二:Type 2 SCD 的 valid_to 不要用 9999-12-31。 用 NULL 表示“当前有效”更安全。9999-12-31 在跨年计算时容易出 bug。
- 坑三:事务时间不要作为 PIT Join 的关联条件。 除非你明确知道自己在做什么。事务时间只用于数据加载顺序的控制,不是业务语义的一部分。
一个小技巧:在做 PIT Join 之前,先检查维度表的 Type 2 版本是否完整。我习惯写一个 SQL 检查每个业务主键的 valid_from / valid_to 是否有重叠或断层。这一步能避免 80% 的 PIT Join 问题。