事实表设计:三种类型、粒度选择与设计原则

事实表,说白了就是维度建模的核心。你想想看,业务上发生的每一笔交易、每一次盘点、每一个订单状态变更,最终都要落到事实表里。我做了这么多年数据仓库,见过太多把事实表设计成「大宽表」的惨案——跑不动、改不了、说不清。

这一章,咱们就把事实表掰开揉碎。三种类型怎么选?粒度定多细?设计时有哪些铁律?我用自己的踩坑经验跟你聊透。

事实表的三种类型

事实表不是只有一种。根据业务过程的不同,我们通常把它分成三类:事务事实表、周期快照事实表、累积快照事实表。选对了类型,后面能省一半的力气。

类型 核心特点 典型场景 粒度
事务事实表 记录单笔业务事件,一行一笔 订单下单、支付、退款 每笔交易
周期快照事实表 按固定时间间隔记录状态快照 每日库存、每月账户余额 每个周期每个实体
累积快照事实表 跟踪一个流程从开始到结束 订单全生命周期、工单处理 每个流程实例

事务事实表:最常用,也最容易理解

事务事实表记录的是「发生了什么」。每一行代表一个业务事件,比如用户下单、点击广告、刷卡消费。它的特点是:只增不删,来了新数据就追加。

我记得刚入行时,有个同事把订单的「创建」和「支付」放在同一张事务表里。结果想统计「已创建但未支付」的订单时,发现根本没法算——因为支付事件和创建事件混在一起,粒度乱了。这就是典型的类型选错。

我的习惯:只要业务过程是「瞬间完成」的,优先用事务事实表。比如点击、下单、退款成功。别把多个阶段塞进一张表。

周期快照事实表:解决「状态查询」难题

事务事实表有个短板——它只记录变化,不记录「当前是什么」。比如你想知道昨天仓库里每种商品有多少库存,事务表只能告诉你昨天进了多少、出了多少,但算不出存量。这时候就需要周期快照。

周期快照表按固定周期(每天、每小时)拍一张「快照」。每一行代表某个实体在某个时间点的状态。我做过一个零售项目,库存表就是典型的周期快照——每天凌晨跑一次,记录每个SKU的库存量、在途量、可售量。

注意:周期快照表的数据量会随时间线性增长。如果每天拍一次,一年就是365倍。所以周期不能太密,除非业务真的需要小时级快照。

累积快照事实表:追踪「流程进度」

有些业务不是瞬间完成的,而是一个流程。比如订单从「下单→支付→发货→签收」,中间可能隔几天。事务表只能记录每个环节的事件,但你想看「从下单到签收平均花了多久」?事务表做不到,累积快照表就是干这个的。

累积快照表里,一行代表一个流程实例。它会用多个日期字段记录每个关键里程碑的时间。比如订单累积快照表会有:下单日期、支付日期、发货日期、签收日期。随着流程推进,不断更新这些字段。

我曾经帮一个物流公司设计工单系统的事实表。工单从「创建→派单→接单→完成」要经过多个环节。如果用事务表,查一个工单的完整链路要join好几次。换成累积快照表后,一行搞定,查询效率提升了10倍。

粒度选择:事实表设计的「第一粒扣子」

粒度,就是事实表里每一行代表什么。这是设计事实表时最先要确定的。粒度错了,后面全歪。

我见过最典型的错误:把「订单头」和「订单行」混在一张表里。订单头是一笔订单的总金额,订单行是每个商品的金额。粒度不同,强行放一起,统计时要么重复计算,要么漏算。

粒度选择三原则:
  1. 最细粒度原则:能到行级别就别到订单级别。细粒度可以聚合,粗粒度无法拆分。
  2. 业务事件对齐:一行对应一个业务事件。比如「一次下单」对应一行,「一次支付」对应另一行。
  3. 可加性保障:粒度越细,度量值的可加性越强。细粒度下,金额、数量都可以随意聚合。

举个例子。电商订单,最细粒度是「订单行+商品」。每个订单行一行,包含商品ID、数量、单价、小计金额。这样你可以按商品、按品类、按品牌任意聚合。如果你只存到订单头粒度,想分析「哪个商品卖得好」?对不起,拆不开。

事实表设计原则:五个铁律

设计事实表不是拍脑袋。下面这五条原则,是我在多个项目中反复验证过的。每一条背后都有血泪教训。

原则一:只关注业务过程,不关注业务部门

事实表应该围绕「发生了什么」来设计,而不是「谁想看」。销售部想看销售额,市场部想看广告效果,但你不能给他们各建一张表。应该建一张统一的「订单事实表」,谁需要谁取数。

我遇到过最离谱的事:一个公司有7张「销售额」相关的事实表,口径还不一样。财务算出来1000万,销售算出来950万,开会时吵了半小时才发现口径不同。这就是没遵守这个原则。

原则二:所有事实表都要有明确的外键

事实表里的维度外键,必须指向维度表的主键。不能出现「维度值直接写在事实表里」的情况。比如订单事实表里应该存「客户ID」,而不是直接存「客户名称」。为什么?因为客户名称会变,存ID才能关联到最新的维度信息。

避坑指南:我曾经为了图方便,在事实表里直接存了「商品名称」。后来商品改名了,历史数据全对不上。从那以后,我坚持所有维度字段都用ID。

原则三:度量值必须可加、半可加或不可加

事实表里的数字,不是随便放的。你要清楚每个度量值的「可加性」:

  • 可加:金额、数量,可以跨所有维度相加。
  • 半可加:库存量、余额,可以跨商品维度相加,但不能跨时间维度相加(因为不同时间的库存不能直接加)。
  • 不可加:比率、单价、折扣率,不能直接相加。

设计时,尽量把不可加度量拆成可加度量。比如「折扣率」拆成「原价金额」和「折扣金额」,这样就能自由聚合了。

原则四:事实表要有代理键

虽然事实表的主键通常是多个外键的组合,但我建议你加一个自增的代理键。原因很简单:

  • 代理键是整型,join性能比多字段组合键好得多。
  • 业务主键可能会变(比如订单号格式调整),代理键不受影响。
  • ETL过程中,代理键方便做增量更新和去重。

原则五:不要存储派生数据

事实表里只存最原始的度量值。比如存「单价」和「数量」,不存「总金额」。总金额可以在查询时计算。为什么?

  • 派生数据占用存储,而且容易不一致(比如单价变了,总金额没更新)。
  • 查询时计算总金额,成本很低,没必要提前算好。
  • 保留原始数据,未来可以做更灵活的分析(比如按不同规则重新计算)。
例外情况:如果派生数据的计算逻辑极其复杂(比如涉及多表关联、递归计算),而且查询频率极高,可以考虑预计算。但一定要有明确的文档说明计算口径。

三种事实表的对比与选择

说了这么多,到底什么时候用哪种?我画了张图,帮你快速判断。

事实表类型选择决策流程 业务过程是什么? 是瞬间事件还是持续流程? 瞬间事件 事务事实表 持续流程 累积快照事实表 需要定期状态? 周期快照事实表 选择建议: • 订单创建、支付成功 → 事务事实表 • 每日库存、账户余额 → 周期快照事实表 • 订单全生命周期、工单处理 → 累积快照事实表

你看,决策逻辑其实不复杂。先判断业务过程是「瞬间事件」还是「持续流程」。瞬间事件用事务表,持续流程用累积快照表。如果还需要定期查看状态,那就再加一张周期快照表。

实际项目中,这三种表经常配合使用。比如电商系统:

  • 事务表记录每笔下单、支付、退款
  • 周期快照表每天记录每个商品的库存状态
  • 累积快照表跟踪每个订单从创建到完成的完整链路

三张表各司其职,互不干扰。这才是成熟的设计思路。

总结一下

事实表设计,说白了就是回答三个问题:

  1. 记录什么?——选对类型(事务、快照、累积)
  2. 多细?——定好粒度(越细越好)
  3. 怎么存?——遵守原则(外键、代理键、可加性)

这三个问题想清楚了,事实表的设计就稳了。我见过太多项目,上来就画ER图、建表,结果跑了两周发现粒度不对,推倒重来。嗯,设计阶段多花点时间,后面能省几个月。