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 字段」两个关键节点标了出来。

SCD 类型 0 处理流程 读取源系统记录 记录是否 已存在? 否(新记录) 插入完整新记录 是(已有记录) 仅更新非类型 0 字段 写入目标表 关键说明: • 类型 0 字段在首次插入后永不更新 • 新记录写入所有字段(包括类型 0 字段) • 已有记录只更新非类型 0 字段,类型 0 字段保持不变

2.6 什么时候不该用类型 0

说了这么多适用场景,也得聊聊反例。我个人觉得,下面这几种情况千万别用类型 0:

  • 业务上需要追踪变化的属性——比如客户等级、会员状态,这些用类型 2 更合适
  • 数据质量有问题,需要事后修正——类型 0 一旦写入就改不了,除非删表重跑
  • 源系统属性本身是动态计算的——比如「最近一次登录时间」,这种字段用类型 0 就失去意义了

一个小技巧:我在设计维度表时,习惯把字段分为三类:固定锚点(类型 0)、当前值(类型 1)、历史轨迹(类型 2)。这样设计文档一目了然,开发同学也不会搞混。