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。