跳到主要内容

07.退化维度、巨型维度、多值维度与桥接表

常规维度(用户、商品、时间)人人会建,真正拉开差距的是几种「特殊维度」:退化维度、巨型维度、多值维度,以及处理多值关系的桥接表。这几个也是最容易建模出错的地方。

前面几篇讲的都是「标准」维度表:一张维表、一个主键、和事实表一对一关联。但真实业务里总有各种「不标准」的情况,这篇文章一次讲清楚四种特殊维度怎么设计。


一、退化维度 (Degenerate Dimension)​

1.1 是什么​

退化维度是放在事实表里、没有对应维度表的维度。它本身是维度的「标识」,比如订单号、发票号、运单号。

fact_order(订单事实表)

order_id | user_id | product_id | amount | order_no ← 退化维度
---------|---------|------------|--------|-----------
1 | 100 | 2001 | 88.00 | ORD20260105001

order_no(订单编号)是维度的标识,但没有独立的 dim_order 表——因为除了订单号本身,它没有任何其他描述属性。于是它「退化」成了事实表里的一个普通字段。

1.2 为什么不单独建维度表​

如果建一张 dim_order,里面只有 order_no 一列,那这张维度表除了提供主键,毫无意义,还多一次 JOIN。所以 Kimball 建议:这种「只有标识、没有属性」的维度,直接退化到事实表里。

1.3 典型例子​

  • 订单号、发票号、运单号
  • POS 交易流水号
  • 支付交易号

1.4 价值​

退化维度最大的价值是操作型分析:按订单号精确查找、按单据分组、钻取到单据明细。它让事实表自己就能回答「这笔订单是什么」,不需要 JOIN。


二、巨型维度 (Junk Dimension / 杂项维度)​

2.1 是什么​

业务里常有一堆低基数、零散、彼此无关的小字段,比如订单的「是否加急」「是否保价」「支付类型」「配送方式」。每个字段单独建维度表太浪费,于是把它们打包进一张维度表,这张表就叫杂项维度(巨型维度)。

dim_order_junk(杂项维度,把所有标志位打包)

junk_id | is_urgent | is_insured | pay_type | delivery_type
--------|-----------|------------|----------|---------------
1 | 是 | 是 | 微信 | 当日达
2 | 否 | 是 | 支付宝 | 次日达
3 | 是 | 否 | 银行卡 | 当日达
...

2.2 为什么叫「巨型」​

因为这些小字段组合起来,产生的行数可能远超直觉——5 个布尔字段 = 2^5 = 32 种组合,10 个 = 1024 种。但相比「每个字段单独建表」,打包后的事实表只需一个外键 junk_id,JOIN 少很多。

2.3 设计要点​

  • 只打包低基数、无层级、彼此无关的标志位/小维度
  • 高基数字段(如用户 ID)绝不放进杂项维度
  • 杂项维度通常是预生成的笛卡尔组合表

三、多值维度 (Multivalued Dimension)​

3.1 是什么​

多值维度是一个事实行对应多个维度值的情况。比如:

  • 一张订单对应多个商品(一个订单买了 3 件不同商品)
  • 一个用户有多个标签(既算「高价值」又算「母婴人群」)
  • 一篇内容有多个分类

常规维度建模假设「一行事实 ↔ 一个维度值」,多值维度打破了这个假设。

3.2 两种处理方式​

方式一:降低事实表粒度(推荐)

把事实表粒度下沉到「订单明细」,让每个商品占一行,多值关系自然消失:

fact_order_item(订单明细,一行 = 一个订单的一个商品)

order_item_id | order_id | product_id | amount
--------------|----------|------------|--------
1 | 90001 | 2001 | 50.00
2 | 90001 | 2002 | 38.00 ← 同一个订单,两行

方式二:桥接表(见下一节)

当多值关系本身需要被建模(如「用户 ↔ 多个标签」),用桥接表表达。

3.3 判断标准​

  • 多值关系本身就是事实表粒度的一部分(如订单里的多商品)→ 降粒度
  • 多值关系是维度之间的多对多(如用户 ↔ 标签)→ 桥接表

四、桥接表 (Bridge Table)​

4.1 是什么​

桥接表用于处理维度与事实(或维度与维度)之间的多对多关系。它像一座桥,把「一个事实行对应多个维度值」规范化地表达出来。

最经典的场景:用户有多个标签,一个标签又属于多个用户。

fact_user_behavior(用户行为事实表)

user_id | behavior | ...
--------|----------|----
100 | 下单 | ...
101 | 浏览 | ...

↓ 用户和标签是多对多,不能直接加一个 tag_id 外键

bridge_user_tag(桥接表:用户 ↔ 标签)

user_id | tag_id | weight
--------|--------|--------
100 | 1 | 1.0
100 | 2 | 0.8
101 | 1 | 1.0

4.2 为什么需要桥接表​

如果不建桥接表,就只能二选一:

  • 加 tag_id 外键 → 一个用户多个标签就塞不下(除非降粒度,但会扭曲事实)
  • 在事实表里加 tag1, tag2, tag3 多列 → 标签数量不确定,列数爆炸,且无法优雅过滤

桥接表把「多对多」拆成「两个一对多」,既保留多值关系,又不污染事实表。

4.3 桥接表的坑​

桥接表最大的坑是重复计算。当用户有 3 个标签时,事实表 JOIN 桥接表会产生 3 行:

-- 用户 100 有 2 个标签,JOIN 后行为被放大 2 倍
SELECT t.tag_id, SUM(f.amount) AS total
FROM fact_user_behavior f
JOIN bridge_user_tag t ON f.user_id = t.user_id
GROUP BY t.tag_id;
-- 结果里每个标签的金额都包含了「跨标签」的重复计算

解决方案:加 weight(权重)字段,让同一用户的多个标签权重之和 = 1:

-- 用权重分摊,避免重复计算
SELECT t.tag_id, SUM(f.amount * t.weight) AS total
FROM fact_user_behavior f
JOIN bridge_user_tag t ON f.user_id = t.user_id
GROUP BY t.tag_id;

桥接表最需要注意的一点:「JOIN 会导致度量重复计算」,解决方案就是权重分摊 + 明确聚合语义。

4.4 桥接表 vs 降粒度的选择​

维度降粒度桥接表
适用多值关系是事实表粒度的一部分多值关系是维度间多对多
例子订单里的多商品用户 ↔ 标签
度量影响无(每行独立)需权重分摊
复杂度低中

五、四种特殊维度对比总表​

维度类型一句话典型场景关键动作
退化维度只有标识、无属性的维度订单号、发票号直接放事实表
巨型维度打包零散小字段订单标志位集合合并成一张杂项表
多值维度一行对多个维度值订单多商品、用户多标签降粒度或桥接表
桥接表处理多对多关系用户 ↔ 标签建桥接表 + 权重

六、常见坑点​

  1. 退化维度也建维度表——dim_order 只有 order_no 一列,纯属浪费,直接退化
  2. 杂项维度塞高基数字段——把 user_id 塞进杂项维度,组合爆炸
  3. 多值维度硬加多列——tag1, tag2, tag3 这种方式标签一多就废
  4. 桥接表不处理重复计算——JOIN 后度量被放大,SUM 出来的数全是错的
  5. 桥接表忘记权重语义——权重之和必须等于 1,否则分摊逻辑不成立

七、小结​

  • 退化维度:没有属性的维度标识,直接放事实表,别建空维度表
  • 巨型维度:零散小字段打包成一张杂项维度,省 JOIN
  • 多值维度:优先降粒度;维度间多对多才用桥接表
  • 桥接表:处理多对多的利器,但必须配权重分摊防止重复计算

这四种特殊维度,加上前面的事实表、SCD、拉链表,就构成了完整的维度建模工具箱。


📌 本文是 dwsql.com 数仓建模方法论系列第 5 篇(维度建模部分收尾)。上篇见 拉链表设计原理与 SQL 实现,下一篇进入数仓分层与指标体系,见 数仓分层详解。

📱关注公众号

「数据仓库技术」文章同步更新,不错过每一篇干货

微信公众号二维码
💬加群交流

备注「数据仓库技术」加入社群,每日一道大厂SQL真题

交流微信二维码

你可能还想看