SCD 类型0:保留原始值——最“懒”也最稳的策略

说实话,SCD 类型0 是所有缓慢变化维策略里最没“技术含量”的一个。但你别小看它。我在好几个项目里都见过有人把不该变的字段给覆盖了,结果数据对不上,业务方追着问“为什么用户注册日期变了?”——那场面,挺尴尬的。

类型0 的核心就一句话:写入后永不修改。不管源系统怎么变,不管业务状态怎么更新,这个字段的值一旦落进数仓,就永远定格在那一刻。

类型0 定义:维度属性在首次加载后,无论源系统发生任何变化,目标表都保持原始值不变。相当于给数据拍了一张“出生照”。

实现方式:简单到不需要动脑子

实现类型0 的方式,说白了就是“不处理”。你只需要在 ETL 里写一条规则:首次加载写入,后续更新忽略该字段。

我习惯用两种方式来实现:

方式一:全量快照 + 固定字段

每次全量拉取时,只对非类型0 字段做覆盖,类型0 字段保持首次值。举个例子:

-- 假设用户维度表,birth_date 和 reg_time 是类型0 字段
-- 首次加载
INSERT INTO dim_user (user_id, user_name, birth_date, reg_time, city)
SELECT user_id, user_name, birth_date, reg_time, city
FROM source_user;

-- 后续更新:只更新非类型0 字段
UPDATE dim_user t
SET t.user_name = s.user_name,
    t.city = s.city
FROM source_user s
WHERE t.user_id = s.user_id;
-- birth_date 和 reg_time 不动

方式二:增量合并 + 条件判断

用 MERGE 语句时,对类型0 字段做“仅插入不更新”处理:

MERGE INTO dim_user t
USING source_user s
ON t.user_id = s.user_id
WHEN MATCHED THEN
  UPDATE SET
    t.user_name = s.user_name,
    t.city = s.city
  -- 注意:birth_date 和 reg_time 不在 UPDATE 子句中
WHEN NOT MATCHED THEN
  INSERT (user_id, user_name, birth_date, reg_time, city)
  VALUES (s.user_id, s.user_name, s.birth_date, s.reg_time, s.city);

我的小习惯:在建模文档里,我会给类型0 字段加一个特殊标记,比如字段注释里写上 【SCD0】。这样后来接手的人一眼就能看出来——这个字段不能动。

适用场景:哪些字段该“冻住”?

你想想看,什么样的数据一旦产生就不该再变了?我总结了三个典型场景:

场景 典型字段 为什么用类型0
生物特征 出生日期、性别(生理)、血型 这些是客观事实,不会因为业务操作而改变
注册/创建时间 注册时间、首次下单时间、账号创建时间 时间戳一旦记录,回溯历史时不能篡改
业务主键 身份证号、手机号(作为业务标识时) 主键变了意味着实体变了,不是同一个对象了
历史快照标识 首次渠道来源、首次推荐人 分析“首次”行为时,这些值必须固定

我在做电商数仓时遇到过这样一个需求:运营要分析“用户注册当年的购买转化率”。如果注册时间被后续更新覆盖了,那这个分析就全乱了。所以注册时间我直接标了类型0,谁也别想动它。

优缺点分析:稳,但死板

类型0 的好处很明显,坏处也很明显。咱们摊开来说:

优点

  • 实现成本极低:不需要拉链表,不需要生效时间,不需要版本号。写几行 SQL 就搞定。
  • 查询性能最优:没有历史版本,没有分区膨胀,一张表搞定所有。查询时不需要考虑“取哪个版本”。
  • 数据一致性最强:永远不会出现“同一个用户两个出生日期”的怪事。审计时也最干净。
  • ETL 逻辑最简:不需要判断变化,不需要生成新版本,出错概率最低。

缺点

  • 丢失历史变化:如果某个字段其实会变(比如用户改了手机号),类型0 就记录不了这个变化。
  • 与源系统脱节:源系统更新了,数仓不跟着变,时间长了两边数据对不上。
  • 适用范围窄:真正“永不变化”的字段其实很少。大部分业务属性都会变。
  • 容易误用:我见过有人把“用户等级”也设成类型0,结果分析时发现用户等级永远是最初的——那分析还有什么意义?

我曾经踩过的坑:有一次我把用户的“所在城市”设成了类型0,想着“用户注册时填的城市应该不变吧”。结果半年后业务方要做地域分析,发现大量用户已经搬家了,但数仓里还是老城市。最后不得不重刷历史数据,折腾了两天。所以——类型0 只适用于真正不会变的属性,别凭感觉拍脑袋。

一张图看懂类型0

下面这张图展示了类型0 的核心逻辑:数据一旦写入,就永远定格。源系统后续的变化,跟数仓没关系了。

SCD 类型0:保留原始值 源系统 T1: 注册时 城市=北京 T2: 搬家后 城市=上海 T3: 再次搬家 城市=广州 数仓维度表 用户维度表(类型0 字段:城市) 城市 = 北京(永远定格在 T1 的值) ✘ T2、T3 的更新被忽略 无论源系统怎么变,数仓里始终保留首次加载的值 适用于出生日期、注册时间等“一次定终身”的字段

什么时候该用类型0?

我给自己定了个判断标准,分享给你参考:

  1. 这个字段在业务上有没有“变”的合理性?——出生日期变了?不可能。手机号变了?有可能。先问自己这个问题。
  2. 如果变了,分析场景是否需要追溯历史?——比如“注册时的城市”和“当前的城市”是两个不同的分析维度。前者用类型0,后者用类型1或类型2。
  3. 这个字段是不是业务主键的一部分?——如果是,那必须类型0。主键变了就是另一个实体了。
  4. 有没有监管或审计要求?——金融行业里,交易时间、客户首次登记信息等,监管要求不得修改。这种场景类型0 是唯一选择。

我的建议:建模初期,把所有字段过一遍,把明显不会变的先标成类型0。剩下的再按业务需求选类型1、类型2 或类型3。这样能减少后续的维护成本。但记住——宁可少标,不要多标。标错了类型0,后面改起来很麻烦。