宽表设计原则:高扇出与低扇出设计、适度冗余原则、避免过度关联
宽表设计,说白了就是在「查询快」和「维护难」之间找平衡。我做了这么多年数据仓库,见过太多团队一上来就搞几百列的超级宽表,结果跑任务跑到凌晨三点还在报错。嗯,今天咱们就聊聊宽表设计的三个核心原则,这些都是我在项目里踩过坑之后总结出来的。
高扇出与低扇出设计
先说说扇出这个概念。你想想看,一张事实表里,一条记录能关联出多少条明细?这就是扇出率。
- 高扇出:一条主记录关联出多条子记录。比如一个订单对应多个商品明细。
- 低扇出:一条主记录只关联一条或零条子记录。比如一个用户对应一个注册信息。
我在项目中遇到过最典型的案例:某电商团队把订单表和商品明细表直接做全外连接,结果订单表 1000 万行,商品明细表 3000 万行,一关联变成了 8000 万行。为什么?因为一个订单平均有 8 个商品,扇出率是 8 倍。
核心原则:高扇出场景必须做聚合或分桶,低扇出场景可以直接关联。
我建议这样处理:
- 高扇出场景:先按主键聚合,再关联。比如先算每个订单的总金额、商品数量,再关联到订单宽表。
- 低扇出场景:直接 left join,性能影响不大。
- 混合场景:把高扇出和低扇出拆成两张宽表,不要混在一起。
| 场景 | 扇出率 | 推荐做法 | 我踩过的坑 |
|---|---|---|---|
| 订单-商品明细 | 高(1:N) | 先聚合再关联 | 直接关联导致数据膨胀 8 倍 |
| 用户-注册信息 | 低(1:1) | 直接 left join | 没问题,放心用 |
| 店铺-商品 | 中(1:几十) | 按业务场景拆分 | 混在一起查询极慢 |
小技巧:建宽表前先跑一下 SELECT count(*)/count(DISTINCT 主键) FROM 明细表,算算平均扇出率。超过 5 倍就要警惕了。
适度冗余原则
宽表之所以叫宽表,就是因为允许冗余。但「适度」这两个字是关键。我曾经见过一张宽表,把用户的所有历史地址都塞进去,结果一张表 200 列,80% 的字段都是空的。
我个人习惯遵循三个判断标准:
- 查询频率:这个字段每天被查询多少次?如果一周都用不到一次,别放进来。
- 更新频率:字段多久变一次?比如用户等级可能每月变,但性别基本不变。高频更新的字段放进来,维护成本会很高。
- 存储成本:冗余字段占多少空间?字符串类型尤其要注意,一个
varchar(500)的备注字段,1000 万行就是 5GB。
避坑指南:我曾经把一个「用户最后登录 IP」字段冗余进订单宽表,结果每天凌晨 ETL 要跑 2 小时去更新这个字段。后来发现业务方根本不用这个字段做分析,白白浪费计算资源。
我建议的冗余策略:
- 只冗余稳定字段:比如用户注册时间、商品分类名称。
- 控制冗余层级:最多冗余两级关联。比如订单宽表可以冗余用户名称和商品名称,但不要冗余用户的地址再冗余地址的省份。
- 设置冗余上限:单表字段数建议控制在 50 列以内。超过这个数,查询优化器都开始犯迷糊。
避免过度关联
这个原则其实是对前两个原则的补充。过度关联指的是:一张宽表里关联了太多张源表,导致维护成本和查询复杂度双双飙升。
我记得有个项目,业务方要求一张「全链路宽表」,从用户点击到下单到支付到物流,全部塞进一张表。结果呢?关联了 12 张表,ETL 脚本写了 800 行,每次跑数都要 4 小时。而且只要其中一张源表结构变了,整张宽表就崩。
为什么会这样?因为关联的表越多,数据一致性越难保证。你想想看,12 张表的更新频率、数据质量都不一样,强行关联在一起,最后出来的数据你敢信吗?
我的经验法则:一张宽表关联的源表不要超过 5 张。超过这个数,拆成两张宽表或者用视图来解耦。
具体怎么做?
- 按业务域拆分:交易域一张宽表,用户域一张宽表,商品域一张宽表。需要跨域分析时,用星型模型或者物化视图。
- 用中间表解耦:如果非要关联 6 张以上的表,先做一层中间表,把关联逻辑拆成两步。比如先做「订单-用户」中间表,再做「订单-商品」中间表,最后合并。
- 设置关联深度上限:最多关联 3 层。比如订单→用户→用户等级,这是 2 层。订单→用户→用户地址→地址省份,这是 3 层,已经到极限了。
实用建议:每次建宽表前,先画一张关联关系图。如果发现关联线超过 5 条,停下来想想能不能拆。我团队现在把这个写进了开发规范,谁违反谁请下午茶。
最后总结一下这三个原则的关系:高扇出与低扇出设计解决的是「数据膨胀」问题,适度冗余原则解决的是「查询效率」问题,避免过度关联解决的是「维护成本」问题。三者缺一不可,但优先级可以按场景调整。我个人习惯是:先控制扇出,再考虑冗余,最后检查关联深度。按这个顺序走,宽表设计基本不会出大问题。