维度建模四步法:选择业务过程、声明粒度、确认维度、确认事实

说实话,很多刚接触数仓的朋友,一上来就问我:“到底怎么开始建模?”

我的回答永远是——先别急着画表。先搞清楚这四步:选过程、定粒度、找维度、列事实。这是 Kimball 老爷子留给我们的核心方法论,我做了十几年数仓,踩过的坑有一半都是因为跳过了其中某一步。

核心原则:四步法不是流水线,而是一个迭代循环。你可能会在确认事实时发现粒度不对,然后回头调整。这很正常,别怕改。

维度建模四步法核心流程 ① 选择业务过程 “发生了什么?” ② 声明粒度 “一行代表什么?” ③ 确认维度 “谁/什么/何时/何地?” ④ 确认事实 “度量什么?” 迭代调整 各步骤核心产出 ① 业务过程 → 事件清单(如:下单、支付、发货) ② 粒度 → 每行数据的原子级别(如:一次支付行为) ③ 维度 → 描述环境的上下文(如:时间、客户、产品) ④ 事实 → 可量化的指标(如:金额、数量、耗时) 注意:步骤②的粒度决定了③和④的边界,这是最容易出错的地方。

第一步:选择业务过程

业务过程,说白了就是业务中“发生了什么”。比如电商场景:用户浏览商品、加入购物车、提交订单、支付、发货、退货——这些都是业务过程。

我个人习惯,先拉一张业务事件清单。跟业务方聊的时候,不要问“你们有什么指标”,要问“你们每天在做什么事”。

我的经验:一次只聚焦一个业务过程。别想着一个模型覆盖所有。我见过最惨的案例,有人把“下单”和“支付”揉在一个事实表里,结果粒度乱得一塌糊涂,最后拆表花了三周。

选过程时,问自己三个问题:

  • 这个事件有明确的触发点吗?(比如“点击购买”按钮)
  • 这个事件能产生可度量的结果吗?(比如金额、数量)
  • 这个事件对业务分析有价值吗?(比如“用户浏览”虽然难度量,但对转化分析很重要)

举个例子,我在做某零售项目时,业务方一开始说“我们要分析销售”。我一追问,发现“销售”其实包含了:客户进店、导购接待、试穿、开单、付款、取货六个过程。最后我们拆成了三个事实表——试穿事实、开单事实、付款事实。粒度不同,分析价值完全不同。

第二步:声明粒度

这一步是四步法里最容易被忽略、也最致命的一步。粒度,就是事实表中每一行数据代表什么。

你想想看,如果一行代表“一次订单”,那“一次支付包含多个订单”的情况怎么处理?如果一行代表“一次支付”,那“一个订单分多次支付”又怎么办?

避坑指南:我曾经在一个项目中,没有明确声明粒度就直接建表。结果事实表里既有订单级数据,又有支付级数据,分析时 SUM 出来的金额对不上。后来花了整整两天重新梳理粒度,才把数据对齐。从那以后,我要求团队在建模文档的第一行就写清楚粒度声明。

常见的粒度级别:

粒度级别 一行代表 典型场景
事务粒度 单次业务事件 每笔订单、每次点击
周期快照 固定周期内的汇总 每日账户余额、每月库存
累积快照 流程全生命周期 订单从下单到完成的全过程

怎么选?我的建议是:能选事务粒度,就别选快照。事务粒度最灵活,下游可以随意聚合。但代价是数据量大。如果业务方只关心“月底汇总”,那周期快照更合适。

第三步:确认维度

粒度定好了,维度就自然浮现了。维度就是回答“谁、什么、何时、何地”这些问题的属性。

比如粒度是“一次订单”,那维度就包括:

  • 时间维度:下单时间、支付时间、发货时间
  • 客户维度:客户ID、姓名、等级、地域
  • 产品维度:产品ID、品类、品牌、价格
  • 门店维度:门店ID、城市、区域、类型

这里有个容易犯的错:把维度属性塞进事实表。比如把“客户姓名”直接放在订单事实表里。嗯,千万别这么干。维度属性会变(客户改名字、换地址),事实表里存冗余属性会导致更新灾难。

黄金法则:事实表只存外键(维度ID)和度量值。所有描述性属性都放在维度表里。这样维度可以慢慢变,事实表不用动。

我个人习惯,在确认维度时画一张维度矩阵,横轴是事实表,纵轴是维度,打勾表示关联。这样一眼就能看出哪些维度是共享的,哪些是某个事实表独有的。

第四步:确认事实

最后一步,也是最“爽”的一步——把度量值填进去。事实就是你要分析的数值:金额、数量、时长、次数……

事实分三种:

  • 可加事实:可以跨所有维度求和(比如销售额)
  • 半可加事实:只能跨部分维度求和(比如账户余额,不能跨时间求和)
  • 不可加事实:不能求和,只能做比值或平均值(比如比率、单价)

举个例子,我在做金融数仓时,交易金额是可加的,但“账户日均余额”就是半可加的——你可以跨客户求和,但不能跨日期求和,因为余额是快照值。

小技巧:事实表里尽量存原子度量,不要存计算后的指标。比如存“单价”和“数量”,而不是存“总价”。这样下游可以灵活计算。我见过有人把“折扣率”直接算成“折后价”存进去,结果业务方想分析折扣分布时,数据完全没法用。

确认事实时,还要注意事实的零值和空值处理。比如“退款金额”,没有退款时是存0还是存NULL?我个人习惯存0,因为NULL在聚合时会被忽略,可能导致统计偏差。

四步法实战:一个完整的例子

假设我们要做电商的订单分析模型:

  1. 选择业务过程:用户提交订单
  2. 声明粒度:一行代表一次订单中的单个商品(订单明细行)
  3. 确认维度:时间(下单日期)、客户、产品、门店、促销活动
  4. 确认事实:商品数量、单价、折扣金额、实际支付金额
-- 最终的事实表结构(简化版)
CREATE TABLE fact_order_detail (
    order_detail_id   BIGINT,       -- 主键
    order_date_key    INT,          -- 时间维度外键
    customer_key      INT,          -- 客户维度外键
    product_key       INT,          -- 产品维度外键
    store_key         INT,          -- 门店维度外键
    promotion_key     INT,          -- 促销维度外键
    quantity          INT,          -- 商品数量(可加)
    unit_price        DECIMAL(10,2),-- 单价(不可加)
    discount_amount   DECIMAL(10,2),-- 折扣金额(可加)
    payment_amount    DECIMAL(10,2) -- 实际支付金额(可加)
);

你看,这个模型里,粒度是“订单明细行”,所以每个商品单独一行。维度全部用外键关联,事实只存原子度量。下游想分析“每个客户的总消费金额”,直接 SUM(payment_amount) 就行。想分析“折扣率分布”,用 discount_amount / (quantity * unit_price) 计算。

四步法走完,模型基本就稳了。剩下的就是物理化、索引优化、分区策略这些细节。但核心骨架,就是这四步。我做了这么多年,每次建模都老老实实走一遍,从来没出过大问题。