4. 缓慢变化维度(SCD)陷阱:错误选择SCD策略,导致历史数据丢失或存储爆炸

SCD 策略选错了,后果很直接——要么历史数据丢了,要么存储炸了。我见过太多团队在这上面栽跟头。

说白了,SCD 就是处理维度属性变化的一套规则。比如客户的地址变了、产品的分类调整了,你怎么在数据仓库里记录这个变化?

选错了策略,轻则分析结果对不上,重则整个数仓重做。嗯,这里咱们得好好捋一捋。

4.1 三种经典 SCD 策略,你真的用对了吗?

先快速过一遍基础。SCD 有三大经典策略,我直接给你列个表对比:

策略 核心逻辑 存储方式 典型场景
SCD 类型 1 直接覆盖旧值 只保留最新值 错误修正、不重要属性
SCD 类型 2 新增一条记录 保留全部历史 客户地址、产品分类
SCD 类型 3 增加新列存储旧值 只保留有限历史 有限回溯分析

看起来很简单对吧?但陷阱就藏在细节里。

4.2 陷阱一:滥用 SCD 类型 1,历史数据说丢就丢

我遇到过一家电商公司,他们的客户维度表一直用类型 1。客户改个手机号,直接覆盖。结果财务部门要做历史订单的客户联系方式回溯,发现所有老订单的客户手机号都是最新的——完全对不上账。

为什么会这样? 因为类型 1 的设计初衷就是「不保留历史」。你覆盖了,就再也找不回来了。

避坑指南: 我曾经在金融项目中吃过这个亏。后来定了个规矩:凡是涉及审计、合规、财务对账的维度属性,一律不用类型 1。哪怕业务方说「这个属性不重要」,你也要多问一句:「三个月后要查历史数据怎么办?」

什么时候可以用类型 1?

  • 数据录入错误修正(比如把「张三」改成「张三」)
  • 不参与历史分析的属性(比如客户昵称)
  • 业务上明确不需要回溯的属性

4.3 陷阱二:无脑使用 SCD 类型 2,存储爆炸

类型 2 是保留历史最彻底的方式,但也是最容易「存储爆炸」的。

我见过一个极端案例:某零售企业的产品维度表,每天跑全量更新,所有属性都按类型 2 处理。结果呢?一张产品维度表,半年时间从 10 万条膨胀到 800 万条。查询性能直线下降,ETL 跑一次要 6 小时。

问题出在哪? 他们把「产品上架时间」「产品状态」这种高频变化的属性也用了类型 2。你想想看,产品状态每天变几次,每次变都新增一条记录,不爆炸才怪。

我的建议: 类型 2 只用于「真正需要保留历史」且「变化频率可接受」的属性。对于高频变化属性,要么拆成事实表,要么用类型 1 覆盖。

类型 2 的正确打开方式:

  1. 给每条记录加上 valid_from 和 valid_to 时间戳
  2. 加上 current_flag 标记当前有效记录
  3. 定期归档过期记录到历史表
-- 类型 2 维度表示例
CREATE TABLE dim_customer (
    customer_sk INT PRIMARY KEY,      -- 代理键
    customer_id INT,                   -- 业务主键
    customer_name VARCHAR(100),
    address VARCHAR(200),
    valid_from DATE,                   -- 生效日期
    valid_to DATE,                     -- 失效日期(NULL 表示当前有效)
    current_flag CHAR(1) DEFAULT 'Y'   -- Y/N
);

4.4 陷阱三:类型 3 的「有限历史」陷阱

类型 3 看起来很美——加一列存旧值,既能回溯又能节省空间。但它的局限性很明显:只能保留一次变化。

我有个客户,用类型 3 记录客户的「省」属性变化。第一次变化没问题,第二次变化时,旧值就被新值覆盖了。结果业务方要查「客户最初在哪个省」,数据已经没了。

类型 3 适合什么场景?

  • 只需要对比「当前值」和「上一次值」
  • 变化次数极少(比如一年变一次)
  • 存储空间极度受限

核心原则: 类型 3 不是类型 2 的替代品,它是特定场景下的折中方案。别指望用它来保留完整历史。

4.5 如何正确选择 SCD 策略?

我个人习惯用这个决策流程,你可以参考一下:

  1. 这个属性需要保留历史吗? 不需要 → 类型 1
  2. 需要保留完整历史吗? 需要 → 类型 2
  3. 只需要对比当前和上一次? 是 → 类型 3
  4. 变化频率高且需要历史? 考虑拆成事实表

下面这张图把决策逻辑画清楚了:

SCD 策略选择决策流程图 SCD 策略选择决策流程 维度属性变化 需要保留历史数据? 否 SCD 类型 1 是 需要完整历史? 是 SCD 类型 2 否 仅需对比当前和上一次? 是 SCD 类型 3 否 拆成事实表 提示:高频变化属性优先考虑拆成事实表,避免维度表膨胀

4.6 混合策略:现实世界的最优解

实际项目中,我很少只用一种策略。大多数时候是混合使用。

举个例子,客户维度表:

  • 客户姓名:类型 1(错误修正用覆盖)
  • 客户地址:类型 2(需要完整历史)
  • 客户等级:类型 3(只需要对比当前和上一次)
  • 最后登录时间:类型 1(高频变化,不保留历史)

我的经验: 设计维度表时,先和业务方确认每个属性的「历史回溯需求」。别自己猜,猜错了就是坑。我曾经因为没问清楚,把「客户等级」用了类型 2,结果一年后存储翻了 10 倍,业务方却说「我们只看当前等级」——白费功夫。

4.7 存储爆炸的终极解法:归档与分区

即使选对了策略,类型 2 的维度表还是会越来越大。怎么办?

两个手段:

  1. 分区表:按时间分区,过期数据自动归档到冷存储
  2. 历史表分离:当前有效记录放主表,过期记录移入历史表
-- 分区表示例(按 valid_from 月份分区)
CREATE TABLE dim_customer_partitioned (
    customer_sk INT,
    customer_id INT,
    address VARCHAR(200),
    valid_from DATE,
    valid_to DATE,
    current_flag CHAR(1)
)
PARTITION BY RANGE (valid_from) (
    PARTITION p202301 VALUES LESS THAN ('2023-02-01'),
    PARTITION p202302 VALUES LESS THAN ('2023-03-01'),
    PARTITION p_future VALUES LESS THAN (MAXVALUE)
);

嗯,SCD 策略没有银弹。选错了,要么丢历史,要么炸存储。我的建议是:先搞清楚业务需求,再选策略。别一上来就类型 2 走天下,也别图省事全用类型 1。

记住一句话:维度建模的灵活性,来自于对业务变化的深刻理解,而不是对某种策略的盲目崇拜。