3. SCD 类型 1

SCD 类型 1,说白了就是「用新值直接覆盖旧值」。不保留历史,不记录变化轨迹。你查到的永远是当前最新状态。

我刚开始做数据仓库那会儿,觉得这玩意儿太简单了,不就是个 UPDATE 嘛。后来踩过坑才明白——简单归简单,用不对地方照样翻车。

3.1 什么是类型 1

类型 1 的核心逻辑就一句话:维度属性发生变化时,直接更新原记录。

举个例子。客户「张三」改了手机号。在类型 1 的处理方式下,维度表里张三那条记录的 phone 字段直接从旧号变成新号。之前是什么?没人记得了。

核心特征:

  • 不保留历史版本
  • 主键不变,属性覆盖
  • 查询性能最优
  • 历史分析能力为零

你想想看,这种策略适合什么场景?适合那些「历史值没有分析价值」的属性。比如客户的邮箱、联系电话、家庭地址——业务方只关心当前能不能联系上你,谁在乎你三年前用哪个邮箱?

3.2 适用场景

我个人习惯把适用场景分成三类:

场景类别 典型属性 为什么用类型 1
纠错类 录入错误、拼写修正 错误值没有保留意义
时效类 联系电话、电子邮箱 业务只关心当前值
冗余类 描述性文本、备注 历史版本无分析价值

我在项目中遇到过一种情况:客户维度表里有个「客户等级」字段,业务方说「等级变了就覆盖吧,反正我们只看当前等级做营销」。结果三个月后,分析团队跑过来问「能不能统计一下客户等级的变化趋势?」——嗯,这就尴尬了。

避坑指南:

我曾经接手过一个项目,前任把所有维度属性都用了类型 1。结果做客户流失分析时,根本不知道客户在流失前是什么等级、什么区域。后来花了整整两周从历史快照表里往回推算。记住:只要未来可能做趋势分析,就别用类型 1。

3.3 实现方式:SQL UPDATE

类型 1 的实现,核心就是一条 UPDATE 语句。但别小看它,生产环境里怎么写、怎么优化,门道不少。

3.3.1 基础实现

最简单的写法:

UPDATE dim_customer
SET phone = '13800138000',
    update_time = CURRENT_TIMESTAMP
WHERE customer_id = 10086;

这能跑,但只适合单条更新。实际 ETL 里,我们面对的是批量变更。

3.3.2 批量更新(标准做法)

从源系统拉取变更数据,放到临时表里,然后做关联更新:

-- Step 1: 创建临时表装载源端变更
CREATE TEMP TABLE tmp_customer_change AS
SELECT customer_id, phone, email
FROM source_customer
WHERE last_update > '2024-01-01';

-- Step 2: 关联更新维度表
UPDATE dim_customer AS t
SET phone = s.phone,
    email = s.email,
    update_time = CURRENT_TIMESTAMP
FROM tmp_customer_change AS s
WHERE t.customer_id = s.customer_id
  AND (t.phone <> s.phone OR t.email <> s.email);

注意最后那个 WHERE 条件。为什么要加?避免无效更新。我见过有人不加这个条件,每次跑完 ETL,整个维度表的 update_time 全变了。DBA 差点没把我骂死。

性能小技巧:

如果维度表很大(千万级以上),建议在 customer_id 上建索引。临时表也别忘了建索引。我曾经优化过一个跑 40 分钟的 ETL 任务,加了索引后降到 3 分钟——有时候问题就这么简单。

3.3.3 MERGE 写法(推荐)

有些数据库支持 MERGE(也叫 UPSERT),一条语句搞定插入和更新:

MERGE INTO dim_customer AS t
USING tmp_customer_change AS s
ON t.customer_id = s.customer_id
WHEN MATCHED THEN
    UPDATE SET phone = s.phone,
               email = s.email,
               update_time = CURRENT_TIMESTAMP
WHEN NOT MATCHED THEN
    INSERT (customer_id, phone, email, create_time, update_time)
    VALUES (s.customer_id, s.phone, s.email, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

我个人习惯用 MERGE。原因很简单:可读性好,逻辑集中。一条语句就能看清「匹配就更新,不匹配就插入」。不过要注意,MERGE 在并发高的场景下可能有死锁风险,生产环境要评估好。

3.4 类型 1 的变体:缓慢变化维中的「假覆盖」

讲个有意思的。有些场景下,业务方说「覆盖就行」,但你真的覆盖了,他们又后悔。怎么办?

我做过一个折中方案:物理上覆盖,逻辑上保留。

具体做法:

  • 维度表里保留当前值(类型 1)
  • 额外建一张变更日志表,记录每次变更的旧值、新值、变更时间

这样查询当前状态很快,分析历史也有据可查。当然,这已经不是纯粹的类型 1 了,算是一种混合策略。后面讲到类型 6 的时候会详细展开。

3.5 本章小结

类型 1 是 SCD 家族里最「简单粗暴」的成员。实现成本低,查询性能好,但代价是丢失历史。

选不选它,就一个问题:这个属性的历史值,未来有没有人关心?

如果答案是「没有」,放心用类型 1。如果答案是「可能有」或者「不确定」——嗯,我建议你翻到下一章看看类型 2。

SCD 类型 1 核心逻辑 源系统变更 临时变更表 维度表(覆盖) 核心操作:UPDATE dim_table SET new_value WHERE key = ? 不保留历史 旧值直接丢弃 查询性能最优 无需版本判断 实现成本最低 一条 UPDATE 搞定 适用场景判断标准 历史值是否有分析价值? → 没有 → 用类型 1 历史值是否有分析价值? → 有 → 考虑类型 2 或混合策略