3. 事实表设计(上):事务事实表、周期快照事实表、累积快照事实表

事实表,说白了就是咱们数仓里用来记录「发生了什么」的那张表。它存的是度量值,比如销售额、下单数量、点击次数。维度表告诉你「谁、在哪、什么时候」,事实表告诉你「发生了多少」。

我刚开始做数仓时,觉得事实表不就一张表嘛,有啥好设计的?后来被坑过几次才明白——事实表分三种,每种适用场景完全不同。选错了,后面查询慢、逻辑乱、维护成本高,哭都来不及。

今天咱们就把这三种事实表掰开揉碎讲清楚:事务事实表、周期快照事实表、累积快照事实表。

核心记忆点:事务事实表记「每次发生」,周期快照记「每个周期末的状态」,累积快照记「整个流程的进度」。

3.1 事务事实表(Transaction Fact Table)

这是最常用、最直观的事实表。每一行对应一个业务事件。比如电商里的每一笔订单、每一次点击、每一笔退款。

特点:

  • 一行一个事件,粒度最细
  • 可以按任意维度做聚合
  • 数据量最大,增长最快
  • 不支持更新,只追加

举个例子。我在项目中做过一个订单事务事实表,结构大概这样:

CREATE TABLE fact_order_transaction (
    order_id          BIGINT,
    product_id        BIGINT,
    customer_id       BIGINT,
    store_id          BIGINT,
    order_date        DATE,
    order_amount      DECIMAL(10,2),
    quantity          INT,
    discount_amount   DECIMAL(10,2),
    payment_type_id   BIGINT
);

每下一单,就插一条。简单粗暴。但要注意——事务事实表只记录「发生」那一刻。如果订单后来退款了、改金额了,事务表里不会改,你得另建一张退款事务表。

我的经验:事务事实表最适合做「发生了什么」的分析,比如「今天卖了多少」「哪个产品被点击最多」。但别用它做「当前状态」的查询,比如「这个订单现在到哪一步了」——那会查死你。

3.2 周期快照事实表(Periodic Snapshot Fact Table)

周期快照,说白了就是「定期拍照」。比如每天、每周、每月,把某个业务对象的当前状态拍一张照存下来。

特点:

  • 一行对应一个周期 + 一个实体
  • 记录的是「截至某个时间点的状态」
  • 数据量可控,周期越长数据越少
  • 支持更新(同一个周期内可以覆盖)

我做过一个库存周期快照表,每天凌晨跑一次:

CREATE TABLE fact_inventory_snapshot (
    snapshot_date     DATE,
    product_id        BIGINT,
    warehouse_id      BIGINT,
    quantity_on_hand  INT,
    quantity_reserved INT,
    quantity_available INT,
    last_update_time  TIMESTAMP
);

每天一条记录,告诉你「今天早上8点,这个仓库里这个产品还剩多少」。你想知道库存趋势?直接按日期聚合就行,不用去翻几十亿条出入库记录。

避坑指南:我曾经在一个项目中,周期快照的粒度没想清楚,把「每天每个产品每个仓库」拍成「每天每个产品」,结果库存对不上。后来加回仓库维度,数据量翻了三倍,但至少对了。记住——快照的粒度必须和业务实体一致。

3.3 累积快照事实表(Accumulating Snapshot Fact Table)

这个稍微复杂点。它记录的是一个业务流程从开始到结束的完整过程。每一行对应一个流程实例,字段里存的是各个关键节点的「时间戳」或「状态」。

特点:

  • 一行一个流程实例
  • 字段会随着流程推进不断更新
  • 适合有明确生命周期的业务(订单、工单、审批)
  • 数据量最小,但更新最频繁

拿订单流程举例:

CREATE TABLE fact_order_accumulating (
    order_id            BIGINT,
    order_date          DATE,
    payment_date        DATE,
    ship_date           DATE,
    delivery_date       DATE,
    return_date         DATE,
    order_status        VARCHAR(20),
    total_amount        DECIMAL(10,2),
    paid_amount         DECIMAL(10,2),
    refund_amount       DECIMAL(10,2)
);

一开始只填 order_id 和 order_date,其他字段为空。用户付款了,更新 payment_date。发货了,更新 ship_date。直到流程结束。

你想想看,如果你想分析「从下单到发货平均要多久」,用累积快照表就一句话:AVG(ship_date - order_date)。用事务表?你得先找到每个订单的「下单事件」和「发货事件」,再关联起来,麻烦多了。

适用场景判断:

  • 事务事实表 → 分析「事件发生」的量和趋势
  • 周期快照事实表 → 分析「某个时间点」的状态和变化
  • 累积快照事实表 → 分析「流程」的效率和瓶颈

3.4 三种事实表对比

我整理了一张表,方便你对照着看:

对比维度 事务事实表 周期快照事实表 累积快照事实表
粒度 每个事件一行 每个周期 + 实体一行 每个流程实例一行
数据更新 只追加,不更新 周期内可覆盖 随流程推进不断更新
数据量 最大 中等 最小
典型字段 度量值 + 维度外键 快照时间 + 状态度量 各节点时间戳 + 最终状态
典型查询 某时间段内总销售额 某天库存量 平均处理时长
业务场景 交易、点击、日志 库存、余额、账户状态 订单、工单、审批流

3.5 如何选择?

嗯,这里要注意。很多新手会问:「我到底该用哪种?」

我的建议是三步走:

  1. 先看业务问题:你想分析「发生次数」还是「当前状态」还是「流程效率」?
  2. 再看数据源:你拿到的原始数据是事件流、定期快照、还是流程日志?
  3. 最后看查询模式:用户最常问的问题是什么?

举个例子。如果你做的是电商数仓:

  • 订单明细 → 事务事实表
  • 每日库存 → 周期快照事实表
  • 订单履约流程 → 累积快照事实表

三个可以共存,不冲突。我见过很多成熟数仓,同一套业务同时维护两三种事实表,各司其职。

小技巧:如果你不确定选哪种,先从事务事实表开始。它最灵活,后期可以基于它生成周期快照和累积快照。但反过来就难了——从累积快照倒推事务细节,基本不可能。

3.6 三种事实表的关系图

下面这张图帮你理清三种事实表在数据流中的位置和关系:

三种事实表关系图 三种事实表在数据流中的位置 原始业务数据 事务事实表 每事件一行,只追加 周期快照事实表 每周期一行,可覆盖 累积快照事实表 每流程一行,不断更新 查询:某时段总销售额 查询:某天库存量 查询:平均处理时长 💡 三种事实表可以共存 事务表提供最细粒度数据,快照表提供周期性状态,累积表提供流程视角 根据业务需求选择,不必只用一种

3.7 实战中的常见坑

最后,分享几个我踩过的坑:

  • 事务表粒度不一致:同一个事实表里,有的行是订单级,有的行是订单行级。查询时聚合结果对不上。我的建议是——建表前先确认粒度,写进文档。
  • 周期快照的周期选错:库存变化快,你选月快照,中间缺货了都不知道。选日快照,数据量又太大。折中方案是——先按日跑,跑一段时间后看数据量,再决定是否降频。
  • 累积快照更新不及时:ETL 流程没处理好,订单都签收了,累积表里还是「已发货」。这会导致流程分析全错。我的做法是——给累积表加一个 last_updated 字段,监控更新延迟。

重要提醒:累积快照事实表虽然好用,但只适合有明确开始和结束的流程。如果你的业务流程没有固定终点(比如用户行为轨迹),别用累积快照,老老实实用事务事实表。