维度建模基础:星型模型与雪花模型、事实表与维度表、缓慢变化维

聊到数据仓库建模,维度建模是绕不开的核心。我刚开始做数仓那会儿,觉得不就是画几张表嘛,后来踩了坑才明白——模型设计的好坏,直接决定了后续ETL好不好写、查询快不快、业务方骂不骂娘。

这一章,咱们把维度建模的底子打扎实。说白了,就是搞清楚三件事:星型和雪花怎么选、事实表和维度表怎么分、缓慢变化维怎么处理。

星型模型 vs 雪花模型

先看两张最经典的模型图。我习惯把星型模型叫做“一张大桌子围着一圈小凳子”——中间是事实表,周围是维度表,直接关联,不绕弯子。

核心区别一句话:星型模型是维度表直接连事实表,雪花模型是维度表再连子维度表。

星型模型 vs 雪花模型 星型模型 事实表 时间维度 产品维度 门店维度 雪花模型 事实表 产品维度 门店维度 时间维度 产品分类 门店区域 年月日 星型:查询快、易理解,适合大多数OLAP场景 雪花:节省存储、规范化高,但查询需多表JOIN

我在项目中遇到过不少团队,一上来就选雪花模型,理由是“数据更规范”。结果呢?查询慢了3倍,业务方天天催。你想想看,数仓的核心目标是快速响应分析,不是给数据库做范式设计。

对比维度 星型模型 雪花模型
查询性能 高(JOIN少) 低(JOIN多)
存储空间 较大(冗余) 较小(规范化)
ETL复杂度 简单 较复杂
业务理解 直观 需要熟悉表关系
适用场景 OLAP、报表、BI 数据治理要求高的场景

我的建议:80%的场景用星型模型就够了。除非你的维度表有上百个字段、且存储成本敏感,才考虑雪花化。

事实表与维度表

这两个概念是维度建模的基石。我习惯这么记:事实表记录“发生了什么”,维度表描述“是谁、在哪、什么时候”。

事实表

事实表的核心是度量值——销售额、订单量、点击次数,这些数字是可以加总、求平均的。事实表一般很大,动辄几亿行。

举个例子,电商订单事实表:

CREATE TABLE fact_order (
  order_id        BIGINT,
  date_sk         INT,       -- 时间维度外键
  product_sk      INT,       -- 产品维度外键
  store_sk        INT,       -- 门店维度外键
  customer_sk     INT,       -- 客户维度外键
  quantity        INT,       -- 数量(可加总)
  amount          DECIMAL,   -- 金额(可加总)
  discount        DECIMAL    -- 折扣(半可加)
);

注意看,事实表的外键指向维度表,度量字段才是分析的核心。我记得有一次,业务方要算“每个门店的月均客单价”,如果事实表设计得好,一条SQL就搞定。

维度表

维度表是“描述性”的,字段多是文本、分类、层级。比如产品维度:

CREATE TABLE dim_product (
  product_sk      INT PRIMARY KEY,
  product_id      VARCHAR(20),
  product_name    VARCHAR(100),
  category        VARCHAR(50),
  brand           VARCHAR(50),
  price           DECIMAL,
  effective_date  DATE,
  expired_date    DATE
);

维度表一般不会太大,几千到几百万行。但维度表的设计质量,直接决定了分析的灵活度。你想想看,如果产品维度里没有“品牌”字段,业务方想按品牌分析销售额,那就得重新ETL——这就是设计时的坑。

避坑指南:维度表不要只存ID,要把业务上常用的描述字段都加进去。我曾经见过一个客户维度表只有customer_id和name,结果业务方每次都要去CRM系统查地址、等级——这就是典型的维度表设计不足。

缓慢变化维(SCD Type 1/2/3)

现实世界的数据是会变的。客户的地址变了、产品的价格调了、员工的部门换了。怎么处理这些变化?这就是缓慢变化维要解决的问题。

我习惯把SCD分成三类,用一句话记住:Type 1覆盖、Type 2保留历史、Type 3记快照。

Type 1:直接覆盖

最简单粗暴。客户改了地址,直接把旧地址覆盖成新地址。优点是实现简单,缺点是历史没了。

-- 客户改了地址,直接UPDATE
UPDATE dim_customer
SET address = '新地址'
WHERE customer_sk = 123;

什么时候用?错误修正、不重要的属性。比如客户姓名打错了,直接改掉就行。

Type 2:保留历史

这是最常用的方式。每次变化都新增一条记录,用生效日期和失效日期来标记版本。

-- 客户改了地址,新增一条记录
INSERT INTO dim_customer (customer_sk, customer_id, address, effective_date, expired_date, is_current)
VALUES (456, 'C001', '新地址', '2024-06-01', '9999-12-31', 1);

-- 旧记录标记为失效
UPDATE dim_customer
SET expired_date = '2024-05-31', is_current = 0
WHERE customer_sk = 123;

Type 2的好处是能回溯历史——比如分析“去年每个地址的客户数”,Type 2就能做到。代价是维度表会膨胀,但现代数仓完全扛得住。

我的经验:金融、保险、审计类业务,必须用Type 2。我曾经帮一个银行做数仓,他们需要查三年前的客户地址——没有Type 2根本做不到。

Type 3:记录快照

Type 3是在维度表里加几个“历史字段”,比如“原地址”和“现地址”。只能保留最近一次变化。

-- 客户改了地址,保留原地址
UPDATE dim_customer
SET previous_address = address,  -- 把当前地址移到历史字段
    address = '新地址'           -- 写入新地址
WHERE customer_sk = 123;

Type 3用得少,因为只能记一次变化。什么时候用?只需要对比“变化前后”的场景,比如分析“搬家后消费行为有没有变化”。

SCD类型 实现方式 历史保留 存储开销 典型场景
Type 1 直接覆盖 不保留 低 错误修正、不重要属性
Type 2 新增版本行 完整保留 高 金融、审计、客户分析
Type 3 增加历史字段 仅保留上一次 中 前后对比分析

选型建议:如果业务方说“我要看历史数据”,直接上Type 2。如果他们说“改了就改了,不用管旧的”,Type 1就够了。Type 3作为补充,别当主力。

嗯,维度建模的基础就这些。星型模型和雪花模型选型、事实表和维度表的分工、SCD的处理策略——这三块搞清楚了,数仓建模的骨架就有了。剩下的就是根据业务场景去填充血肉。