订阅更新

数仓事实表的粒度怎么设计?

面试问 027 约 3 分钟读完 3 次阅读

本文目录
  1. 面试怎么回答
  2. 第一步:先用一句话声明“一行是什么”
  3. 第二步:区分三类常见事实表
  4. 第三步:检查维度和度量是否服从粒度
  5. 第四步:不要在一张表里混合两种粒度
  6. 一个典型场景

面试怎么回答

我会先明确业务过程,再用一句话声明一行代表什么,把粒度当成事实表的契约。然后选择事务事实、周期快照或累积快照,校验主键、维度和度量是否都服从这个粒度,尤其检查度量的可加性。订单级和明细级数据不能混在一张表里,否则很容易重复汇总。需要更高层查询时,可以单独建设聚合事实表,但不会破坏原子事实表的粒度。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

很多人的第一反应是:

“粒度就是一行数据代表什么,事实表越细越好。”

这句话只答对了一半。

真正需要讲清楚的是:

粒度是一张事实表的契约。先声明一行到底代表什么,再决定维度、事实和主键;任何字段都不能破坏这份契约。


第一步:先用一句话声明“一行是什么”

建事实表时,不要先把源表字段全部搬进来,而要先写一句完整的粒度声明。

例如:

一行代表一个订单中的一个商品明细

或者:

一行代表一个账户在一个自然日结束时的余额快照

这句话至少要回答三个问题:

  • 业务过程是什么:下单、支付、退款,还是账户余额
  • 识别对象是什么:订单、订单明细、用户,还是账户
  • 时间范围是什么:一次事件、一天快照,还是整个流程周期

粒度一旦确定,后面的维度键和度量都必须与它一致。


第二步:区分三类常见事实表

事实表不是都按同一种方式设计,常见的有三类:

1. 事务事实表

一行记录一次最原子的业务事件,例如一条订单明细、一次支付、一次点击。

它适合回答“发生了什么”,通常数据只追加,不反复更新。

2. 周期快照事实表

一行记录某个对象在固定周期的状态,例如账户每日余额、门店每月库存。

它适合回答“某个时间点是什么状态”。

3. 累积快照事实表

一行对应一个完整业务流程,随着关键节点到达不断更新,例如订单从创建、支付、发货到签收。

它适合回答“流程走到哪一步、各环节耗时多久”。

事实表三类粒度对比


第三步:检查维度和度量是否服从粒度

粒度声明完成后,我会做三类检查。

第一,检查主键能不能唯一标识这一行。

订单明细粒度通常需要 order_id + item_id,只用 order_id 就不够。

第二,检查每个维度是否能落在这一行上。

如果事实表是一条支付事件,那么支付时间、支付渠道可以直接描述它;但“用户当前等级”是会变化的状态,需要明确取事件发生时的等级,不能含糊地关联当前值。

第三,检查度量能不能在这个粒度上安全汇总。

明细金额通常可以跨商品、订单和日期累加;库存余额只能在商品之间相加,不能跨日期直接累加;订单总金额如果复制到每条商品明细上,再做 SUM 就会重复计算。

不是把字段放进事实表就结束了,还要知道这个度量在哪些维度上可加。


第四步:不要在一张表里混合两种粒度

最常见的问题,是为了“查询方便”把订单级和订单明细级字段放到同一张事实表里。

比如一笔订单有三条商品明细,订单总金额是 300 元。如果把 order_amount=300 复制到三条明细上:

商品A:order_amount = 300
商品B:order_amount = 300
商品C:order_amount = 300

下游一旦直接求和,就会得到 900 元。

更合理的做法是:

  • 明细事实表保存能够按明细累加的 item_amount
  • 订单级金额放在订单事实表,或者明确只在订单粒度聚合
  • 如果查询确实需要高层汇总,再单独建设聚合事实表

查询方便不能以口径不可靠为代价。


一个典型场景

比如报表里 GMV 比财务口径高,排查后发现事实表是一行一条订单明细,但订单总金额被重复写到了每条明细。

这个问题表面上是聚合 SQL 写错了,根因其实是表的粒度和度量粒度不一致。

修复时不能只在当前报表里加一个 DISTINCT。更重要的是回到模型层:重新声明粒度,拆分订单事实和订单明细事实,并标清每个度量的可加性。

事实表设计得好不好,最先看的不是字段多不多,而是一行的业务含义是否始终唯一。


刷题进度保存在当前浏览器