陷阱二:粒度声明陷阱——事实表粒度不明确,导致数据重复或丢失

好,咱们接着聊第二个坑。这个坑,说实在的,我见过太多次了。

很多刚入行的朋友,建事实表的时候,上来就写 INSERT INTO fact_sales,然后从源系统里把订单数据一股脑全拉过来。结果呢?跑出来的报表,金额总和要么翻倍,要么少一截。老板一问,谁都不敢拍胸脯说数据是对的。

为什么会这样?根本原因就是:事实表的粒度没有说清楚。

什么是粒度?一句话讲明白

粒度,说白了就是一行事实数据代表什么。

你想想看,一张销售事实表,如果一行代表「一笔订单」,那跟一行代表「一笔订单中的一个商品」,统计出来的总金额能一样吗?

我个人的习惯是,在开始建模之前,先拿笔在纸上写一句话:
「这张表里,每一行记录的是……的一次度量。」

这句话写不出来,或者写出来模棱两可,那后面肯定要出问题。

粒度声明公式:

事实表粒度 = 业务事件发生的自然单位 + 维度组合的唯一性

粒度不明确的三种典型症状

我在项目中遇到过几种常见情况,你可以对照看看自己中招了没:

  1. 「一行多意」型——同一张表里,有的行代表订单头,有的行代表订单行。数据源不同,ETL 合并时没做统一。
  2. 「粒度漂移」型——一开始说好是「每日店铺汇总」,后来业务要「每笔交易明细」,直接在原表上加字段,结果既有汇总又有明细。
  3. 「隐式聚合」型——源系统已经做了预聚合,ETL 没注意,又做了一次 sum,数据直接翻倍。

⚠️ 避坑指南:我曾经接手过一个项目,销售事实表里既有订单金额又有退款金额。开发同学把退款也当成正向订单加载进来,结果月报里「净销售额」比实际多了两倍。查了三天才发现,是粒度声明里没区分「正向事件」和「逆向事件」。

粒度声明的最佳实践

怎么避免这个坑?我建议你按下面几步来:

步骤 动作 产出物
1 明确业务事件 「下单」「支付」「退款」「发货」
2 确定自然粒度 「一次支付行为」vs「一笔订单中的一次支付」
3 列出所有维度 时间、产品、客户、门店、渠道
4 验证唯一性 这些维度组合在一起,能唯一确定一行吗?
5 写进设计文档 「fact_sales 每行代表一次订单行级别的销售事件」

嗯,这里要注意,第 4 步特别关键。你想想看,如果维度组合不能唯一确定一行,那数据加载的时候要么丢数据,要么重复。这就是事实表最常见的「多对多」问题。

一个真实的粒度声明案例

咱们来看一个具体的例子。假设你要建一张「电商订单事实表」。

错误做法:

-- 没有粒度声明,直接拉数据
INSERT INTO fact_order
SELECT
  order_id,
  product_id,
  order_amount,
  discount_amount,
  shipping_fee
FROM source_orders;

这里的问题在哪?order_amount 是整单金额,但 product_id 是商品级别的。如果一笔订单买了 3 个商品,那 order_amount 就会被重复计算 3 次。

正确做法:

-- 先声明粒度:每行代表一个订单行(Order Line Item)
-- 维度组合:order_id + product_id 唯一确定一行

INSERT INTO fact_order_line
SELECT
  order_id,
  product_id,
  line_item_amount,   -- 单个商品的金额
  discount_amount,    -- 单个商品的折扣
  shipping_fee        -- 运费分摊到行
FROM source_order_lines;

💡 小技巧:我习惯在事实表的注释里直接写明粒度声明。比如:

COMMENT ON TABLE fact_order_line IS 
'粒度:每行代表一次订单行级别的销售事件。
唯一键:order_id + product_id。
度量:line_item_amount, discount_amount, shipping_fee。'

这样后来的人一看就明白,不会乱用。

粒度声明与数据重复/丢失的关系

咱们用一张图来理清这个逻辑:

粒度声明与数据质量的关系 粒度明确声明 粒度不明确 维度组合唯一 度量值准确对应 数据不重复、不丢失 维度组合可能重复 度量值被错误分摊或聚合 数据重复或丢失 对比 核心结论:粒度声明是事实表设计的「第一性原理」 声明清楚,数据质量就有保障;声明模糊,后面全是坑

常见粒度声明场景对照表

我把实际工作中最常见的几种场景整理了一下,你可以直接拿来参考:

业务场景 推荐粒度 唯一键 典型度量
电商订单 订单行级别 order_id + product_id 销售额、折扣、数量
银行交易 单笔交易级别 transaction_id 交易金额、手续费
库存快照 每日商品级别 date + product_id + warehouse_id 库存数量、库存金额
用户行为日志 单次事件级别 session_id + event_seq 停留时长、点击次数
财务流水 单笔凭证级别 voucher_id + line_item 借贷金额、余额

如何检查粒度是否声明正确?

给你一个我常用的自检清单:

  • 自检 1:随便抽两行数据,它们的维度组合能一样吗?如果能,说明粒度太粗。
  • 自检 2:对度量字段做 SELECT SUM,然后跟源系统对账。对不上,说明粒度有问题。
  • 自检 3:如果同一个事实表里既有金额又有数量,检查一下它们是不是在同一粒度上。比如金额是订单级,数量是商品级,那就出问题了。
  • 自检 4:问问自己:「如果业务方问这一行数据是什么意思,我能用一句话说清楚吗?」

我的个人经验:每次建新事实表,我都会先写一段注释,把粒度声明写清楚。然后让团队里的另一个人 review 一下。很多时候,别人一眼就能看出你声明里的漏洞。这比上线后出问题再回头查,成本低太多了。

小结

粒度声明这件事,听起来简单,做起来容易忽略。但它是事实表设计的基石。基石不稳,上面盖的楼再漂亮也没用。

记住一句话:「先声明,后建模;声明不清,模型必崩。」

嗯,这个坑咱们就聊到这儿。你只要每次建表前多花 5 分钟把粒度想清楚,就能避开 80% 的数据质量问题。