3. 维度建模优化:缓慢变化维(SCD)策略、维度退化、维度合并与拆分
维度建模做得好不好,直接决定了数据仓库好不好用。我见过太多项目,ETL 跑得飞快,但业务方查一个历史数据要等半天——问题就出在维度设计上。
这一章,咱们聊聊维度建模里最绕不开的三个优化点:缓慢变化维、维度退化、维度合并与拆分。说白了,就是怎么让维度表既能存得下历史,又查得快,还不至于把模型搞成一团乱麻。
3.1 缓慢变化维(SCD)策略
先问个问题:客户的地址变了,你是直接覆盖,还是保留旧地址?
这就是 SCD 要解决的事。业务数据里的属性会变,但分析时往往需要知道「变化那一刻」的样子。我习惯把 SCD 策略分成三类,大部分场景用这三种就够了。
3.1.1 SCD Type 1:直接覆盖
最简单,也最粗暴。新值来了,旧值直接抹掉。
适用场景:不需要追溯历史的属性,比如客户的「最后登录时间」、产品的「当前库存状态」。
-- Type 1 更新示例
UPDATE dim_customer
SET city = '上海', update_time = CURRENT_TIMESTAMP
WHERE customer_id = 1001;
嗯,这里要注意:Type 1 虽然快,但历史数据就丢了。如果你做的是财务审计,千万别用这个。
3.1.2 SCD Type 2:保留历史版本
这是我最常用的策略。每次变化都新增一条记录,用生效时间和失效时间来标记版本。
我在项目中遇到过这样一个坑:业务方说「我要看去年每个月的客户归属地」,结果 Type 1 一覆盖,全乱套了。后来改成 Type 2,才把账算清楚。
-- Type 2 维度表示例
CREATE TABLE dim_customer_scd2 (
customer_sk BIGINT PRIMARY KEY, -- 代理键
customer_id INT, -- 业务主键
customer_name VARCHAR(100),
city VARCHAR(50),
eff_date DATE, -- 生效日期
end_date DATE, -- 失效日期,'9999-12-31' 表示当前有效
is_current CHAR(1) -- 'Y' 当前有效,'N' 历史
);
| customer_sk | customer_id | city | eff_date | end_date | is_current |
|---|---|---|---|---|---|
| 1001 | 1 | 北京 | 2023-01-01 | 2024-06-30 | N |
| 1002 | 1 | 上海 | 2024-07-01 | 9999-12-31 | Y |
我曾经踩过的坑:Type 2 表如果没建好索引,查询当前记录时会全表扫描。一定要在 is_current 和 customer_id 上建联合索引。
3.1.3 SCD Type 3:保留有限历史
Type 3 只保留上一次变化的值。比如客户表里加一个 previous_city 字段。
说实话,Type 3 用得不多。除非业务明确说「我只需要对比当前和上一次」,否则我建议直接用 Type 2。
3.2 维度退化
什么叫维度退化?你想想看,有些属性本来属于维度表,但因为业务太简单,或者变化频率太低,单独建一张维度表反而浪费。
比如订单状态。状态值就那么几个(待支付、已支付、已发货、已完成),而且变化不频繁。单独建一张 dim_order_status 表?没必要。直接把这个字段留在事实表里就行。
我的判断标准:如果维度属性不超过 3 个,且取值少于 20 个,我就倾向于退化到事实表。这样能减少一次 JOIN,查询性能提升很明显。
-- 退化前:需要 JOIN 维度表
SELECT f.order_id, s.status_name
FROM fact_order f
JOIN dim_order_status s ON f.status_sk = s.status_sk;
-- 退化后:直接查事实表
SELECT order_id, order_status
FROM fact_order;
你看,少了一次 JOIN,查询快了,模型也简单了。但要注意:如果状态值以后会扩展成复杂属性(比如增加「状态描述」「状态分组」),那还是老老实实建维度表。
3.3 维度合并与拆分
这是维度建模里最考验经验的地方。合并还是拆分,没有的对错,关键看业务怎么用。
3.3.1 维度合并
什么时候合并?当两个维度总是同时出现,而且查询时几乎不会单独过滤其中一个。
举个例子:客户维度和客户等级维度。每次分析都要看「客户在哪个等级」,而且等级属性很少单独使用。那就合并成一张 dim_customer 表,把等级字段直接放进去。
合并的好处很明显:少 JOIN,查询快。坏处呢?如果等级维度有独立的更新逻辑(比如等级规则变了),合并后更新起来会比较麻烦。
3.3.2 维度拆分
拆分正好相反。当一个维度表太大,或者某些属性更新频率差异太大时,拆开更合理。
我做过一个电商项目,产品维度表有 80 多个字段。其中「产品名称」「品牌」几乎不变,但「促销价」「库存状态」每天变好几次。混在一起,每次更新都要全表扫描,慢得要命。
后来拆成两张表:dim_product_base(稳定属性)和 dim_product_daily(高频变化属性)。更新高频表时只影响少量字段,性能提升了好几倍。
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 维度合并 | 维度总是一起查询,且更新频率一致 | 减少 JOIN,查询快 | 更新逻辑耦合,灵活性降低 |
| 维度拆分 | 维度属性多,或更新频率差异大 | 更新快,维护灵活 | 查询时需要 JOIN,增加复杂度 |
3.4 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作一个决策树:遇到维度变化时,先判断用哪种 SCD 策略;再看能不能退化;最后决定是合并还是拆分。
你看,整个决策过程其实不复杂。先搞定 SCD 策略,再判断能不能退化,最后决定合并还是拆分。每一步都有明确的判断标准。
我的建议:刚开始做维度建模时,别追求一步到位。先用 Type 2 兜底,等业务稳定了再考虑退化或拆分。我曾经在一个项目里过早做了维度拆分,结果业务需求一变,又得重新合并,白白浪费了两周时间。
维度建模优化,说白了就是平衡「查询性能」和「维护成本」。没有银弹,但掌握了 SCD、退化、合并拆分这三板斧,大部分场景都能应付。