维度建模四步法:选择业务过程、声明粒度、确认维度、确认事实
说实话,很多刚接触数仓的朋友,一上来就问我:“到底怎么开始建模?”
我的回答永远是——先别急着画表。先搞清楚这四步:选过程、定粒度、找维度、列事实。这是 Kimball 老爷子留给我们的核心方法论,我做了十几年数仓,踩过的坑有一半都是因为跳过了其中某一步。
核心原则:四步法不是流水线,而是一个迭代循环。你可能会在确认事实时发现粒度不对,然后回头调整。这很正常,别怕改。
第一步:选择业务过程
业务过程,说白了就是业务中“发生了什么”。比如电商场景:用户浏览商品、加入购物车、提交订单、支付、发货、退货——这些都是业务过程。
我个人习惯,先拉一张业务事件清单。跟业务方聊的时候,不要问“你们有什么指标”,要问“你们每天在做什么事”。
我的经验:一次只聚焦一个业务过程。别想着一个模型覆盖所有。我见过最惨的案例,有人把“下单”和“支付”揉在一个事实表里,结果粒度乱得一塌糊涂,最后拆表花了三周。
选过程时,问自己三个问题:
- 这个事件有明确的触发点吗?(比如“点击购买”按钮)
- 这个事件能产生可度量的结果吗?(比如金额、数量)
- 这个事件对业务分析有价值吗?(比如“用户浏览”虽然难度量,但对转化分析很重要)
举个例子,我在做某零售项目时,业务方一开始说“我们要分析销售”。我一追问,发现“销售”其实包含了:客户进店、导购接待、试穿、开单、付款、取货六个过程。最后我们拆成了三个事实表——试穿事实、开单事实、付款事实。粒度不同,分析价值完全不同。
第二步:声明粒度
这一步是四步法里最容易被忽略、也最致命的一步。粒度,就是事实表中每一行数据代表什么。
你想想看,如果一行代表“一次订单”,那“一次支付包含多个订单”的情况怎么处理?如果一行代表“一次支付”,那“一个订单分多次支付”又怎么办?
避坑指南:我曾经在一个项目中,没有明确声明粒度就直接建表。结果事实表里既有订单级数据,又有支付级数据,分析时 SUM 出来的金额对不上。后来花了整整两天重新梳理粒度,才把数据对齐。从那以后,我要求团队在建模文档的第一行就写清楚粒度声明。
常见的粒度级别:
| 粒度级别 | 一行代表 | 典型场景 |
|---|---|---|
| 事务粒度 | 单次业务事件 | 每笔订单、每次点击 |
| 周期快照 | 固定周期内的汇总 | 每日账户余额、每月库存 |
| 累积快照 | 流程全生命周期 | 订单从下单到完成的全过程 |
怎么选?我的建议是:能选事务粒度,就别选快照。事务粒度最灵活,下游可以随意聚合。但代价是数据量大。如果业务方只关心“月底汇总”,那周期快照更合适。
第三步:确认维度
粒度定好了,维度就自然浮现了。维度就是回答“谁、什么、何时、何地”这些问题的属性。
比如粒度是“一次订单”,那维度就包括:
- 时间维度:下单时间、支付时间、发货时间
- 客户维度:客户ID、姓名、等级、地域
- 产品维度:产品ID、品类、品牌、价格
- 门店维度:门店ID、城市、区域、类型
这里有个容易犯的错:把维度属性塞进事实表。比如把“客户姓名”直接放在订单事实表里。嗯,千万别这么干。维度属性会变(客户改名字、换地址),事实表里存冗余属性会导致更新灾难。
黄金法则:事实表只存外键(维度ID)和度量值。所有描述性属性都放在维度表里。这样维度可以慢慢变,事实表不用动。
我个人习惯,在确认维度时画一张维度矩阵,横轴是事实表,纵轴是维度,打勾表示关联。这样一眼就能看出哪些维度是共享的,哪些是某个事实表独有的。
第四步:确认事实
最后一步,也是最“爽”的一步——把度量值填进去。事实就是你要分析的数值:金额、数量、时长、次数……
事实分三种:
- 可加事实:可以跨所有维度求和(比如销售额)
- 半可加事实:只能跨部分维度求和(比如账户余额,不能跨时间求和)
- 不可加事实:不能求和,只能做比值或平均值(比如比率、单价)
举个例子,我在做金融数仓时,交易金额是可加的,但“账户日均余额”就是半可加的——你可以跨客户求和,但不能跨日期求和,因为余额是快照值。
小技巧:事实表里尽量存原子度量,不要存计算后的指标。比如存“单价”和“数量”,而不是存“总价”。这样下游可以灵活计算。我见过有人把“折扣率”直接算成“折后价”存进去,结果业务方想分析折扣分布时,数据完全没法用。
确认事实时,还要注意事实的零值和空值处理。比如“退款金额”,没有退款时是存0还是存NULL?我个人习惯存0,因为NULL在聚合时会被忽略,可能导致统计偏差。
四步法实战:一个完整的例子
假设我们要做电商的订单分析模型:
- 选择业务过程:用户提交订单
- 声明粒度:一行代表一次订单中的单个商品(订单明细行)
- 确认维度:时间(下单日期)、客户、产品、门店、促销活动
- 确认事实:商品数量、单价、折扣金额、实际支付金额
-- 最终的事实表结构(简化版)
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) 计算。
四步法走完,模型基本就稳了。剩下的就是物理化、索引优化、分区策略这些细节。但核心骨架,就是这四步。我做了这么多年,每次建模都老老实实走一遍,从来没出过大问题。