因为工作原因,小黄需要涉入大数据这一块的工作,所以再次补习一下数仓建模这一块的理论,参考《阿里大数据之路》这本书,以及AI来给我讲解的方式进行学习。
什么是数仓建模
我觉得是这样,数仓整套工作分为数据存储和数据建模两部分:
数据存储指将各业务系统的原始数据同步至数仓引擎落地保存;
数据建模是对已同步的原始数据,按照统一规范分层、结构化重构,生成标准化分析模型,最终依旧落地到数仓存储中供分析使用。
这里就引出了两个专业术语,OLTP、OLAP
- OLTP(联机事务处理):用于产生原始业务数据的业务数据库,典型如 ERP、BIP、订单系统底层库,负责记录日常增删改业务操作,常用 MySQL、SQL Server、Oracle 这类关系型数据库;设计上遵循三范式,侧重单条事务读写性能。
- OLAP(联机分析处理):专门存放经过建模加工后的分析数据,也就是数据仓库,会同步抽取 OLTP 业务库的数据做清洗分层建模,常见引擎有 Doris、Hive、StarRocks、ClickHouse;面向大批量、多维度统计查询,存储全量历史数据。
维度建模
我看数据仓库的建模方法论有很多种,其他的我也不太没怎么了解,大部分制造业的应用全是维度建模,所以主攻这一块的理论知识。
我们拿销售订单来举例
原始数据
| 订单号 | 日期 | 客户 | 商品 | 数量 | 金额 |
|---|---|---|---|---|---|
| SO001 | 2026-06-01 | 张三 | 苹果 | 10 | 100 |
| SO002 | 2026-06-01 | 李四 | 香蕉 | 20 | 80 |
| SO003 | 2026-06-02 | 张三 | 苹果 | 5 | 50 |
维度建模分为:维度表+事实表,我是这么理解的
- 维度:需要以某个字段汇总的数据,相当于
group by(字段),存储维度数据的表就叫维度表 - 事实:实际上需要汇总的数据,相当于
group by之后sum(字段),存储事实数据的表就叫事实表
那么在上述的表中,日期、客户、商品属于维度,数量、金额属于事实。接下来就要建立维度表和事实表。
事实表
| 订单 id | 日期 id | 客户 id | 商品 id | 销售数量 | 销售金额 |
|---|---|---|---|---|---|
| SO001 | 2026-06-01 | C01 | P01 | 10 | 100 |
| SO002 | 2026-06-01 | C02 | P02 | 20 | 80 |
| SO003 | 2026-06-02 | C01 | P01 | 5 | 50 |
客户维度表
| 客户 id | 客户名称 |
|---|---|
| C01 | 张三 |
| C02 | 李四 |
商品维度表
| 商品 id | 商品名称 |
|---|---|
| P01 | 苹果 |
| P02 | 香蕉 |
时间维度表
| 日期 id | 年份 | 月份 |
|---|---|---|
| 2026-06-01 | 2026 | 6 |
| 2026-06-02 | 2026 | 6 |
其实我感觉就有点类似于把一张表拆分为多张表,把实际的数据转换为关联数据。
一张事实表+多张维度表=星型模型
这里我还特地问了AI,这种星型模型,跟我们平时业务系统中实际存储的主键其实是类似的,但他们实际的目的是不一样的。
业务系统(OLTP,三范式)
服务日常增、删、改业务操作:下单、改客户信息、修改商品价格、退货。
- 优先保证数据一致性,修改一处数据只改一张表,不会出现数据不一致;
- 限制冗余,极致拆分,防止更新异常。
数仓星型模型(OLAP)
服务批量统计、报表、多维分析:按月算销售额、按客户统计销量。
- 几乎不删改历史数据,只新增;
- 优先降低查询 JOIN 复杂度,适度接受少量冗余,提升统计速度。
数仓分层
我们先来讨论一个问题,我们需要看报表统计数据的时候,有两种方案
**方案A:**直接从业务数据库里写SQL查询
**方案B:**每天把业务数据库同步到数仓,在写SQL查询数仓里的数据
一般来说当数据量非常大的时候,我们应该选择方案B,从以下几方面分析
- 对于关系型数据库来说,统计的SQL经常需要关联很多张表、聚合查询这种,数据量大的情况下查询会变得非常慢。
- 工作时间大部分都在使用系统,一个长时间的查询可能导致CPU飙升,造成业务系统卡顿。
ODS(业务系统备份)
所以,在这种情况下就引出一个ODS层,大家可以把他理解为业务系统在数仓中的备份,比如销售订单表做一个ODS的话,基本上字段都跟业务系统字段一致。
DWD(数据明细层)
那么,ODS有什么问题呢?这就要引出下一层DWD,可以理解为明细数据层,把原始数据进行清理后存在DWD层。
比如你做销售订单分析,真正关心的字段有:订单号、客户、物料、数量、金额、日期等,而原始表中有删除标志、修改时间、审批意见等一堆对于分析没有的字段,所以ODS层对我们来说数据是非常脏、乱、杂的,这时候就需要DWD来保证数据的可用性。
DWS(汇总层)
接下来我们要说DWS层,对于DWD和DWS来说他们本质上还是业务系统有多少条数据他们就有多少条数据,当数据量非常之大的时候,该慢还是要慢,这就引出了DWS层,也就是汇总层。
说白了汇总层就是预聚合,提前group by,比如老板要看销售明细等数据,按维度看,例如这个月、某个商品、某个客户销售了多少,我们提前设计好表,去定期统计数据存入这张表,那么在查询相关数据时,只需要查询DWS,明显提升效率。
| 月份 | 客户 | 销售金额 |
|---|---|---|
| 2026-06 | 01 | 100万 |
| 2026-06 | 02 | 80万 |
ADS(应用层)
ADS层数据是直接服务报表的,无论是DWD还是DWS,他们落库始终还是建立在维度建模上,也就是说事实表中只存储维度ID,而不存具体业务名称,例如客户名称等
而ADS是给报表、BI 直接使用的成品表,在建表时就提前把所有维度名称关联好,把 ID 翻译成中文名称直接落地入库,表里一整条包含全部业务可读信息,前端不用再做任何关联。
总结
那么数据建模中分为以下几层
ODS = 原始镜像,完整同步业务全量数据,不作加工 DWD = 清洗规整后的标准明细,维度建模事实/维表存放层 DWS = 按维度预聚合的中间汇总数据,供多报表复用 ADS = 面向业务需求的成品报表数据,直接对外展示完整数据流链路
线上 ERP 业务库 → ODS 原始备份层 → DWD 清洗明细层 → DWS 预汇总层 → ADS 业务报表层
学到这里,我又诞生了一个问题,为什么不能在取数的时候直接进行清理存到DWD层,而需要ODS来备份一套数据,这样会浪费磁盘空间,以下是AI给我的解释:
- 对账溯源必须要ODS(这我其实觉得业务系统才需要对账,我并不会为了这个而备份数据)
- 清洗出错可重新修复,不用重读业务库(这个是我觉得非常重要的一点)
- 一套原始数据,多业务复用(这个我觉得也蛮重要的,可能业务A需要某几个字段,业务B需要不同的几个字段)
事实表设计
业务过程
业务过程在我的理解下面是一个动作,一个可以记录、可以计量的行为。
这要跟平时做Java的思想区别开,做Java关注点往往是数据表,比如说销售订单,对于Java来说他只是冷冰冰的一张记录表,而对于数仓的这边的理解,是先找业务动作,再构建承载这个动作的事实表,例如销售下单,下单这个动作称之为业务过程。
如何识别业务过程
简单方法问自己两个问题:
问题1
有没有业务动作发生?
- 客户下单:有下单这个动作
- 客户档案:没有动作,只有基础资料
所以,客户是维度,下单是业务过程
问题2
能不能统计指标?
- 销售订单:可以统计销售金额、销售数量
- 客户档案:里面只有客户名称、客户电话,无法统计
所以,销售订单属于事实,客户属于维度
粒度
官方定义:粒度(Grain)就是事实表中一行数据所代表的业务含义。
同一种业务过程中,通常会有很多种粒度,还是拿销售订单举例
业务表是这样
| 订单号 | 行号 | 商品 | 数量 |
|---|---|---|---|
| SO001 | 1 | 苹果 | 10 |
| SO001 | 2 | 香蕉 | 20 |
粒度1:订单级
如果按照订单级来区分粒度,那么事实表应该是一行=一张订单
| 订单号 | 金额 |
|---|---|
| SO001 | 300 |
粒度2:订单行级
如果按照订单行级来区分粒度,那么事实表应该是一行=一条订单行
| 订单号 | 行号 | 商品 | 金额 |
|---|---|---|---|
| SO001 | 1 | 苹果 | 100 |
| SO001 | 2 | 香蕉 | 200 |
为什么强调粒度
因为粒度决定了后面能分析什么,例如我想知道苹果卖了多少,如果是订单级粒度,他是不知道某个商品卖了多少,他只知道这一个订单卖了多少
所以,DWD通常选择最细粒度
度量
事实表中可以计算统计的字段被称为度量
假设销售订单行有下面几个字段
订单号 行号 客户 物料 数量 金额 税额 折扣额 单价 业务员 订单类型 来源单据销售订单行字段分类
- 唯一标识:订单号、行号、来源单据
- 维度:客户、物料、业务员、订单类型
- 度量:数量、金额、税额、折扣额、单价
维度设计
退化维
本该属于维度表的属性,但不单独建维度表,直接存在事实表里的字段,叫退化维。
例如订单号、行号、来源单据等,这些字段没有统计意义,也没有分组意义的字段,就叫退化维
一致性维度
多张事实表共用一套标准的维度表,这些维度就是一致性维度。
例如物料、客户、日期,只要在同一家公司,不管是销售、库存、采购都是统一使用这些维度
缓慢变化维(SCD)
有些维度是不会变化的,例如物料,他从投入使用开始就不可能改编码这种,但是有些维度是会有变化的,比如客户的所属区域,可能25年属于华东,26年属于华南,这种会变化的维度叫做缓慢变化维。
25年客户维度
| 客户ID | 客户名称 | 所属区域 |
|---|---|---|
| C001 | 张三公司 | 华东 |
26年客户维度
| 客户ID | 客户名称 | 所属区域 |
|---|---|---|
| C001 | 张三公司 | 华南 |
销售事实表
| 日期 | 客户 | 金额 |
|---|---|---|
| 2025-06 | 张三公司 | 100万 |
| 2026-06 | 张三公司 | 200万 |
我觉得这个应该是在设计前,跟业务确定好要怎么看数据,比如统计25年的数据时,这个销售额应该属于华东还是华南。
SCD Type 1(覆盖更新)
这种情况是直接讲25年的数据统计到华南的做法,直接覆盖客户数据
SCD Type 2(保留历史)
我觉得这个才是企业常用的,并且更适合用的,修改后变成
| SK | 客户ID | 客户 | 区域 |
|---|---|---|---|
| 1 | C001 | 张三公司 | 华东 |
| 2 | C001 | 张三公司 | 华南 |
然后销售事实表应该是通过SK1和SK2关联,2025的销售数据关联SK1,2026年的数据关联SK2。
事实表类型
维度建模把事实表分为三大类
- 事务事实表
- 周期快照事实表
- 累计快照事实表
事务事实表
这就是目前所理解的事实表,专门记录业务动作,并且这个业务动作只增不改。
例如:销售订单事实表、采购订单事实表等
周期快照事实表
这个也比较好理解,正常用的时候比较多,在我接触的ERP项目中,他的库存状态是实时更新的,只有库存流水记录着每一笔出入库状态,这时候没法统计昨天库存多少、上个月库存多少,所以就需要事实表来每天保存一次库存状态,例如:
| 日期 | 商品 | 库存数量 |
|---|---|---|
| 6月1日 | 苹果 | 100 |
| 6月2日 | 苹果 | 70 |
| 6月3日 | 苹果 | 80 |
累计快照事实表
累积快照事实表记录的是一个业务实体(订单、工单、采购单等)从开始到结束的生命周期状态,一般一条业务实体对应一行记录,这行记录会随着流程推进不断更新。
创建订单时
| order_id | 下单日期 | 审核日期 | 发货日期 | 签收日期 | 状态 |
|---|---|---|---|---|---|
| SO001 | 2026-06-01 | NULL | NULL | NULL | 待审核 |
审核完成
| order_id | 下单日期 | 审核日期 | 发货日期 | 签收日期 | 状态 |
|---|---|---|---|---|---|
| SO001 | 2026-06-01 | 2026-06-01 | NULL | NULL | 待发货 |
发货完成
| order_id | 下单日期 | 审核日期 | 发货日期 | 签收日期 | 状态 |
|---|---|---|---|---|---|
| SO001 | 2026-06-01 | 2026-06-01 | 2026-06-02 | NULL | 已发货 |
签收完成
| order_id | 下单日期 | 审核日期 | 发货日期 | 签收日期 | 状态 |
|---|---|---|---|---|---|
| SO001 | 2026-06-01 | 2026-06-01 | 2026-06-02 | 2026-06-04 | 已完成 |