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 降粒度的选择
| 维度 | 降粒度 | 桥接表 |
|---|---|---|
| 适用 | 多值关系是事实表粒度的一部分 | 多值关系是维度间多对多 |
| 例子 | 订单里的多商品 | 用户 ↔ 标签 |
| 度量影响 | 无(每行独立) | 需权重分摊 |
| 复杂度 | 低 | 中 |
五、四种特殊维度对比总表
| 维度类型 | 一句话 | 典型场景 | 关键动作 |
|---|---|---|---|
| 退化维度 | 只有标识、无属性的维度 | 订单号、发票号 | 直接放事实表 |
| 巨型维度 | 打包零散小字段 | 订单标志位集合 | 合并成一张杂项表 |
| 多值维度 | 一行对多个维度值 | 订单多商品、用户多标签 | 降粒度或桥接表 |
| 桥接表 | 处理多对多关系 | 用户 ↔ 标签 | 建桥接表 + 权重 |
六、常见坑点
- 退化维度也建维度表——
dim_order只有order_no一列,纯属浪费,直接退化 - 杂项维度塞高基数字段——把
user_id塞进杂项维度,组合爆炸 - 多值维度硬加多列——
tag1, tag2, tag3这种方式标签一多就废 - 桥接表不处理重复计算——JOIN 后度量被放大,SUM 出来的数全是错的
- 桥接表忘记权重语义——权重之和必须等于 1,否则分摊逻辑不成立
七、小结
- 退化维度:没有属性的维度标识,直接放事实表,别建空维度表
- 巨型维度:零散小字段打包成一张杂项维度,省 JOIN
- 多值维度:优先降粒度;维度间多对多才用桥接表
- 桥接表:处理多对多的利器,但必须配权重分摊防止重复计算
这四种特殊维度,加上前面的事实表、SCD、拉链表,就构成了完整的维度建模工具箱。
📌 本文是 dwsql.com 数仓建模方法论系列第 5 篇(维度建模部分收尾)。上篇见 拉链表设计原理与 SQL 实现,下一篇进入数仓分层与指标体系,见 数仓分层详解。
「数据仓库技术」文章同步更新,不错过每一篇干货

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

