外观
宽表 vs 星型何时用
难度与前置
难度:进阶
前置:知道事实表/维表;写过 DWS 汇总或 ADS 报表 SQL;了解 数仓分层。
读完能做什么:按查询形态与变更频率在宽表、星型(及轻度冗余)之间选型,用成本与正确性清单验收,避免「一切皆宽表」或「一切皆星型」。
适用场景
- 主题域建模评审:订单、用户、流量是否打成一张宽表
- ADS / 报表层 SQL 经常 5+ 表 Join,BI 同学抱怨难写
- 维属性频繁变更,宽表回刷成本失控
- 面试要讲清「维度建模 vs 宽表」的工程取舍,而不是背定义
不适用:
- 仅讨论理论范式(3NF)而无查询与运维约束
- 实时流内状态 Join(见 Flink 维表 / 状态 Join(专篇待开;状态侧可先看 State TTL))
- 已有稳定星型且无性能/维护痛点——勿为改而改
真实场景(一条)
某电商 ADS「订单明细宽表」把用户等级、商品类目三级、渠道、优惠券规则共 40+ 维属性打平。BI 查询快,但维表类目每周调整:宽表全量回刷 6 小时,且历史订单「下单时类目」与「当前类目」口径混乱,导致同比异常。
改造:DWD 保留星型(事实 + 维快照键);ADS 仅对高频报表物化「查询宽表」,并固化「下单时维」快照列;低频分析走星型 Join。回刷缩到受影响分区,口径争议下降。
怎么做(步骤清单)
1. 先对齐两种形态在干什么
| 形态 | 典型落点 | 优点 | 代价 |
|---|---|---|---|
| 星型 / 雪花 | DWD~DWS | 维变更局部、口径清晰、存储省 | 查询多 Join、对分析师不友好 |
| 宽表(打平) | DWS~ADS | 查询简单、预关联性能稳 | 维变更要回刷、膨胀、口径易糊 |
| 轻度冗余 | 中间层 | 保留键 + 少量高频属性 | 需约定「快照时点」 |
口诀:稳定高频查询 → 可宽表;易变维 / 多口径 → 星型 + 按需物化。
2. 用「查询画像」决定默认形态
收集 2~4 周真实查询(或 BI 看板 SQL),填表:
| 维度 | 问题 | 倾向 |
|---|---|---|
| Join 深度 | 是否经常 >3 表 | 深 → 考虑物化宽表 |
| 过滤列 | 是否总用同一批维属性 | 是 → 可打进宽表 |
| 维变更频率 | 类目/组织是否周级变 | 高 → 星型或「下单时快照」 |
| 回刷窗口 | SLA 能否扛全量重算 | 否 → 勿大宽表绑易变维 |
| 消费者 | BI 自助 vs 固定报表 | 自助且复杂 → 星型文档 + 语义层 |
sql
-- 示意:从查询日志粗看「常 Join 的维」
SELECT dim_table, COUNT(*) AS cnt
FROM query_audit
WHERE dt BETWEEN '${d7}' AND '${d1}'
AND sql_text RLIKE 'join\\s+dim_'
GROUP BY dim_table
ORDER BY cnt DESC
LIMIT 20;3. 分层落位建议(与分层对齐)
- ODS:贴源,不纠结宽/星
- DWD:优先星型事实 + 维(或维键);事务过程保持可解释
- DWS:轻度汇总;可冗余稳定维属性(如城市等级)
- ADS:面向消费的宽表 / 主题表;一张表服务一类看板
4. 宽表设计清单(决定做时)
- 固化时点:每条事实带「下单时 / 支付时」维快照,禁止默默用「当前维」覆盖历史
- 列裁剪:只打高频列;长尾属性走星型或侧车表
- 主键与粒度:一行一业务粒度(订单行 / SKU 日),写进表注释
- 分区:按业务日;维变更回刷只动受影响分区
- 血缘:宽表 ← 事实 + 哪些维版本,登记在元数据
sql
-- ADS 宽表示意:事实 + 下单时维快照
INSERT OVERWRITE TABLE ads_order_wide_di PARTITION (dt = '${biz_date}')
SELECT
o.order_id,
o.user_id,
o.sku_id,
o.pay_amt,
o.order_time,
u.user_level_at_order, -- 下单时快照,不是当前等级
s.cate_l1_at_order,
s.cate_l2_at_order,
c.channel_name_at_order
FROM dwd_order_di o
LEFT JOIN dim_user_snap_di u
ON o.user_id = u.user_id AND u.dt = '${biz_date}' AND u.snap_type = 'order_time'
LEFT JOIN dim_sku_snap_di s
ON o.sku_id = s.sku_id AND s.dt = '${biz_date}' AND s.snap_type = 'order_time'
LEFT JOIN dim_channel_snap_di c
ON o.channel_id = c.channel_id AND c.dt = '${biz_date}'
WHERE o.dt = '${biz_date}';5. 星型设计清单(决定做时)
- 事实表只留外键 + 度量 + 退化维(订单号等)
- 维表独立;缓慢变化用 快照 vs 拉链 / SCD2
- 提供语义层 / 视图降低 BI Join 成本,而不是强行宽表
- 热点小维可 Broadcast,避免每次 Shuffle(见 Spark/Hive Join 文)
sql
-- 星型查询示意:事实 + 维,口径在维表版本上控制
SELECT
d.cate_l1,
SUM(f.pay_amt) AS pay_amt
FROM dwd_order_di f
JOIN dim_sku_zf d
ON f.sku_id = d.sku_id
AND f.order_time >= d.start_time
AND f.order_time < d.end_time
WHERE f.dt BETWEEN '${d_start}' AND '${d_end}'
GROUP BY d.cate_l1;6. 混合策略(多数团队的现实答案)
| 模式 | 做法 |
|---|---|
| 双轨 | DWD 星型为真源;ADS 物化 3~5 张高频宽表 |
| 延迟打平 | 夜间物化宽表;白天分析走星型 |
| 属性分级 | P0 属性进宽表;P1 按需 Join;P2 仅主题域 |
| 变更隔离 | 易变组织树不进订单宽表,单独组织主题表 |
7. 决策树(评审可投影)
text
查询是否固定且高频?
├─ 否 → 星型 + 文档/语义层
└─ 是 → 维是否周级以上变更?
├─ 是 → 宽表只打「事件时快照」;易变维勿绑死
└─ 否 → 可打平进 ADS 宽表;约定回刷范围
存储/回刷 SLA 是否吃得消?
└─ 否 → 缩列或拆主题宽表,禁止「万能一张表」8. 与指标口径的衔接
宽表一旦把指标预计算进去(如「优惠后 GMV」),必须挂 指标口径一致性 文档:公式、是否含退款、时区。宽表不是口径的替代品。
常见坑(至少牢记这些)
一切皆宽表
维一变全链路回刷;历史口径被「当前维」污染,同比失真。一切皆星型却无语义层
BI 每人写一套 Join,出现三套「GMV」;性能与口径双崩。宽表用当前维覆盖历史
类目调整后重刷把去年订单改到新类目,业务以为「数据错了」。粒度没写清
订单头与订单行混在一张「宽表」,SUM 直接翻倍。雪花过深当星型
五层维链 Join,优化器与人都痛苦;应扁平化常用路径或物化。为「消除冗余」反范式清洗掉快照
存储省了,审计与监管要的「当时画像」没了。
如何验收(可量化)
- [ ] 选型文档写明:默认形态、例外宽表清单、快照时点约定
- [ ] 宽表行数 = 声明粒度计数(抽样对账)
- [ ] 维变更演练:只回刷约定分区,耗时 < SLA
- [ ] 同口径指标:宽表结果 vs 星型 Join 结果偏差在阈值内(如金额相对差 < 0.1%)
- [ ] BI 高频看板 SQL 平均 Join 表数下降或 P95 耗时下降(有基线)
sql
-- 粒度验收:宽表一行一订单行
SELECT
COUNT(*) AS wide_cnt,
COUNT(DISTINCT order_id, sku_id) AS grain_cnt
FROM ads_order_wide_di
WHERE dt = '${biz_date}';
-- 期望 wide_cnt = grain_cnt
-- 口径对账:宽表 GMV vs 星型
SELECT 'wide' AS src, SUM(pay_amt) AS gmv FROM ads_order_wide_di WHERE dt = '${biz_date}'
UNION ALL
SELECT 'star', SUM(f.pay_amt)
FROM dwd_order_di f
WHERE f.dt = '${biz_date}';bash
# 回刷演练记录(示意)
echo "biz_date,mode,duration_min,partitions_touched"
# 2026-09-20,cate_change_restate,45,7相关面试题
占位:与题库小程序对齐后补链接(建议关联:维度建模、宽表利弊、SCD、指标口径、分层落位)。
改写说明:结构与判断标准参考 Wood 原创数仓/面试文,已改写成可执行清单;未包含未公开公司数据与内部表名。