4. 事实表设计(下):无事实事实表、事实表粒度选择、退化维度

好,咱们接着聊事实表。上一章我们把事实表的基础结构和分类讲完了,这一章要啃的,是三个容易让人迷糊、但又极其重要的概念:无事实事实表、粒度选择、退化维度。

说实话,这三个东西,我当年刚入行时也踩过坑。尤其是无事实事实表,第一次听到这个名字,我心想:“没事实还叫事实表?这不是自相矛盾吗?” 后来在项目中真用上了,才发现——嗯,真香。

4.1 无事实事实表:没有度量,但依然重要

先解决这个最反直觉的概念。

无事实事实表(Factless Fact Table),顾名思义,就是一张事实表里没有数值型度量。它只记录维度之间的关联关系,或者某个事件是否发生。

你可能会问:“没有数字,那它记录什么?”

记录事件。比如:

  • 学生选了哪门课
  • 用户参加了哪个活动
  • 产品上架到了哪个渠道

这些事件本身没有销售额、没有数量,但它们的组合本身就包含了信息。

核心思路:无事实事实表 = 维度外键的集合 + 时间戳(可选)。它回答的是“有没有发生过”的问题,而不是“发生了多少”。

4.1.1 什么时候用?

我个人习惯,在以下三种场景会优先考虑无事实事实表:

  1. 覆盖度分析:比如“哪些客户在哪些月份购买了哪些产品”。你不需要知道买了多少,只需要知道“买过”这个事实。
  2. 事件跟踪:用户点击了某个按钮、访问了某个页面。这些行为没有金额,但可以统计次数。
  3. 多对多关系的桥接:比如一个产品属于多个品类,一个品类包含多个产品。用无事实事实表来记录这个映射关系,比在维度表里塞逗号分隔的ID要优雅得多。

我的经验:我在做电商CRM项目时,需要分析“哪些会员在哪些促销活动中领过优惠券”。这个场景没有金额,只有“领了/没领”。用无事实事实表,每天跑一次快照,后面做复购分析、活动效果评估,数据直接就能用,不用再去日志表里翻。

4.1.2 设计示例

假设我们要记录“学生选课”这个事件。一张无事实事实表大概长这样:

-- 无事实事实表:学生选课事实
CREATE TABLE fact_student_course_enrollment (
    student_key INT NOT NULL,      -- 学生维度外键
    course_key INT NOT NULL,       -- 课程维度外键
    semester_key INT NOT NULL,     -- 学期维度外键
    enrollment_date DATE,          -- 选课日期(可选)
    PRIMARY KEY (student_key, course_key, semester_key)
);

你看,没有销售额,没有分数,没有数量。但通过这张表,你可以回答:

  • 这个学期有多少人选了《数据仓库》?
  • 张三一共选了多少门课?
  • 哪些课程连续三个学期都有人选?

这些分析,靠维度表之间的笛卡尔积也能做,但性能会差很多。无事实事实表把关联关系物化了,查询效率直接起飞。

注意:无事实事实表虽然没度量,但粒度依然要定义清楚。每一行代表什么?是一次选课事件,还是一个人在一个学期内的选课状态?这个不明确,后面分析就会打架。

4.2 事实表粒度选择:最关键的决策

好,接下来聊粒度。这是事实表设计里最核心、最不能出错的一步。

什么叫粒度?说白了就是一行数据代表什么。

我见过太多失败的模型,根源都是粒度没想清楚。比如一张销售事实表,有的行代表“一笔订单”,有的行代表“订单中的一行商品”,有的行甚至代表“一个包裹”。混在一起,聚合时数据就翻倍,对不上数。

4.2.1 粒度的三个层次

我个人把粒度分为三个层次,你设计时可以对号入座:

粒度层次 一行代表 典型场景 行数规模
事务粒度 单次事件 一笔支付、一次点击 极大
周期快照粒度 某个周期内的汇总 每日账户余额、每月库存 中等
累积快照粒度 整个流程的当前状态 订单从下单到签收的全过程 较小

你想想看,如果你要分析“用户每次点击的转化路径”,那必须是事务粒度。如果你要分析“月底每个仓库的库存水位”,周期快照更合适。如果你要跟踪“每个订单从创建到完成花了多久”,累积快照是首选。

4.2.2 粒度选择的黄金法则

我总结了一条原则,你可以记下来:

粒度 = 最细的维度组合 + 业务事件的最小单位

举个例子。在电商订单场景中:

  • 最细的维度组合:客户 + 商品 + 时间 + 店铺
  • 业务事件的最小单位:一个客户在一次下单中购买的一件商品

所以,粒度应该是订单行(Order Line Item),而不是整个订单。

为什么?因为一个订单可能包含多件商品,如果你把粒度定为“订单”,那商品维度的信息就丢了。后面想分析“哪个SKU卖得好”,根本做不了。

避坑指南:我曾经在一个项目中,业务方说“我们只需要看订单总额”,我就把粒度定在了订单级别。结果三个月后,他们要分析“不同品类的销售占比”,我只好重新ETL,把订单表拆成订单行表。那一次重构,整整花了两周。所以,粒度一定要往细了定,细了可以聚合,粗了没法拆。

4.2.3 粒度声明:写在表注释里

我有个习惯,每张事实表建好后,第一件事就是在表注释里写明粒度:

COMMENT ON TABLE fact_sales IS 
'粒度:每个客户在每个店铺每次下单中购买的每个SKU。
 一行代表:一个订单行(Order Line Item)。
 不包含:退款订单、测试订单。';

这样做的好处是,半年后你自己回来看,或者新人接手,一眼就知道这张表该怎么用。别小看这个习惯,它能省掉无数沟通成本。

4.3 退化维度:把维度“塞”回事实表

最后一个概念,退化维度(Degenerate Dimension)。名字听着吓人,其实很简单。

退化维度,就是把那些没有自己的维度表、只有ID的维度属性,直接放在事实表里。

最常见的例子就是订单号。订单号本身是一个维度属性,但它通常没有自己的维度表(你不会专门建一张 dim_order 表,里面只放一个订单号吧?)。所以,我们直接把订单号字段放在事实表里。

4.3.1 什么时候用?

我个人判断标准就两条:

  1. 这个属性没有其他描述信息。比如订单号、交易流水号、发票号,它们本身就是一个标识符,不需要额外的名称、分类、描述。
  2. 这个属性只在这个事实表上下文中才有意义。比如“物流运单号”,它只在物流事实表里出现,不会在其他地方被引用。

满足这两条,就别费劲建维度表了,直接退化到事实表里。

4.3.2 设计示例

看一个典型的销售事实表:

CREATE TABLE fact_sales (
    order_number VARCHAR(20),      -- 退化维度:订单号
    line_item_id INT,              -- 退化维度:订单行号
    product_key INT,               -- 常规维度外键
    customer_key INT,              -- 常规维度外键
    store_key INT,                 -- 常规维度外键
    date_key INT,                  -- 常规维度外键
    quantity INT,                  -- 度量
    amount DECIMAL(10,2)           -- 度量
);

这里的 order_number 和 line_item_id 就是退化维度。它们没有自己的维度表,但可以用来做明细查询、关联外部系统。

注意:退化维度虽然方便,但不要滥用。如果一个ID背后有丰富的属性(比如“店铺ID”对应着店铺名称、区域、等级),那就必须建维度表。退化维度只适用于纯标识符。

4.4 三者关系:一张图看懂

好,三个概念都讲完了。它们之间是什么关系?我画了一张图,帮你理清思路:

事实表设计核心概念关系图 事实表设计核心概念 事实表 无事实事实表 粒度选择 退化维度 无事实事实表 记录事件是否发生 没有数值型度量 粒度选择 一行代表什么? 事务/周期快照/累积快照 退化维度 纯标识符直接放事实表 不建维度表 三者共同决定:事实表的设计质量与分析能力

这张图想表达的是:无事实事实表解决“有没有”的问题,粒度选择解决“多细”的问题,退化维度解决“放哪里”的问题。三者共同决定了你的事实表设计质量。

4.5 本章小结

好,这一章的内容就到这儿。我们来快速回顾一下:

  • 无事实事实表:没有度量,只记录事件或关联关系。适用于覆盖度分析、事件跟踪、多对多桥接。
  • 粒度选择:事实表设计中最关键的决策。粒度 = 最细维度组合 + 最小业务事件。记住,往细了定。
  • 退化维度:把没有维度表的纯标识符直接放在事实表里。简单、高效,但别滥用。