订阅更新

维度表设计——什么字段该冗余、什么该 JOIN 进来?

数仓开发手册 第 7 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 业务场景:我在某短视频平台的用户维度表设计
  3. 段落一:冗余 vs 不冗余的三条判断标准
  4. 段落二:缓慢变化维(SCD)的四种方案
  5. 段落三:高基数维度必须独立
  6. 段落四:维度冗余的真实收益计算
  7. 面试高频追问
  8. 实战踩坑:维度属性变更没做 SCD 的惨案
  9. 总结

先说结论

冗余的唯一理由是『减少下游 JOIN』。如果一个字段 90% 的查询都要 JOIN 它进来,那就冗余到维度表里;如果只有一两个报表用,就让它们 JOIN。

变化频率是冗余决策的第一原则。稳定字段(国家编码、性别)放心冗余;频繁变化的字段(VIP 等级、会员积分)宁可不冗余,因为变化一次要全表重写。

高基数维度(百万级以上)要独立一张表。比如商品维度、设备维度不能直接冗余到用户维度里,因为会把维度表炸成事实表。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

面试进入 20 分钟后通常会拐到建模话题,第一道必问的建模题就是维度表设计。面试官会抛出一个场景(比如『设计一张用户维度表』),看你怎么决策字段怎么放、冗余到哪、粒度多粗。回答『就放用户基础信息』太平,回答『所有能想到的字段都冗余进来』太粗。真正考的是你对『冗余成本 vs 查询性能』这个 trade-off 的理解。

业务场景:我在某短视频平台的用户维度表设计

我早年在某短视频平台做用户维度表时,核心诉求有两个:(1)80% 的看板都要按 country/city/age_group/device_model 分析,这些字段要能秒级 group by;(2)用户等级每周变一次,不能因为等级变了就重刷历史数据。所以我把字段分成了三类:冗余、按日快照、不冗余。

-- 用户维度表 dim_user
CREATE TABLE dim_user (
  uid BIGINT COMMENT '用户ID(主键)',
 
  -- 【冗余类:稳定字段,90%+ 查询用到】
  gender STRING           COMMENT '性别(注册时定,不变)',
  register_country STRING COMMENT '注册国家',
  register_city STRING    COMMENT '注册城市',
  age_group STRING        COMMENT '年龄段(10岁区间)',
  first_device_model STRING COMMENT '首次使用设备型号',
 
  -- 【快照类:会变但要保留历史】
  level INT               COMMENT '当前等级(每周变,用快照)',
  vip_status STRING       COMMENT '当前VIP状态',
  dt STRING               COMMENT '快照日期,分区键'
) PARTITIONED BY (dt STRING);
 
-- 【不冗余类】:
-- last_login_time、current_coins、friend_count 等
-- 这些字段变化太频繁,放事实表或每次 join

分区表每天一份快照是我们常用的做法。下游要看『2024-03-15 的用户分布』就读 dt=2024-03-15 的分区,要看『当前分布』就读最新分区。这样等级变了只影响新快照,历史数据永远不动。

段落一:冗余 vs 不冗余的三条判断标准

判断维度适合冗余不适合冗余
使用频率80%+ 的下游查询会用到偶尔用到,2-3 个报表而已
变化频率月级以上才变天级甚至小时级变化
基数低基数(几十到几万)高基数(百万以上)
是否有历史诉求不需要保留历史需要历史(用 SCD 方案)
字段计算成本原值直接存需要复杂计算派生的

经验规则:满足 4-5 条对勾的字段就冗余,满足 2-3 条的字段放快照维度,只满足 1 条或 0 条的字段不冗余。

段落二:缓慢变化维(SCD)的四种方案

维度字段会变怎么办?这就是 SCD(Slowly Changing Dimension)。四种标准方案,面试常考 SCD Type 1 / Type 2。

SCD 类型做法优点缺点
Type 1直接覆盖,不保留历史简单、表小丢失历史
Type 2加 start_dt/end_dt 开新行完整历史表膨胀
Type 3加历史字段(prev_value)保留上一版只能留一版历史
Type 6(快照)每天一个快照分区最直观存储成本高

实务里最常用的是 Type 2(拉链表)和 Type 6(快照)。快照简单但存储贵(每天重复存 99% 的相同数据),拉链表存储省但查询复杂。互联网公司普遍用快照——存储便宜、查询简单,代价可控。拉链表第 9 篇会专门讲。

段落三:高基数维度必须独立

新人最爱犯的错是把『商品名 / 设备型号 / 渠道名』直接冗余到 dim_user。这种做法的问题是:维度表会被『用户 × 商品 × 设备』笛卡尔炸开,维度表变成事实表。

-- ❌ 错误:把商品信息冗余到用户维度
CREATE TABLE dim_user_BAD (
  uid BIGINT,
  last_buy_sku_name STRING,      -- 商品名,100 万个 SKU
  last_buy_sku_category STRING,   -- 品类
  last_buy_sku_brand STRING,      -- 品牌
  -- 每个用户的这三个字段都在频繁变化
  -- 维度表膨胀到事实表规模
);
 
-- ✅ 正确:商品维度独立
CREATE TABLE dim_sku (
  sku_id BIGINT PRIMARY KEY,
  sku_name STRING, category STRING, brand STRING
);
 
-- 事实表只存外键
CREATE TABLE fact_order (
  order_id BIGINT, uid BIGINT, sku_id BIGINT, ...
);
 
-- 查询时 JOIN 维度
SELECT u.country, s.brand, SUM(o.amount)
FROM fact_order o
JOIN dim_user u USING (uid)
JOIN dim_sku s USING (sku_id)
GROUP BY u.country, s.brand;

段落四:维度冗余的真实收益计算

什么时候『冗余划算』?一个简单的判断:看冗余后节省的 JOIN 成本 vs 增加的存储成本。我在某短视频平台做过一次测算:把 country 冗余到事实表里让 60% 的报表不需要 JOIN dim_user,节省的计算资源远大于多存 30GB 的代价。

Plain

冗余决策成本分析(某短视频平台真实测算):
 
冗余 country 字段到 fact_order:
  【成本】存储成本:多 30GB / 天 × 365 天 × 低价 S3 ≈ $80/年
  【收益】60% 报表少一次 JOIN dim_user
         计算成本节省 ≈ $1200/年
  结论:冗余
 
冗余 last_login_time 到 fact_order:
  【成本】每小时变一次,要每小时刷 fact_order 维度列
         计算成本增加 ≈ $2000/年
  【收益】只有 2 个报表用到
  结论:不冗余,JOIN 即可

面试高频追问

追问一:你说 80% 查询用到才冗余,怎么知道是 80%?

真实项目里有两种方法:(1)查询日志统计——把最近一个月所有 SQL 日志扒出来,grep 字段名统计出现频率;(2)业务访谈——和主要 BI / 业务方对齐他们常用的分析维度。一般结合起来用。如果是新业务没日志,就按业务方的核心诉求先粗放冗余,上线后再根据实际查询量迭代。

追问二:维度表的主键选什么?自增 ID 还是业务主键?

经典问题。Kimball 教科书推荐代理键(surrogate key,自增 ID),理由是业务主键可能变化。但实务里数仓里直接用业务主键(uid、sku_id)更多,因为事实表关联维度更直观。我的经验是:对于会频繁合并的维度(用户合并、商品合并)用代理键,对于稳定的维度(国家编码、货币编码)直接用业务主键。

追问三:维度表多大就算大?怎么加速查询?

一般 500MB 以上就开始影响性能。加速手段:(1)Broadcast 维度——把小维度表 Broadcast 到所有 executor;(2)冗余高频字段到事实表——减少 JOIN;(3)维度列裁剪——JOIN 时只 SELECT 需要的维度列,不要 JOIN 整张维度表。我在某出海硬件品牌把 Broadcast 阈值从 10MB 调到 200MB,Join 自动变 Broadcast,作业时间减少 40%。

实战踩坑:维度属性变更没做 SCD 的惨案

我在某短视频平台早期有个教训:用户的 country 字段直接覆盖更新(Type 1),没保留历史。结果有一次用户批量『地区迁移』(后台把一批用户的 country 从 TW 改到 CN),第二天业务方炸了——过去 6 个月的地区分布报表全变了,因为历史事实表 JOIN 当前维度表,历史数据被『重分地区』了。

事后改造:dim_user 从 Type 1 改成 Type 6(快照),每天一个快照分区,事实表 JOIN 维度时用『事实发生当天的维度快照』。改造后历史数据稳定,新数据按新口径。教训是:只要业务维度有可能变更,宁可一开始就用快照或拉链,不要 Type 1 覆盖。

-- ✅ 事实表 JOIN 快照维度的标准写法
SELECT f.*, d.country
FROM fact_order f
JOIN dim_user d
  ON f.uid = d.uid
  AND f.dt = d.dt;  -- 关键:按事实发生日关联维度快照

总结

维度表设计的核心是『trade-off 表达能力』。面试时要能说出三件事:(1)冗余的判断标准(频率、变化、基数);(2)变化维度的 SCD 方案选型;(3)一个真实的冗余决策案例(划算 vs 不划算)。能讲清楚这三点,这题就是资深水平。

下一篇:『宽表设计——什么时候该建宽表?什么时候该拆?』——建模题第二道。宽表是一把双刃剑,用得好是王牌,用不好是黑洞。

你们公司的用户维度表有多少字段?有没有那种『传了三任 TL 都没人敢删』的神秘字段?

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器