一、多对多关系下“星型模型”的限制
1、无法处理类目层级过深
结构限制:星型模型的维度表是扁平的单表,一条商品记录只能对应一个类目值(或固定数量的几个字段)。
使用星型模型抽象只能将这一串抽象为一个类目,(如:“食品/饮料/碳酸饮料/可乐”)但这就会导致后面想统计其中任意一个层级时很困难
| |
如果用星型模型来设计:
商品维度表通常是扁平化的,每个商品只有一条记录:
| |
致命问题出现了:
这个表结构只能存储一个类目路径!
如果硬要存多个,只能这样设计:
| |
这种设计的弊端:
不灵活:如果商品需要第4个类目,必须修改表结构(ALTER TABLE)。
查询困难:要找出所有属于“健康饮品”的商品,需要这样写:
1 2 3 4SELECT * FROM dim_product_star_bad WHERE main_category_path LIKE '%健康饮品%' OR promo_category_path LIKE '%健康饮品%' OR scene_category_path LIKE '%健康饮品%';性能极差,且难以维护。
数据冗余:类目路径字符串被大量重复存储。
2、无法表达多对多的关系
在电商平台中,商品分类是复杂,这个商品可能同时属于以下类目:
主类目:
食品 -> 乳制品 -> 牛奶 -> 纯牛奶促销类目:
食品 -> 饮料 -> 健康饮品(平台做活动时把它归到这里)场景类目:
早餐食材(早餐专题活动)
类目复杂
类目层数不固定
层数不固定
类目关系会动态变化(今天在A类目,明天可能加到B类目)
冗余巨大
- “食品/饮料/碳酸饮料” 重复出现几百万次
类目改名要更新全表
- 比如“碳酸饮料”改名为“气泡饮料”
- 数百万行要 update ❌
3、多类目统计困难
假设我们有销售事实表:
| |
需求:统计每个类目的总销售额。
在星型模型中,由于一个商品只关联一个主类目(即使它实际属于多个类目),你只能:
要么只按主类目统计,忽略其他类目 → 数据不准确
要么把销售额重复计算到多个类目 → 数据虚高
示例代码(错误统计):
| |
结果:要么少算,要么多算,永远无法得到准确数据!
⚠️ 问题总结:
结构限制:星型模型的维度表是扁平的单表,一条商品记录只能对应一个类目值(或固定数量的几个字段)。
关系限制:它天然假设事实表(销售记录)和维度表(商品)是一对多关系,然后通过商品维度表连接到类目维度表。这种链式关系无法表达“一个商品→多个类目”。
业务现实:在真实电商中,商品分类是复杂的:
一个商品可以属于多个类目
不同类目可能有不同权重
类目关系会动态变化(今天在A类目,明天可能加到B类目)
数据准确性:星型模型要么丢失关系(只存一个主类目),要么重复计算(存多个字段但无法合理分摊指标)
二、解决方案:雪花模型 + 桥接表
现在看雪花模型如何优雅地解决这个问题:
初级解决方案
步骤1:规范化设计
| |
步骤2:插入数据
| |
步骤3:准确统计各类目的销售额
| |
结果示例:
| 类目名称 | 分摊后销售额 |
|---|---|
| 纯牛奶 | 6000元(10000元×0.6) |
| 健康饮品 | 3000元(10000元×0.3) |
| 早餐食材 | 1000元(10000元×0.1) |
总销售额仍然是10000元,但被合理地分摊到了各个类目!
拓展SCD概念
SCD(Slowly Changing Dimensions,缓慢变化维)是指数据仓库中维度表的数据会随时间缓慢变化的维度。这里的"缓慢"是相对于事实表而言的——事实表数据变化速度快(如交易记录),而维度表数据变化较慢(如用户信息、产品信息)。
典型示例:用户维度表中,用户的出生日期、住址、手机号等信息可能随时间发生变化
出现原因
业务数据的自然演变:
业务数据更新:用户修改个人信息(如搬家后更新地址)
数据修正:发现历史数据错误需要更正(如身份证号录入错误)
状态变更:产品价格调整、员工职位变动等
属性扩展:业务需求变化导致维度属性增加
如果不妥善处理这些变化,数据分析会出现以下问题:
历史事实与当前维度不匹配
无法追踪历史变化趋势
跨时间分析结果失真
应对措施
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 错误数据修正 | Type 1 | 直接覆盖错误值 |
| 用户地址追踪 | Type 2(拉链表) | 完整历史分析 |
| 产品价格变化 | Type 2或Type 3 | 根据分析需求选择 |
| 员工职位变动 | Type 2 | 支持晋升路径分析 |
桥接表的时间化扩展
问题:今天在A类目,明天可能加到B类目
标准的桥接表需要增加时间有效性字段来处理关系的动态变化:
| |
实际案例:跟踪商品类目的历史变化
假设"蒙牛纯牛奶250ml"的类目关系变化如下:
2024-01-01 到 2024-03-31:属于
纯牛奶类目2024-04-01 开始:被重新分类到
低温牛奶类目2024-05-15 到 2024-06-15:添加了
夏季饮品的促销类目
步骤1:初始化数据(2024-01-01)
| |
步骤2:类目变更(2024-04-01)
注意:我们不能直接更新原有记录,而是需要:
关闭原有关系的有效期
添加新的关系记录
| |
步骤3:添加促销类目(2024-05-15)
| |
步骤4:查看时间线上的关系变化
现在桥接表的状态:
| product_id | category_id | relation_type | weight | start_date | end_date |
|---|---|---|---|---|---|
| 1001 | 4 | main | 1.0 | 2024-01-01 | 2024-03-31 |
| 1001 | 8 | main | 1.0 | 2024-04-01 | 9999-12-31 |
| 1001 | 9 | promo | 0.3 | 2024-05-15 | 2024-06-15 |
这才是桥接表的强大之处!我们可以查询任意时间点的商品-类目关系:
适用场景
场景1:查询2024-02-15(春节促销)的商品类目
| |
场景2:查询2024-05-20(夏季促销)的商品类目
| |
场景3:统计2024年Q1(1-3月)各类目的销售额
| |
如何处理复杂的权重变化?
有时候,商品在不同类目间的权重也会随时间变化。比如:
1-3月:主类目权重100%
4月开始:主类目60%,促销类目40%
这需要在变更时创建两条新记录:
| |
为什么桥接表的时间化拓展的设计是优秀的?
1. 历史可追溯
可以查询任意历史时间点的类目归属关系。比如审计时想知道"2023年双十一期间,这个商品属于哪个类目"。
2. 时间一致性
确保销售事实与当时的维度关系一致。今天做的报表和历史报表在逻辑上是一致的。
3. 变更无影响
增加新类目:直接插入新记录
移除旧类目:将原记录的
end_date设为昨天修改权重:关闭旧记录,插入新权重的新记录
所有历史报表不受影响
4. 支持复杂的业务场景
季节性类目(如"夏季特饮"只存在6-8月)
促销活动类目(如"双十一专区"只在活动期间有效)
测试分组(A/B测试时,不同用户看到不同类目)
实际查询模板
模板1:获取当前有效的类目关系
| |
模板2:生成类目历史变化时间线
| |
三、完整的数仓分层设计
以下是基于商品订单的完整分层设计:
1. ODS层(操作数据存储)
目标:原样同步,保留历史,快速恢复
| |
2. DWD层(数据仓库明细层)
目标:清洗、规范化、构建维度模型
| |
3. DWS层(数据仓库汇总层)⭐
目标:反规范化,构建星型宽表,提升查询性能
| |
4. ADS层(应用数据服务)
目标:面向应用,直接供给报表/接口
| |
数据处理流程示例
从DWD到DWS的ETL过程
| |
查询性能对比
雪花模型查询(DWD层)
| |
星型模型查询(DWS层)
| |
分层架构总结
| 分层 | 模型 | 核心目标 | 数据特征 | 用户 | 存储策略 |
|---|---|---|---|---|---|
| ODS | 原始模型 | 数据同步、历史备份 | 原始数据、无清洗 | 数据工程师 | 全量/增量 |
| DWD | 雪花模型 | 数据质量、一致性、灵活性 | 规范化、明细数据 | 数据开发 | 分区+列存 |
| DWS | 星型模型 | 查询性能、易用性 | 反规范化、汇总数据 | 分析师/业务 | 宽表+索引 |
| ADS | 应用模型 | 业务适配、接口性能 | 高度聚合、应用定制 | 应用/报表 | 缓存+分区 |
关键设计原则:
DWD层保持灵活性:用雪花模型支持多对多、历史变化、层级可变
DWS层追求性能:用星型模型预关联、预计算,空间换时间
层层递进:ODS → DWD(清洗) → DWS(汇总) → ADS(应用)
适度冗余:在DWS层合理冗余维度属性,避免过度规范化
数据复用:DWS层宽表可被多个ADS应用复用