4. 数据模型设计(上)
好,咱们进入数据集市最核心的部分——模型设计。说实话,我见过太多项目在建模阶段图省事,结果后面ETL跑不动、业务方看不懂、需求一变更就崩盘。维度建模不是银弹,但它是企业级数据集市最成熟、最经得起折腾的一套方法论。
这一章我们先啃理论,下一章再讲实战落地。嗯,先搭好骨架,再填肉。
4.1 维度建模的核心思想
维度建模说白了就两样东西:事实和维度。事实是“发生了什么”,维度是“谁、在哪、什么时候、什么产品”。
我习惯把事实表想象成账本,维度表就是字典。账本记录每一笔交易,字典告诉你这笔交易是谁买的、在哪个门店、卖的是什么商品。分开存,查起来快,改起来也方便。
核心原则:事实表尽量细,维度表尽量宽。细到原子粒度,宽到能覆盖所有分析角度。
4.2 星型模型 vs 雪花模型
这两个是维度建模最经典的两种形态。我刚开始做数仓时也纠结过选哪个,后来踩过坑才明白——没有的好坏,只有合不合适的场景。
4.2.1 星型模型
星型模型就是一张事实表在中间,周围一圈维度表直接连过来,像星星一样辐射出去。维度表不继续往下拆,每个维度都是扁平的一张表。
优点:查询快、理解简单、SQL好写。业务方自己都能拖拽出报表来。
缺点:数据冗余。比如“产品类别”这种属性,如果产品维度表里存了类别名称,那每个产品都重复存一遍。
我的经验:90%的数据集市场景,星型模型就够了。别为了省那点存储空间去搞雪花,存储便宜,人力和查询性能贵。
4.2.2 雪花模型
雪花模型就是把维度表继续规范化拆开。比如产品维度拆成产品表、类别表、品牌表,一层层连下去。
优点:数据一致性好,更新维护方便。改一个类别名称,只改一张小表就行。
缺点:查询要关联多张表,性能下降,SQL复杂度飙升。业务方基本看不懂。
| 对比维度 | 星型模型 | 雪花模型 |
|---|---|---|
| 查询性能 | 高(关联少) | 低(关联多) |
| 数据冗余 | 高 | 低 |
| 维护成本 | 低 | 高 |
| 业务理解 | 容易 | 困难 |
| 适用场景 | OLAP查询、报表 | 数据治理、ETL中间层 |
避坑指南:我曾经在一个金融项目中强行用雪花模型,结果业务方每次提需求都要拉着我解释表结构。后来全部改回星型,虽然存储多了30%,但开发效率提升了不止一倍。
4.3 事实表设计
事实表是数据集市的“心脏”。你想想看,所有分析最终都要落到事实表上。事实表设计错了,后面全白搭。
事实表分三种类型,每种解决不同的问题。我按使用频率从高到低来讲。
4.3.1 事务事实表
这是最常用的一种。记录每一笔业务事件,比如每笔订单、每次登录、每笔转账。一行就是一次事件。
特点:
- 粒度最细,一行一个事件
- 只记录发生那一刻的事实
- 数据量最大,但最灵活
-- 订单事务事实表示例
CREATE TABLE fact_order (
order_sk BIGINT, -- 代理键
order_id VARCHAR(50), -- 业务主键
customer_sk INT, -- 客户维度外键
product_sk INT, -- 产品维度外键
store_sk INT, -- 门店维度外键
date_sk INT, -- 日期维度外键
order_amount DECIMAL(10,2),-- 订单金额
order_qty INT, -- 订单数量
discount_amt DECIMAL(10,2) -- 折扣金额
);
我的习惯:事务事实表一定要保留业务主键(比如order_id),方便溯源和对账。别为了省空间把业务主键丢了,后面查数据问题时会哭的。
4.3.2 周期快照事实表
这种表记录某个时间点的状态。比如每天统计一次库存余额、每月统计一次账户余额。它不关心中间发生了什么,只关心“到这一刻为止是什么样”。
适用场景:
- 库存快照(每天盘点一次)
- 账户余额(每月初记录)
- 用户等级(每周统计)
-- 库存周期快照事实表示例
CREATE TABLE fact_inventory_snapshot (
date_sk INT, -- 快照日期
product_sk INT, -- 产品维度外键
warehouse_sk INT, -- 仓库维度外键
onhand_qty INT, -- 当前库存量
reserved_qty INT, -- 已预留量
available_qty INT -- 可用量
);
关键点:周期快照的粒度是“时间周期 + 维度组合”。比如每天每个产品每个仓库一条记录。周期越长,数据量越小,但分析精度也越低。
4.3.3 累积快照事实表
这种表记录一个完整流程从开始到结束的累计状态。比如一笔订单从下单、支付、发货、到签收的全过程。一行就是一个流程实例,字段会随着流程推进不断更新。
特点:
- 一行代表一个完整流程
- 字段会更新(不像事务事实表只追加)
- 适合分析流程耗时、瓶颈环节
-- 订单累积快照事实表示例
CREATE TABLE fact_order_accumulating (
order_sk BIGINT,
order_date_sk INT, -- 下单日期
pay_date_sk INT, -- 支付日期(初始为空)
ship_date_sk INT, -- 发货日期(初始为空)
receive_date_sk INT, -- 签收日期(初始为空)
order_amount DECIMAL(10,2),
-- 时间间隔字段(ETL计算)
pay_to_ship_days INT,
ship_to_receive_days INT
);
避坑指南:我曾经做过一个物流项目,累积快照表里放了十几个时间字段,结果ETL脚本写得像意大利面条。后来我定了个规矩——累积快照表的时间字段不要超过8个,超过就拆成多个事实表或用明细表补充。
4.4 维度表设计
维度表是事实表的“翻译官”。事实表里存的都是ID,维度表告诉你这些ID是什么意思。维度设计得好,业务方自己就能看懂数据。
4.4.1 退化维度
退化维度就是把维度属性直接塞进事实表里。比如订单号、发票号、物流单号——这些维度通常只有一个属性,单独建一张维度表反而浪费。
什么时候用?
- 维度只有一个属性(比如订单号)
- 维度属性很少(比如发票号+发票类型)
- 查询时经常需要这个字段做过滤或分组
-- 退化维度示例:订单号直接放在事实表中
CREATE TABLE fact_order (
order_id VARCHAR(50), -- 退化维度
customer_sk INT,
product_sk INT,
amount DECIMAL(10,2)
-- 不需要单独建dim_order表
);
我的经验:退化维度用得好,能减少30%以上的表关联。但别滥用——如果订单号还需要关联订单的其他属性(比如订单来源、订单类型),那就老老实实建维度表。
4.4.2 角色扮演维度
同一个维度表,在事实表里扮演多个角色。比如“日期”维度,在订单事实表里可能是“下单日期”、“支付日期”、“发货日期”。你不可能为每个角色建一张日期表,太浪费了。
实现方式:在事实表里用不同的外键字段指向同一张维度表。
-- 角色扮演维度示例:日期维度扮演多个角色
CREATE TABLE fact_order (
order_sk BIGINT,
order_date_sk INT, -- 指向dim_date,角色:下单日期
pay_date_sk INT, -- 指向dim_date,角色:支付日期
ship_date_sk INT, -- 指向dim_date,角色:发货日期
customer_sk INT,
amount DECIMAL(10,2)
);
-- 查询时用别名区分角色
SELECT
d1.date_str AS order_date,
d2.date_str AS pay_date,
d3.date_str AS ship_date
FROM fact_order f
JOIN dim_date d1 ON f.order_date_sk = d1.date_sk
JOIN dim_date d2 ON f.pay_date_sk = d2.date_sk
JOIN dim_date d3 ON f.ship_date_sk = d3.date_sk;
核心要点:角色扮演维度是复用,不是复制。一张维度表可以同时服务多个角色,但每个角色在事实表里要有独立的字段。别想着用同一个字段存多个日期,那是在给自己挖坑。
4.5 本章知识体系总览
下面这张图把维度建模的核心结构串起来了。我建议你保存下来,后面做设计时对照着看。
这张图把三种事实表和两种维度设计方式都串起来了。你注意看,事实表是中心,维度表是外围。事务、周期、累积三种事实表解决不同粒度的分析需求;退化维度和角色扮演维度解决维度表的设计效率问题。
总结一句话:维度建模不是死记硬背的教条,而是一套“事实+维度”的思维框架。你掌握了这个框架,后面不管遇到什么业务场景,都能快速拆解出模型结构。