维度建模基础回顾:星型模型与雪花模型、事实表与维度表、缓慢变化维(SCD)处理策略
好,咱们正式开始第二讲。在聊实时场景之前,我习惯先把地基夯实。维度建模这东西,说白了就是一套「怎么把数据组织得让业务人员看得懂、查得快」的方法论。我见过太多团队,一上来就搞实时,结果模型乱七八糟,最后查个指标要关联十几张表——那还不如不搞。
这一章我们聚焦四个核心概念:星型模型、雪花模型、事实表与维度表,还有那个让无数人头疼的缓慢变化维(SCD)。嗯,咱们一个一个来。
星型模型 vs 雪花模型
先看两张最经典的图。星型模型,中心一张事实表,周围一圈维度表,像星星一样辐射出去。雪花模型呢,维度表还能继续往下拆,像雪花一样分叉。
核心区别一句话:星型是「空间换时间」,雪花是「时间换空间」。实时场景下,我几乎只用星型。
我个人习惯,在实时数仓里几乎只用星型模型。为什么?你想想看,实时场景下每多一次 JOIN,延迟就多一分。雪花模型虽然省存储,但那个「省」在实时场景里根本不值一提——你省的那点磁盘,还不够弥补查询延迟带来的损失。
| 对比维度 | 星型模型 | 雪花模型 |
|---|---|---|
| 查询性能 | 高(单层 JOIN) | 低(多层 JOIN) |
| 存储空间 | 有冗余 | 无冗余 |
| ETL 复杂度 | 简单 | 复杂 |
| 实时场景推荐度 | 强烈推荐 | 不推荐 |
我的经验:在实时 Flink 作业里,我习惯把维度表做成「宽表」或者直接加载到内存中。星型模型天然适合这种玩法——维度表少,缓存命中率高。
事实表与维度表
这两个概念是维度建模的基石。我换个说法帮你理解:事实表记录「发生了什么」,维度表描述「谁、什么、在哪、何时」。
事实表的三种类型
事实表不是只有一种。我遇到过不少新手,把所有事实都塞进一张表,结果数据膨胀得吓人。其实事实表分三类:
- 事务事实表:每发生一次事件就记一行。比如每笔订单、每次点击。这是实时场景最常用的。
- 周期快照事实表:按固定时间间隔拍快照。比如每天统计一次库存水位。
- 累积快照事实表:跟踪一个流程的生命周期。比如从下单到发货到签收的全过程。
在实时场景里,我打交道最多的就是事务事实表。Kafka 里流过来的每条消息,基本都能映射成事务事实表的一行。
维度表的设计要点
维度表相对稳定,但也不是一成不变。设计时我习惯遵循几个原则:
- 代理键优先:别直接用业务主键,加一列自增的代理键。为什么?业务主键可能会变,代理键不会。
- 退化维度:如果某个维度只有一个属性,直接塞进事实表,别单独建表。比如订单号,很多时候就是退化维度。
- 维度一致性:同一个维度在不同事实表里要能共享。比如「时间维度」在所有事实表里定义必须一致。
避坑指南:我曾经在一个项目里直接用业务主键关联维度表,结果业务方某天改了主键规则,整个数仓数据对不上了。从那以后,代理键成了我的铁律。
缓慢变化维(SCD)处理策略
终于到了这个让无数人头疼的话题。SCD 说白了就是:维度属性变了,怎么办?
比如客户的等级从「普通」变成了「VIP」,或者产品的分类从「食品」调整成了「生鲜」。这些变化怎么处理?业界有几种经典策略。
SCD 类型一:直接覆盖
最简单粗暴。旧值不要了,直接改成新值。适合那些「错了就改」或者「历史不重要」的属性。
-- 客户等级变更,直接覆盖
UPDATE dim_customer
SET customer_level = 'VIP'
WHERE customer_id = 12345;
优点:实现简单,查询快。缺点:历史丢失了。如果你需要回溯分析,比如「上个月VIP客户有多少」,那就完蛋了。
SCD 类型二:增加新行
这是我最常用的策略。每次变化都新增一行,用生效时间区分。
-- 客户等级变更,新增一行
INSERT INTO dim_customer (customer_id, customer_level, effective_date, end_date, is_current)
VALUES (12345, 'VIP', '2024-06-01', '9999-12-31', 1);
-- 旧行关闭
UPDATE dim_customer
SET end_date = '2024-05-31', is_current = 0
WHERE customer_id = 12345 AND is_current = 1;
这样你既能查当前状态,也能回溯历史。实时场景下,我习惯在 Flink 里维护一个「最新维度快照」,同时把变更流写入历史表。
SCD 类型三:增加新列
这个策略比较奇葩——每次变化加一列。比如第一次变化用 level_1,第二次用 level_2。说实话,我几乎不用。列数会无限膨胀,而且查询时你得知道该查哪一列。
| 策略 | 实现方式 | 历史可追溯 | 实时场景适用性 |
|---|---|---|---|
| SCD 1 | 直接覆盖 | 否 | 高(简单快速) |
| SCD 2 | 增加新行 | 是 | 中(需维护版本) |
| SCD 3 | 增加新列 | 有限 | 低(列膨胀) |
注意:实时场景下 SCD 2 的实现要格外小心。Flink 的状态后端如果保存了全量维度历史,状态会越来越大。我一般只保留「当前有效版本」在状态里,历史版本丢到 HBase 或 MySQL 里。
实时场景下的 SCD 选择建议
嗯,这里我直接给结论吧:
- 维度属性变化不频繁(比如性别、出生日期):用 SCD 1,直接覆盖。
- 维度属性变化频繁且需要回溯(比如客户等级、产品分类):用 SCD 2,但只把当前版本放内存,历史版本放外部存储。
- 别碰 SCD 3。除非你确定这个属性最多只变一次,而且业务方明确要求保留两次值。
我记得有一次做实时用户画像,客户等级一天变好几次。如果用 SCD 2 全量存状态,Flink 的状态能撑到 10GB。后来改成「只存最新 + 变更流写 Kafka」,状态降到 200MB,问题就解决了。
一个小技巧:在实时维度表里加一个 version 字段,每次变更 version + 1。这样下游消费时,如果发现 version 变了,就知道维度更新了,可以重新关联。