写在前面:这是我学习数仓系列记录的第一篇,我会把每天学习时对工作重要的,难理解的东西记录下,我试着把 Kimball 维度建模的核心概念串成一条线------从"为什么数仓必须独立存在"讲起,再落到维度表、事实表、代理键、可加性这些基础零件上。文中会穿插一些我自己的理解方式,不一定严谨。
一、OLTP 与 OLAP:为什么数仓必须独立存在
刚接触数仓时我有个疑问:业务库好好的,为什么不能直接拿来做分析?答案藏在两类系统的基因差异里。
OLTP(联机事务处理) 的设计目标是"快而准地处理每一笔交易"。它高度规范化,同一个客户信息只存一份,更新时只改一个地方。这在写入场景下很高效,但一旦面对"统计上个月华东地区所有品类销售额"这类查询,就需要把十几张规范化表 Join 起来,慢得让人绝望。
OLAP(联机分析处理) 的设计目标恰恰相反:让分析查询跑得快。它采用维度建模,把数据组织成星型或雪花型,适度反规范化,用冗余换取更少的 Join 和更快的聚合。而且分析往往要回溯历史,OLTP 库如果为了性能定期清理旧数据,分析就没法做了。
更现实的问题是资源争抢。一条扫描几百万行的分析查询扔到业务库上,会直接拖慢用户的正常下单和支付。所以数仓独立存在,本质上是在保护交易系统的稳定性,同时给分析工作一个按分析场景优化过的环境。这不是架构上的奢侈,是工程上的必要。
二、数仓 vs 数据库:不只是名字不同
把"数仓"和"数据库"混着叫,是我入门时最常犯的错。现在我会这样区分:
数据库(偏 OLTP) 关心的是"当前这一刻,这笔订单的状态是什么"。它追求事务的 ACID,数据是规范化的,保留的是最新的、一致的状态快照。
数据仓库(偏 OLAP) 关心的是"过去三年,这类订单的趋势是什么"。它追求的是查询吞吐和聚合效率,数据是反规范化的,保留的是完整的历史轨迹。一个客户搬了三次家,OLTP 库里只有最新地址,但数仓里可能有三条记录,分别对应他每次下单时所在的地址。
另外,数仓是面向主题的。业务库按"订单表、用户表、商品表"这样按功能划分,数仓则按"销售主题、库存主题、用户行为主题"来组织,把散落在多个业务系统里、关于同一主题的数据集成到一起。这种集成本身就需要清洗、去重、统一口径,不是简单复制能完成的。
三、维度表与事实表:数仓的两块基石
维度建模把所有表分成两类,这个二分法极其简洁,但足以覆盖绝大多数分析场景。
事实表 记录的是"发生了什么"。每一行对应一个业务事件(一次下单、一笔支付、一条库存快照),里面装的是可度量的数字,比如金额、数量、次数。事实表通常又窄又长,行数可以大到千亿级。
维度表描述的是"谁、什么、哪里、何时"。它给事实表里的数字提供上下文。比如事实表里有一行"2024-03-15,客户123,商品456,买了2件,花了199元",维度表负责告诉你:客户123住在杭州、是金卡会员;商品456属于户外运动品类、是某个品牌的帐篷;2024-03-15是周五、属于第一季度。
我自己的记忆方法是:事实表是"动词+数字",维度表是"名词+描述"。事实表回答"多少",维度表回答"关于什么"。
四、自然键 vs 代理键:为什么维度表需要两个身份证
维度表里通常有两个"键",一开始我觉得这是冗余,后来才明白各自的分工。
自然键 (也叫业务键)是业务系统里本来就有的标识,比如订单号、身份证号、商品编码。它来自源系统,对人友好,但有个致命问题:可能变,也可能重复。比如商品编码在系统升级时改规则了,或者两个源系统的客户 ID 撞号了。
代理键 是数仓自己生成的、无业务含义的整数(通常自增)。它唯一的职责就是在数仓内部唯一标识一行 。用代理键做 Join 有几个实际好处:整数比字符串 Join 快;源系统的键变了不影响数仓;更重要的是,它支持缓慢变化维度------同一个客户因为搬家产生了三条历史记录,自然键都是同一个身份证号,但代理键分别是 101、102、103。这样,搬家前下的单关联的是 101,搬家后关联的是 102,历史归属就保住了。
我的理解是:自然键是"给业务看的",代理键是"给数仓自己用的"。
五、关于"维度设计六原则"的说明
Kimball 原著和主流实践里并没有一个被广泛公认的、固定编号的"六原则"。不同团队会根据自己的经验总结出不同条数。结合常见实践,大致可以归纳为:
- 原子性:维度属性尽量不可再分,日期就存日期,不要存"2024年3月"这种混合值。
- 一致性:同一个业务概念在不同维度表里的命名、类型、枚举值必须统一,否则跨主题分析会错。
- 可理解性:属性用文字描述而非编码,让业务用户能直接看懂。
- 适当冗余:为了少 Join,维度表可以反规范化,把层次属性打平放在同一张表里。
- 可扩展性:预留未来可能增加的属性位置,避免频繁改表。
- 代理键独立:维度表必须有代理键,与业务键分离。
需要说明,这不是金科玉律,更像是一个检查清单。不同书和文章里的"原则"条目差异很大,不必纠结于数字。
六、事实的三种可加性
事实表里的数字,不是都能随便相加的。这个区分在做汇总时至关重要。
可加事实:可以沿任意维度相加。销售额、数量、次数,怎么加都对。
半可加事实 :只能沿部分维度相加,最典型的是余额。你可以把同一天所有账户的余额加起来(沿账户维度可加),但不能把一个账户一个月的余额加起来(沿时间维度不可加,那没有意义)。
不可加事实 :任何维度都不能直接相加,比如比率(转化率、毛利率)。处理办法是拆成分子和分母分别存储,让它们变成可加事实,使用时再相除。比如不存"转化率 3%",存"曝光 1000 次,转化 30 次"。
这个知识点看起来小,但在实际做报表时,直接把比率字段 sum() 起来是最常见的错误之一。
七、三种事实表:对应三类业务节奏
事实表按业务过程的时间特性,分成三种。我一开始分不清后两种,后来用"一个事件 vs 一个周期 vs 一个生命周期"来记,就清楚多了。
事务事实表 是最基础的。业务发生一次,插入一行,只插入,不更新。下单、支付、点击、退款,每个原子事件一行。粒度最细,灵活性最高。
周期快照事实表按固定周期(天/周/月)记录状态。比如每天日终的库存余额、每月末的账户余额。它不是记录"发生了什么变化",而是记录"在那一时刻,状态是什么"。同样只插入,不更新,但粒度是"实体 × 周期"。
累积快照事实表 最特殊,它跟踪一个实体的完整生命周期 。一个订单从创建、付款、发货到签收,在累积快照表里是同一行 ,随着流程推进不断被 UPDATE,不同阶段的日期填进不同字段。它的价值在于能直接算出"下单到付款平均间隔多久"这类跨阶段指标。
三种表的核心区别:
| 事务事实表 | 周期快照事实表 | 累积快照事实表 | |
|---|---|---|---|
| 一行代表 | 一个事件 | 一个实体在一个周期内的状态 | 一个实体的完整生命周期 |
| 写入方式 | 只插入 | 只插入 | 插入后更新 |
| 时间字段 | 单一事务时间 | 单一快照时间 | 多个里程碑时间 |
写在最后
这篇笔记梳理了维度建模最底层的几个概念。我的体会是:维度建模的价值不在于概念多精妙,而在于它强迫你在动手写 SQL 之前,先把"业务过程是什么、粒度是什么、谁在分析什么"想清楚。代理键、可加性、三种事实表,这些看似琐碎的规则,每一条背后都有真实的踩坑教训。