4. 维度表设计:维度表结构、退化维度、角色扮演维度、维度表设计原则

维度表,说白了就是数据仓库里的「字典」和「标签本」。它告诉我们事实表中的每一行数据,到底是谁、在哪儿、什么时候、什么产品。没有维度表,事实表就是一串冰冷的数字,毫无意义。

我刚开始做维度建模时,总觉得维度表就是简单的「码表」,把ID翻译成名称就完事了。后来被业务部门追着问「为什么这个月华东区的销售额比华北区高这么多,但你们的数据里连区域层级都查不了」——嗯,那次之后我才真正重视起维度表的设计。

核心观点:维度表的质量,直接决定了数据仓库的可用性和分析深度。一个设计良好的维度表,能让分析师爱不释手;一个糟糕的维度表,会让整个数仓变成「数据垃圾场」。

4.1 维度表的基本结构

维度表的结构其实很简单,就两大部分:

  • 主键(Surrogate Key):我习惯用自增整数或雪花ID,不用业务主键。为什么?因为业务主键可能会变,比如产品编码改了、部门合并了,用代理键能隔离这些变化。
  • 属性列(Attribute Columns):描述维度的各种特征,比如产品名称、品类、品牌、颜色、尺寸等。

举个例子,一个典型的产品维度表长这样:

CREATE TABLE dim_product (
    product_sk      INT PRIMARY KEY,      -- 代理键
    product_id      VARCHAR(20),          -- 业务主键(保留用于追溯)
    product_name    VARCHAR(100),
    category        VARCHAR(50),
    sub_category    VARCHAR(50),
    brand           VARCHAR(50),
    color           VARCHAR(20),
    size            VARCHAR(10),
    unit_price      DECIMAL(10,2),
    effective_date  DATE,                 -- 缓慢变化维用
    expiry_date     DATE,
    is_current      CHAR(1)
);

这里要注意,product_id 是业务主键,我保留它只是为了方便和源系统对账,但维度表的主键永远是 product_sk。这个习惯让我少踩了很多坑。

4.2 退化维度

退化维度这个概念,我第一次听到时觉得挺玄乎。说白了,就是那些「本来应该做成维度表,但属性太少,直接塞进事实表更划算」的字段。

最常见的例子就是订单号。订单号本身就是一个维度,但它通常只有这一个属性(最多加个订单类型)。如果单独建一张订单维度表,里面就一列订单号,然后事实表通过外键关联——这太浪费了。不如直接把订单号放在事实表里,这就是退化维度。

我的经验:判断一个字段是否应该做退化维度,就看它有没有「可分析的属性」。如果除了ID本身,你再也想不出3个以上的描述性属性,那就直接退化到事实表。我在电商项目中,把订单号、物流单号、支付流水号都做成了退化维度,事实表只多了几个字符串字段,但省去了好几张「空壳」维度表的关联开销。

退化维度的优缺点对比:

维度 优点 缺点
退化维度 减少关联、查询更快、ETL更简单 事实表变宽、无法存储维度属性变化历史
标准维度表 可存储丰富属性、支持SCD、易于复用 增加关联、查询性能略低

4.3 角色扮演维度

角色扮演维度,名字听着高大上,其实就是一个维度表在事实表中被多次引用,每次扮演不同的「角色」。

最经典的例子就是日期维度。一张订单事实表里,可能有下单日期、付款日期、发货日期、收货日期。难道要为每个日期都建一张维度表?当然不。我们只需要一张日期维度表,然后在事实表里用不同的外键字段去关联它。

看这个例子:

-- 事实表
CREATE TABLE fact_order (
    order_sk        INT PRIMARY KEY,
    order_date_sk   INT,      -- 关联 dim_date,角色:下单日期
    pay_date_sk     INT,      -- 关联 dim_date,角色:付款日期
    ship_date_sk    INT,      -- 关联 dim_date,角色:发货日期
    customer_sk     INT,
    product_sk      INT,
    amount          DECIMAL(12,2)
);

-- 查询时用别名区分角色
SELECT 
    d1.full_date AS order_date,
    d2.full_date AS pay_date,
    d3.full_date AS ship_date,
    f.amount
FROM fact_order f
JOIN dim_date d1 ON f.order_date_sk = d1.date_sk
JOIN dim_date d2 ON f.pay_date_sk = d2.date_sk
JOIN dim_date d3 ON f.ship_date_sk = d3.date_sk;

除了日期,客户维度也经常玩角色扮演。比如一笔交易中,有买家、卖家、推荐人,它们本质上都是客户,只是扮演的角色不同。

避坑指南:我曾经在一个项目中,把客户维度复制了三份——dim_buyer、dim_seller、dim_referrer,结果数据量翻了三倍,维护起来想死。后来改成角色扮演,一张表搞定。记住:只要属性结构相同,就别复制表,用角色扮演。

4.4 维度表设计原则

做了这么多年维度建模,我总结了几条铁律。这些原则不是书本上抄来的,是实打实用血泪换来的。

4.4.1 原则一:用代理键,别用业务主键

前面提过,我再强调一次。业务主键会变,代理键不会。代理键是整数,关联性能好。代理键能隔离源系统变化。我见过一个项目,直接用产品编码做维度主键,结果源系统产品编码重构,整个数仓瘫痪了两周。

4.4.2 原则二:维度属性要「可枚举、可分组、可钻取」

每个属性都要能回答业务问题。比如「产品颜色」这个属性,能用来分组统计吗?能用来下钻吗?如果某个属性99%的值都是「黑色」,那它就没有分析价值,可以考虑去掉。

4.4.3 原则三:维度表要「胖」不要「瘦」

维度表宽一点没关系,把能放的描述性属性都放进去。分析师喜欢在维度表里一次性拿到所有标签,而不是关联四五张表才能拼出一个完整的产品信息。我习惯把维度表的字段数控制在20-50个之间,太少了不够用,太多了维护困难。

4.4.4 原则四:处理缓慢变化维(SCD)

维度属性会变,比如客户搬家了、产品改价了。怎么处理?

  • SCD Type 1:直接覆盖,不保留历史。适合「改错了」的属性。
  • SCD Type 2:新增一行,保留历史。适合需要追溯分析的属性。
  • SCD Type 3:加一列存旧值。适合「只关心上一次变化」的场景。

我个人90%的场景用Type 2,因为它最灵活。但要注意,Type 2会让维度表膨胀,需要定期清理过期数据。

4.4.5 原则五:维度表之间不要直接关联

维度表应该通过事实表间接关联,不要搞成「维度表关联维度表」的网状结构。否则你就是在建雪花模型,查询性能会急剧下降。记住:星型模型是王道,雪花模型是邪道(除非你有特殊理由)。

总结一下:代理键、胖维度、可分析属性、SCD策略、星型结构——这五条原则,你只要遵守了,维度表设计就不会出大问题。

4.5 维度表设计核心逻辑图

下面这张图,是我自己梳理的维度表设计决策流程。每次设计新维度时,我都会过一遍这个流程:

维度表设计决策流程 维度表设计决策流程 识别业务维度 是否有 ≥3 个可分析属性? 是 建标准维度表(代理键+属性) 是否被事实表多次引用? 是 使用角色扮演(别名关联) 应用SCD策略(推荐Type 2) 否 退化维度 提示:退化维度直接放入事实表;标准维度表使用代理键,避免雪花模型

这个流程我用了好几年,基本覆盖了日常工作中90%的场景。你按照这个思路走,不会出大错。

4.6 避坑总结

最后,分享几个我踩过的坑,希望你别再踩:

  • 别把维度表做成「代码翻译表」:只放ID和名称,不放分析属性。这样的维度表,分析师用一次骂一次。
  • 别滥用退化维度:如果一个字段有10个以上的属性,还是老老实实建维度表吧。退化维度只适合「属性极少」的场景。
  • 别忘记处理NULL:维度表中的NULL会导致事实表关联丢失。我习惯在维度表中加一行「未知」记录,代理键设为-1,专门处理这种情况。
  • 别让维度表无限膨胀:SCD Type 2会让维度表越来越大。定期归档历史数据,或者用拉链表技术控制数据量。

嗯,维度表设计就聊到这儿。记住:维度表是数据仓库的「脸面」,分析师每天打交道最多的就是它。设计得好,人人夸你;设计得烂,天天背锅。