04.事实表完全指南:事务 / 周期快照 / 累积快照
事实表是维度建模的核心,但「事实表」三个字下面其实藏着三种完全不同设计思路的表。选错类型,要么查询性能崩,要么统计口径乱。
很多人以为事实表就是「存数值的那张表」,于是把订单流水、每日汇总、订单状态全塞进一张表里。结果是:粒度混乱、度量重复计算、分析需求一变就得推倒重来。
事实表按其粒度和业务过程建模方式,分为三种经典类型。这篇文章把它们彻底讲清楚。
一、先搞清楚:事实是什么、粒度是什么
1.1 事实 (Fact)
事实是业务过程中可度量的数值,通常表示某个业务事件的结果。比如「一笔订单金额 88 元」「一次登录」。
1.2 粒度 (Grain)
粒度是事实表一行代表什么。这是事实表设计里最核心、最容易被忽略的概念。
| 粒度声明 | 含义 |
|---|---|
| 一行 = 一个订单 | 订单级事实表 |
| 一行 = 一个订单的一个商品明细 | 订单明细级事实表 |
| 一行 = 一个用户一天 | 用户日级快照事实表 |
粒度的声明必须先于字段选择。一行代表什么没定清楚,后面加字段全是赌运气。详见 数据仓库建模原则。
1.3 事实的可加性
事实按其能否跨维度求和,分为三类:
| 类型 | 定义 | 举例 | 处理方式 |
|---|---|---|---|
| 可加事实 | 可跨所有维度求和 | 金额、数量 | 直接 SUM |
| 半可加事实 | 只能跨部分维度求和 | 库存余额(可跨商品,不可跨时间求和) | 取期末值或平均值 |
| 不可加事实 | 完全不能求和 | 比率、单价 | 存「分子+分母」两个可加分量 |
对不可加事实(如单价),正确做法是存可加的分量(销售额 + 销量),在 BI 层再相除得到单价。
二、三种事实表:一句话区分
| 事实表类型 | 一行代表什么 | 粒度 | 关键特征 |
|---|---|---|---|
| 事务事实表 | 一个业务事件(瞬间) | 事件级,最细 | 稀疏、行数多、不可更新 |
| 周期快照事实表 | 一个周期内的汇总(如一天/一周) | 周期级 | 稠密、定期生成、可更新 |
| 累积快照事实表 | 一个过程的关键节点(从开始到结束) | 过程级 | 一行贯穿全程、持续更新 |
用电商业务理解:
- 事务事实表 = 「每笔订单流水」——一行一条下单记录
- 周期快照事实表 = 「每天的商品库存快照」——一行一个商品一天
- 累积快照事实表 = 「每个订单的履约进度」——一行一个订单,从下单到签收不断更新
三、事务事实表 (Transaction Fact Table)
3.1 定义与特征
事务事实表是最细粒度的事实表,一行对应空间或时间上某点的一个度量事件。
fact_order_transaction(一行 = 一个订单明细)
order_item_id | order_id | user_id | product_id | order_time | amount | qty
--------------|----------|---------|------------|---------------------|--------|----
1001 | 90001 | 100 | 2001 | 2026-01-05 10:23:01 | 88.00 | 1
1002 | 90002 | 101 | 2002 | 2026-01-05 11:02:44 | 45.00 | 2
特征:
- 只插入,不更新(业务事件一旦发生不可变)
- 稀疏:业务没发生就无行,比如没下单的用户不出现在表里
- 行数巨大:与业务事件数量一一对应
3.2 适用场景
- 订单明细、支付流水、点击日志、浏览记录等流水型数据
- 需要下钻到单笔事件的明细分析
3.3 SQL 示例
-- 事务事实表:每笔支付流水
CREATE TABLE dwd_trd_payment_detail (
payment_id STRING COMMENT '支付流水号',
order_id STRING COMMENT '订单号',
user_id STRING COMMENT '用户ID',
channel STRING COMMENT '支付渠道',
pay_amount DECIMAL(18,2) COMMENT '支付金额',
pay_time STRING COMMENT '支付时间'
) COMMENT '交易域-支付明细事务事实表'
PARTITIONED BY (dt STRING)
STORED AS PARQUET;
事务事实表的度量通常是可加事实,可以放心 SUM。
四、周期快照事实表 (Periodic Snapshot Fact Table)
4.1 定义与特征
周期快照事实表汇总了某个标准周期内(一天、一周、一月)发生的多个度量事件。粒度是周期性的,而不是单个事务。
fact_inventory_daily(一行 = 一个商品 + 一天)
product_id | snapshot_dt | stock_qty | cost_amount | ...
-----------|-------------|-----------|-------------|----
2001 | 2026-01-05 | 500 | 12500.00 |
2001 | 2026-01-06 | 480 | 12000.00 |
2002 | 2026-01-05 | 30 | 900.00 |
特征:
- 稠密:即使某商品当天没有任何交易,只要它还「存在」,就有一行
- 定期生成:每天(或每周)固定生成一份快照
- 度量常为半可加:如库存余额,可跨商品求和,但不能跨时间求和(把两天的库存相加没意义)
4.2 适用场景
- 库存快照、账户余额快照、用户状态快照
- 需要回答「截至某天,状态是多少」这类问题(而非「某天发生了什么」)
4.3 半可加事实的正确用法
-- ❌ 错误:把两天的库存余额直接 SUM(跨时间求和,无意义)
SELECT SUM(stock_qty) FROM fact_inventory_daily; -- 结果没意义
-- ✅ 正确:跨商品可以求和(看某天总库存)
SELECT snapshot_dt, SUM(stock_qty) AS total_stock
FROM fact_inventory_daily
WHERE snapshot_dt = '2026-01-05'
GROUP BY snapshot_dt;
-- ✅ 正确:跨时间取「期末值」或「平均值」,而不是 SUM
SELECT product_id, AVG(stock_qty) AS avg_stock
FROM fact_inventory_daily
WHERE snapshot_dt BETWEEN '2026-01-01' AND '2026-01-31'
GROUP BY product_id;
半可加事实的坑:求和之前先想清楚「这个维度能加吗」。库存、余额这类「状态型」度量,跨时间维度通常只能取期初/期末/均值。
五、累积快照事实表 (Accumulating Snapshot Fact Table)
5.1 定义与特征
累积快照事实表汇总了一个过程从开始到结束之间可预测步骤内的度量事件。一行代表一个正在推进的业务过程,比如一个订单从下单到签收的整个生命周期。
fact_order_fulfillment(一行 = 一个订单的履约全过程)
order_id | order_time | pay_time | ship_time | sign_time | status
---------|---------------------|---------------------|---------------------|---------------------|--------
90001 | 2026-01-05 10:00:00 | 2026-01-05 10:05:00 | 2026-01-06 09:00:00 | NULL | shipped
90002 | 2026-01-05 11:00:00 | 2026-01-05 11:10:00 | NULL | NULL | paid
特征:
- 一行贯穿全过程:订单在走完流程前,始终是同一行
- 持续更新:订单从「已下单」→「已支付」→「已发货」→「已签收」,每一步都在更新同一行
- 通常带一个状态字段,标记当前进度
5.2 适用场景
- 订单履约、审批流程、贷款申请等有明确生命周期的业务
- 需要回答「当前有多少订单卡在发货环节」「平均下单到签收要多久」
5.3 SQL 示例(增量更新)
累积快照事实表最大的挑战是更新——它不是只插入,而是要持续修改已有行。
-- 每天用最新状态覆盖(INSERT OVERWRITE 保证幂等)
INSERT OVERWRITE TABLE dws_trd_order_fulfillment PARTITION (dt = '${biz_date}')
SELECT
o.order_id,
o.order_time,
COALESCE(p.pay_time, s.ship_time, o.order_time) AS pay_time, -- 逐步补全
s.ship_time,
sn.sign_time,
CASE
WHEN sn.sign_time IS NOT NULL THEN 'signed'
WHEN s.ship_time IS NOT NULL THEN 'shipped'
WHEN p.pay_time IS NOT NULL THEN 'paid'
ELSE 'created'
END AS status
FROM dim_order_base o
LEFT JOIN pay_detail p ON o.order_id = p.order_id
LEFT JOIN ship_detail s ON o.order_id = s.order_id
LEFT JOIN sign_detail sn ON o.order_id = sn.order_id;
累积快照事实表里那些「过程耗时」(下单到支付、支付到发货)是典型的可加事实,可以放心算平均值。
六、三种事实表对比总表
| 维度 | 事务事实表 | 周期快照事实表 | 累积快照事实表 |
|---|---|---|---|
| 一行代表 | 一个事件 | 一个周期汇总 | 一个过程 |
| 粒度 | 最细(事件级) | 周期级 | 过程级 |
| 数据密度 | 稀疏 | 稠密 | 中 |
| 是否更新 | 只插入 | 定期新增快照 | 持续更新同一行 |
| 典型度量 | 可加事实 | 半可加事实 | 可加(耗时类)+ 状态 |
| 典型场景 | 订单流水、点击日志 | 库存、余额 | 订单履约、审批流程 |
| 回答的问题 | 「发生了什么」 | 「截至某天是多少」 | 「现在进展到哪一步」 |
七、选型决策树
你要回答什么问题?
├── 「某时刻发生了什么事件」→ ✅ 事务事实表
├── 「截至某天,状态/余额是多少」→ ✅ 周期快照事实表
└── 「一个过程从开始到现在,进展到哪了」→ ✅ 累积快照事实表
实战经验:
- 事务事实表是底座,其他两种都可以从它聚合/演化而来
- 周期快照 = 事务的定期聚合,但如果查询只是「某天发生了什么」,直接查事务表 + 按天 GROUP BY 即可,不必专门建快照表
- 累积快照只用于有明确生命周期的过程,不是所有业务都要建
- 三者可以共存:一个订单业务,可以同时有「订单事务事实表」(明细)+「订单履约累积快照」(进度)+「每日销售周期快照」(日报)
八、常见坑点
- 粒度混用——订单级和明细级塞一张表,度量无法直接聚合
- 半可加事实跨时间 SUM——把两天的库存余额相加,得到一个无意义的数
- 不可加事实硬算——直接存「单价」列,下游怎么 SUM 都是错的;应存销售额 + 销量两个可加分量
- 累积快照不用 INSERT OVERWRITE——用 INSERT INTO 会导致订单重复,见 数据仓库建模原则 的「任务重跑必须结果一致」
九、小结
| 记住这句话就够了 | |
|---|---|
| 事务事实表 | 一行一个事件,只插入 |
| 周期快照事实表 | 一行一个周期,定期拍快照 |
| 累积快照事实表 | 一行一个过程,持续更新 |
选对事实表类型,粒度就自然清晰;粒度清晰,度量才不会算错。
📌 本文是 dwsql.com 数仓建模方法论系列第 2 篇。上篇见 维度建模入门:星型 vs 雪花 vs 星座,下篇讲维度表怎么应对「缓慢变化」,见 SCD 缓慢变化维详解。
「数据仓库技术」文章同步更新,不错过每一篇干货

备注「数据仓库技术」加入社群,每日一道大厂SQL真题

