事实表类型:事务、周期快照与累积快照
事实表的设计,说白了就是回答一个问题:业务事件怎么存?
我做了这么多年数据仓库,见过太多团队一上来就建一张超级大宽表,结果跑数慢、维护难、业务方还天天抱怨口径对不上。其实,Kimball 老爷子早就给了我们三把利器——事务事实表、周期快照事实表、累积快照事实表。选对类型,你的模型就成功了一半。
核心观点:三种事实表不是互斥的,它们往往共存于同一个总线矩阵中,各自解决不同粒度的分析需求。
1. 事务事实表:记录每一次业务动作
事务事实表,最直观。每一行对应一个原子业务事件。
举个例子:你在电商平台下单,每笔订单的每个商品行,就是一条事务事实。它记录的是「发生了什么」以及「什么时候发生的」。
适用场景
- 订单明细分析
- 支付流水追踪
- 点击流日志
- 库存变动记录
设计要点
- 粒度最细:一行一个事件,不可再分
- 维度外键丰富:时间、产品、客户、门店……能挂的都挂上
- 度量可加:金额、数量、时长,跨行求和有意义
- 数据量大:每天几千万甚至上亿行很常见
我的经验:事务事实表是数据仓库的「底表」。我习惯在它之上做各种汇总,而不是直接让业务方查原始表。原因很简单——查询性能扛不住,而且业务方容易写错 SQL。
示例:订单事务事实表
CREATE TABLE fact_order_transaction (
order_sk BIGINT,
order_line_id INT,
product_sk BIGINT,
customer_sk BIGINT,
store_sk BIGINT,
date_sk BIGINT,
order_amount DECIMAL(10,2),
order_qty INT,
discount_amt DECIMAL(10,2)
);
你看,每一行就是一个订单行项目。想算某天某店的销售额?直接 sum(order_amount) 就行,简单粗暴。
2. 周期快照事实表:定期拍一张「快照」
事务事实表虽然细,但有些场景它搞不定。比如:我想知道每天结束时每个账户的余额。用事务表?你得把当天所有交易算一遍,再和前一天余额累加,麻烦不说,还容易出错。
周期快照事实表就是干这个的。它按固定周期(每天、每周、每月)记录某一时刻的状态。
适用场景
- 账户余额快照
- 库存水位监控
- 会员等级状态
- 月度销售目标达成进度
设计要点
- 周期固定:通常是每天一条
- 度量半可加:跨时间求和无意义,但跨维度求和有意义
- 数据量可控:行数 = 维度组合数 × 周期数
- 适合趋势分析:一眼看出状态变化
我曾经踩过的坑:做库存快照时,我一开始只存了「当前库存量」。后来业务要分析「日均出货速度」,我不得不回头去补历史快照。记住:快照表里除了状态值,最好也带上周期内的累计度量,比如「本月累计销量」。这样分析起来才灵活。
示例:每日库存快照
CREATE TABLE fact_inventory_snapshot_daily (
product_sk BIGINT,
warehouse_sk BIGINT,
date_sk BIGINT,
on_hand_qty INT,
in_transit_qty INT,
reserved_qty INT,
available_qty INT,
daily_in_qty INT,
daily_out_qty INT
);
嗯,这里要注意:on_hand_qty 是状态值,而 daily_in_qty 是周期内累计值。两者结合,分析起来才顺手。
3. 累积快照事实表:跟踪一个流程的完整生命周期
有些业务事件不是瞬间完成的,而是有一个明确的流程。比如:订单从「下单 → 支付 → 发货 → 签收」。事务表只能记录每个环节的独立事件,但你想看「从下单到签收一共花了多久」?那就得用累积快照事实表。
累积快照事实表,每一行对应一个流程实例,记录它从开始到结束的关键里程碑时间戳和累计度量。
适用场景
- 订单履约流程
- 工单处理流程
- 物流配送跟踪
- 审批流程分析
设计要点
- 粒度为一个流程实例:一行一个订单、一个工单
- 包含多个时间戳:每个里程碑一个日期字段
- 记录累计度量:总耗时、环节间间隔
- 数据会更新:流程没结束前,记录会被反复修改
我个人的习惯:累积快照事实表通常用 MERGE 或 UPSERT 来更新。每次流程推进,就更新对应行的里程碑时间。这样查询时直接算时间差,不用 join 多张表。
示例:订单履约累积快照
CREATE TABLE fact_order_accumulating (
order_sk BIGINT,
order_date_sk BIGINT,
payment_date_sk BIGINT,
ship_date_sk BIGINT,
delivery_date_sk BIGINT,
order_amount DECIMAL(10,2),
payment_amount DECIMAL(10,2),
days_to_payment INT,
days_to_ship INT,
days_to_delivery INT
);
你想想看,要分析「哪个仓库发货最快」?直接按 days_to_ship 排序就行,多痛快。
三种事实表对比
| 维度 | 事务事实表 | 周期快照事实表 | 累积快照事实表 |
|---|---|---|---|
| 粒度 | 单次事件 | 周期 + 维度组合 | 流程实例 |
| 时间维度 | 事件发生时间 | 快照时间 | 多个里程碑时间 |
| 度量可加性 | 完全可加 | 半可加 | 半可加 |
| 数据更新方式 | 只追加 | 覆盖或追加 | 更新已有行 |
| 典型数据量 | 极大 | 中等 | 较小 |
| 典型分析场景 | 明细查询、汇总统计 | 趋势分析、状态监控 | 流程效率、瓶颈分析 |
三种事实表的协作关系
实际项目中,这三种表不是孤立存在的。我画了一张图,帮你理清它们的关系:
如何选择?我的决策框架
每次设计新的事实表,我都会问自己三个问题:
- 业务事件是一次性的,还是持续性的?—— 一次性事件用事务表,持续性状态用快照表
- 用户关心的是「发生了什么」还是「现在怎么样」?—— 前者事务表,后者周期快照
- 是否需要跟踪一个流程的完整耗时?—— 需要的话,累积快照是唯一选择
实战建议:别想着用一张表解决所有问题。我见过最成功的项目,往往是三张表配合使用:事务表做底表,周期快照做日常监控,累积快照做流程分析。各司其职,互不干扰。
避坑指南
- 我曾经在事务表里混入了状态字段,结果同一笔订单出现多条记录,聚合时数据翻倍。记住:事务表只记录事件,状态变化交给快照表。
- 我曾经把周期快照的粒度设成了「每天每个产品」,结果产品维度有 10 万,一年下来 3650 万行,跑批慢得要命。后来加了仓库维度做分区,才解决问题。
- 我曾经在累积快照表里忘了加「流程状态」字段,导致无法区分「已完成」和「进行中」的订单。后来补了一个
order_status枚举字段,查询效率提升不少。
嗯,这三种事实表,说难不难,说简单也不简单。关键是理解它们的本质区别,然后根据业务场景灵活选用。你想想看,数据仓库的灵活性,很大程度上就体现在这里。