2. SCD 类型 0
聊完 SCD 的整体概念,咱们先来看最简单的一种类型——SCD 类型 0。
说白了,类型 0 就是「不变化」。一旦某个维度属性被写入数据仓库,它就永远定格在那里。不管业务系统怎么改,不管用户怎么投诉数据不对,我们都不动它。
我第一次接触这个策略时,心里也犯嘀咕:这不就是偷懒吗?后来在金融项目中才明白,有些数据真的不能改。
2.1 什么是 SCD 类型 0
类型 0 的核心思想是:保留原始值,不做任何变更。
当源系统的维度属性发生变化时,目标表中的对应记录保持原样。新记录按新值写入,旧记录纹丝不动。
一句话总结:写入即冻结,永不更新。
你可能会问:那数据不一致怎么办?嗯,这正是类型 0 的取舍——它牺牲了「与源系统实时一致」,换来了「历史事实的稳定」。
2.2 适用场景
类型 0 不是万金油,它只适合特定场景。我根据自己的项目经验,整理了下面几类典型情况:
| 场景 | 说明 | 举例 |
|---|---|---|
| 交易快照属性 | 记录交易发生时的属性值,后续变化不影响历史 | 订单下单时的商品价格、税率 |
| 不可变标识 | 天然不会变化的属性 | 身份证号、出生日期、注册时间 |
| 审计与合规 | 监管要求保留原始记录,禁止修改 | 银行交易流水中的客户风险等级 |
| 事实参照锚点 | 用于关联历史事实的固定参照 | 销售订单中的客户所属区域(按下单时为准) |
我在做电商数仓时,遇到过这样一个坑:商品类目经常调整,但财务结算必须按下单时的类目来算佣金。如果用了类型 1 或类型 2,历史订单的类目会跟着变,财务数据就全乱了。后来我们果断把订单维度的类目字段设为类型 0,问题迎刃而解。
我的建议:如果你不确定某个字段该不该用类型 0,就问自己一个问题——「这个属性如果变了,历史事实还成立吗?」如果答案是否定的,那就用类型 0。
2.3 实现方式
类型 0 的实现非常简单,甚至可以说「没有实现」。你只需要在 ETL 过程中,对目标字段做一次写入,后续不再更新即可。
2.3.1 SQL 实现
假设我们有一张客户维度表 dim_customer,其中 customer_id 是业务主键。我们要保留客户首次注册时的 register_channel(注册渠道)和 register_date(注册日期)。
首次加载:
INSERT INTO dim_customer (customer_id, customer_name, register_channel, register_date, city, level)
SELECT
s.customer_id,
s.customer_name,
s.register_channel,
s.register_date,
s.city,
s.level
FROM source_customer s
WHERE NOT EXISTS (
SELECT 1 FROM dim_customer t
WHERE t.customer_id = s.customer_id
);
增量更新(仅更新非类型 0 字段):
UPDATE dim_customer t
SET
t.customer_name = s.customer_name,
t.city = s.city,
t.level = s.level
FROM source_customer s
WHERE t.customer_id = s.customer_id
-- 注意:register_channel 和 register_date 不在 SET 子句中
AND (t.customer_name <> s.customer_name
OR t.city <> s.city
OR t.level <> s.level);
看到了吗?register_channel 和 register_date 根本没有出现在 UPDATE 语句里。这就是类型 0 的精髓——不更新就是最好的更新。
2.3.2 ETL 工具实现
如果你用 DataStage、Informatica 或 Kettle 这类工具,做法更简单:
- 在映射阶段,将类型 0 字段的「更新策略」设为 Insert Only
- 或者直接把这些字段从更新流程中排除
我曾经见过一个团队,在 Informatica 里把所有字段都设成了「Update as Update」,结果类型 0 字段也被覆盖了。上线后财务对账对不上,查了两天才找到原因。嗯,这种低级错误,犯一次就记住了。
2.4 类型 0 的优缺点
| 维度 | 优点 | 缺点 |
|---|---|---|
| 实现成本 | 极低,几乎零开发量 | — |
| 查询性能 | 最优,无冗余数据 | — |
| 历史准确性 | 完美保留历史快照 | 与源系统当前值可能不一致 |
| 存储开销 | 最小,不产生新记录 | — |
| 灵活性 | — | 一旦写入无法修正(除非重建) |
我曾经踩过的坑:有次把「客户手机号」设成了类型 0,结果客户换号后,运营部门用旧号码发营销短信,全发到别人那去了。后来我们加了个「当前手机号」字段用类型 1 处理,原始手机号保留在「注册手机号」字段里用类型 0。所以记住:类型 0 只适合「历史参照」属性,不适合「业务操作」属性。
2.5 核心逻辑流程图
下面这张图展示了类型 0 的处理逻辑。我特意把「判断是否已存在」和「是否更新类型 0 字段」两个关键节点标了出来。
2.6 什么时候不该用类型 0
说了这么多适用场景,也得聊聊反例。我个人觉得,下面这几种情况千万别用类型 0:
- 业务上需要追踪变化的属性——比如客户等级、会员状态,这些用类型 2 更合适
- 数据质量有问题,需要事后修正——类型 0 一旦写入就改不了,除非删表重跑
- 源系统属性本身是动态计算的——比如「最近一次登录时间」,这种字段用类型 0 就失去意义了
一个小技巧:我在设计维度表时,习惯把字段分为三类:固定锚点(类型 0)、当前值(类型 1)、历史轨迹(类型 2)。这样设计文档一目了然,开发同学也不会搞混。