事务事实表设计:设计原则、行来源与典型场景
事务事实表,说白了就是记录「每一笔业务事件」的明细表。我做了这么多年数据仓库,接触最多的就是它。你想想看,销售订单、银行交易流水、网站点击日志……这些都属于事务事实表。
今天咱们就把它拆开揉碎了讲清楚。我会结合自己踩过的坑,把设计原则、行来源和两个典型场景说透。
一、设计原则:四个必须守住的红线
事务事实表的设计,我总结出四条铁律。每条都是拿实际教训换来的。
| 原则 | 核心要求 | 我踩过的坑 |
|---|---|---|
| 粒度声明 | 每行代表一次业务事件 | 订单头和订单行混在一个表里,分析时根本拆不开 |
| 维度退化 | 业务主键直接作为事实表的维度属性 | 订单号单独建维度表,查询要关联两次,性能惨不忍睹 |
| 不可变 | 已写入的事实行不允许修改 | 直接更新订单金额,导致历史汇总对不上账 |
| 可加性 | 度量字段必须能跨维度累加 | 把单价放进来,结果按日期求和得到一堆无意义数字 |
核心观点:事务事实表的粒度,决定了整个数据仓库的分析能力上限。粒度越细,灵活性越高,但存储成本也越大。我个人习惯在业务允许范围内,选择最细的粒度。
二、行来源:每一行数据从哪里来?
行来源,就是事实表里每一行数据的「出生证明」。我见过太多项目,上线后发现数据对不上,追根溯源都是行来源没搞清楚。
事务事实表的行来源,通常有这几种:
- 业务系统操作日志:比如订单创建、支付成功、发货确认。每一笔操作产生一条记录。
- 交易流水文件:银行、支付渠道提供的对账单。每行就是一笔交易。
- 消息队列事件:实时系统推送的业务事件。每收到一条消息,就生成一行事实。
- ETL增量抽取:从源表按时间戳或自增ID拉取新增数据。
避坑指南:我曾经接手过一个项目,行来源用了「全量快照+增量比对」的方式。结果每天跑批要对比几千万条数据,性能极差。后来改成基于业务时间戳的增量抽取,效率提升了10倍。记住:事务事实表就应该用增量方式,别搞全量比对。
三、典型场景一:销售订单
销售订单是事务事实表最经典的应用。咱们直接看设计。
3.1 粒度选择
订单的粒度有两种选择:
- 订单头粒度:一行代表一个订单。适合分析订单总数、总金额。
- 订单行粒度:一行代表一个商品明细。适合分析商品维度的销量、销售额。
我建议优先选订单行粒度。为什么?因为订单头粒度丢失了商品信息,后续想做商品分析还得关联子表,反而更麻烦。
3.2 表结构示例
CREATE TABLE fact_sales_order (
-- 维度外键
order_date_sk INT NOT NULL, -- 下单日期
customer_sk INT NOT NULL, -- 客户
product_sk INT NOT NULL, -- 商品
store_sk INT NOT NULL, -- 门店
-- 退化维度
order_no VARCHAR(30), -- 订单号(退化维度)
order_line_no INT, -- 订单行号
-- 度量
quantity DECIMAL(12,2), -- 数量(可加)
unit_price DECIMAL(12,2), -- 单价(不可加,仅做参考)
line_amount DECIMAL(12,2), -- 行金额(可加)
discount_amount DECIMAL(12,2), -- 折扣金额(可加)
-- 元数据
etl_batch_id INT,
create_dt TIMESTAMP
);
注意:unit_price 这个字段,我建议只保留在事实表里做参考,不要用它做汇总。因为不同商品的单价没有累加意义。真正可加的度量是 line_amount 和 quantity。
3.3 行来源逻辑
销售订单的行来源,通常来自订单系统的订单明细表。ETL 流程大概是:
- 每天凌晨,从源系统拉取前一天新增或变更的订单行。
- 通过订单日期、订单号、行号判断是否已存在。
- 对于新增行,直接插入事实表。
- 对于变更行(比如退款),插入一条新的事实行,用「事件类型」字段标记。
嗯,这里要注意:不要直接更新原行。我见过有人把退款金额直接减到原行上,结果历史数据全乱了。正确的做法是新增一行,用「-1」的数量或金额来表示退款。
四、典型场景二:交易流水
交易流水,比如银行转账、支付交易,和销售订单有些不同。它的特点是:一笔交易涉及两个账户。
4.1 粒度选择
交易流水的粒度,我建议按交易事件来定。一行代表一笔交易。但这里有个坑:一笔交易可能涉及借方和贷方两个账户。
我的做法是:拆成两行。一行记录借方(扣款方),一行记录贷方(收款方)。通过交易流水号关联。
4.2 表结构示例
CREATE TABLE fact_transaction (
-- 维度外键
trans_date_sk INT NOT NULL, -- 交易日期
account_sk INT NOT NULL, -- 账户
trans_type_sk INT NOT NULL, -- 交易类型(转账、消费、充值)
-- 退化维度
trans_no VARCHAR(40), -- 交易流水号
trans_seq INT, -- 交易序号(1=借方,2=贷方)
-- 度量
trans_amount DECIMAL(16,2), -- 交易金额(可加)
balance_after DECIMAL(16,2), -- 交易后余额(不可加)
-- 元数据
etl_batch_id INT,
create_dt TIMESTAMP
);
关键设计:trans_seq 字段用来区分借方和贷方。这样设计的好处是:按账户汇总时,借方金额是负数,贷方金额是正数,求和就能得到账户余额变化。我做过一个银行项目,就是用这个方案,跑批效率比传统方案高了30%。
4.3 行来源逻辑
交易流水的行来源,通常来自核心系统的交易日志或消息队列。我建议用事件驱动的方式:
- 交易发生时,业务系统发送一条消息到消息队列。
- ETL 消费消息,解析出交易信息。
- 拆分成借方和贷方两行,写入事实表。
- 如果消息丢失,用 T+1 的批量对账来补漏。
我曾经遇到过一个情况:消息队列积压,导致实时数据延迟了2小时。后来加了监控告警,一旦积压超过1000条就自动扩容消费者。嗯,这种坑踩过一次就记住了。
五、知识体系总览
下面这张图,把事务事实表的核心知识点串起来了。你可以把它当作设计时的检查清单。
六、避坑总结
最后,我把这些年踩过的坑汇总一下。你设计事务事实表时,可以对照检查:
- 粒度不清晰:订单头和订单行混在一起。解决方案:统一用订单行粒度。
- 直接更新历史数据:导致汇总对不上。解决方案:新增行+事件类型标记。
- 行来源用全量比对:性能极差。解决方案:改用增量抽取或消息队列。
- 把不可加度量放进来:比如单价、余额。解决方案:只保留可加度量,不可加字段仅做参考。
- 忽略退化维度:把订单号单独建维度表。解决方案:订单号、流水号直接放在事实表里。
我的建议:设计事务事实表时,先问自己三个问题:每行代表什么事件?哪些字段可以累加?数据来了之后会不会被修改?这三个问题想清楚了,设计就不会跑偏。