KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

05 · 数仓分层与建模 — keel 龙骨

前四章把数据搬对了、算对了。到了落地这一步,同一个业务问题(「每个状态多少单、金额多少」)在链路的哪个位置回答,代价能差三个数量级。这一章把这件事量出来。

前四章把数据搬对了、算对了。到了落地这一步,同一个业务问题(「每个状态多少单、金额多少」)在链路的哪个位置回答,代价能差三个数量级。这一章把这件事量出来。

现场

第 00 章的分析表 orders_analytics 只有一行一行按主键去重后的最新状态。运营要的却是「按状态汇总」。如果每次都去分析表上 group by,数据量涨上去以后报表会越来越慢;如果预先算好存在一张小表里,查询快到不像话——但那张小表就一定会过期。

这不是「选哪个」的问题,是分几层、每层各管一件事的问题。

加工链路

flowchart TD
  subgraph SRC["业务库 labcdc"]
    O["表 public.orders<br/>当前状态 28000 行"] -->|"逻辑槽 cdc_slot<br/>解码"| CH["变更流水<br/>INSERT 30000 / UPDATE 10500 / DELETE 2000"]
  end

  subgraph ODS["ODS 贴源层"]
    ODS_T["ods_orders_cdc<br/>42500 行 · append-only<br/>带 lsn / seq / op"]
  end

  subgraph DWD["DWD 明细层"]
    DWD_T["dwd_orders_current<br/>28000 行 · 按 order_id 取最新<br/>剔掉已删除"]
  end

  subgraph DWS["DWS 汇总层"]
    DWS_T["dws_orders_by_status<br/>3 行 · 按 status 预聚合"]
  end

  CH --> ODS_T
  ODS_T -->|"distinct on + 剔删除"| DWD_T
  DWD_T -->|"group by status"| DWS_T

  ODS_T -. "同一问题要现算:20.9 ms" .-> SLOW["扫 42500 行 + 排序去重"]
  DWD_T -. "直接聚合:4.8 ms" .-> MID["扫 28000 行"]
  DWS_T -. "查预聚合:0.021 ms" .-> FAST["扫 3 行"]
  ODS_T -. "DDL 不同步 / 改口径" .-> REBUILD["用 seq 顺序回放重建 DWD/DWS"]

  style O fill:#e8f0fe,stroke:#3b5bdb,color:#1a1a2e
  style CH fill:#e8f0fe,stroke:#3b5bdb,color:#1a1a2e
  style ODS_T fill:#fff4e6,stroke:#e8590c,color:#1a1a2e
  style DWD_T fill:#e6fcf5,stroke:#0ca678,color:#1a1a2e
  style DWS_T fill:#f3f0ff,stroke:#7048e8,color:#1a1a2e
  style SLOW fill:#ffe3e3,stroke:#c92a2a,color:#1a1a2e
  style MID fill:#fff4e6,stroke:#e8590c,color:#1a1a2e
  style FAST fill:#e6fcf5,stroke:#0ca678,color:#1a1a2e
  style REBUILD fill:#f3f0ff,stroke:#7048e8,color:#1a1a2e

精确定义

ODS(贴源层)存「原样」。它不做清洗、不去重、不建业务语义,保留变更的顺序和信息(本机保留 lsn、seq、op)。它的价值是可回放:任何一层的口径写错了,都能从 ODS 重新算,而不必去业务库重新抽。

**DWD(明细层)**是「清洗后的事实」。本机把它定义成「按主键取最新、剔除已删除」,也就是把变更流折叠成当前状态。真实数仓里这一层往往保留更细的粒度(每个订单的每次状态迁移各一行),因为汇总层会随需求变,明细层不该轻易丢。

**DWS(汇总层)**是「按主题预聚合」。它回答固定的那几类问题,行数极少。

**ADS(应用层)**是对着具体报表/接口做的输出层,本机没建——它和 DWS 的边界是「DWS 服务多个报表,ADS 服务一个」。

维度建模与宽表:维度建模是把事实(订单、支付)和维度(客户、商品、时间)分开,靠 join 还原;宽表是把需要的维度字段直接冗余进事实表,查询时不用 join。取舍点在于查询频次和变更频次:维度变化慢、查询频繁,宽表省下的是每次查询的 join;维度变化频繁,宽表就要反复回刷。

一次完整运行

原始输出见 lab/evidence/stream-data-pipeline/06-warehouse.txt。造 3 万下单、1.05 万次改状态、2 千次删除:

ODS ods_orders_cdc      : 42500 行(INSERT 30000 / UPDATE 10500 / DELETE 2000)
DWD dwd_orders_current  : 28000 行
DWS dws_orders_by_status: 3 行

三层的行数比 42500 : 28000 : 3。同一个查询「每个状态多少单、金额多少」在三层各跑一次 EXPLAIN ANALYZE:

查询对象 计划形态 Execution Time
ODS(要自己去重 + 剔删除) Seq Scan 42500 行 → Sort → Unique → GroupAggregate 20.877 ms
DWD(直接聚合) Seq Scan 28000 行 → HashAggregate 4.790 ms
DWS(读预聚合) Seq Scan 3 行 0.021 ms

DWS 相对 ODS 快了约 1000 倍(20.877 / 0.021)。DWD 相对 ODS 快了约 4.4 倍,省下的是「排序去重」和「多出来那 14500 行」。DWS 再快,是因为它把 group by 从查询时挪到了写入时。

顺带一个反直觉的对照——明细点查:

DWD: Bitmap Index Scan on dwd_orders_current_order_id_idx  Execution Time: 0.073 ms
ODS: Index Scan using ods_orders_cdc_oid_seq ...            Execution Time: 0.066 ms

两者几乎一样快(差 0.007 ms)。因为 ODS 上也有 (order_id, seq desc) 索引,还原单条并不比查宽表慢。分层带来的收益集中在「聚合」和「全表扫描」,不在「点查」。 如果你的负载全是按主键点查,多敷一层 DWD 不划算。

宽表/预聚合换来的是什么

拿本机的数字说话:

两者共同的代价是写入路径变长:每写一条明细,可能要多写几张预聚合表。写多了,写入延迟和失败面都变大。

回填与重跑

ODS 保留 seq(单调递增)和 op,这让重跑变得确定:DWD 的构建 SQL 是

create table dwd_orders_current as
with latest as (
  select distinct on (order_id) * from ods_orders_cdc order by order_id, seq desc
)
select order_id, customer_id, status, amount, updated_at, seq as src_seq
from latest where op <> 'DELETE';

它只依赖 ODS 的内容,不依赖任何外部状态,所以同样的 ODS 一定算出同样的 DWD。改口径时改这条 SQL、重建 DWD、再重建 DWS;要回到某个历史时点,把 ods_orders_cdc 过滤到 seq <= N 再跑一遍即可。

这正是「ODS 贴源」的核心价值:增量管道的正确性难以证明,但一条可重放的基线可以随时验证。 上游 DDL 变了、某天数据被误删、口径要调——只要有 ODS,这些都是重跑一次的问题。

生产边界

动手

  1. 跑 exp06_warehouse.sh,确认三层的行数是 42500 / 28000 / 3,并把三条 EXPLAIN ANALYZE 的 Execution Time 抄下来。
  2. 给 ODS 加一个 seq <= N 的过滤,重建 DWD/DWS,验证「按历史时点回填」能得到当时的口径。
  3. 断言题:给 DWD 的 order_id 加唯一索引,重建后确认没有重复的 order_id(去重逻辑正确)。

自测

  1. ODS 为什么要保留 op 和 seq?去掉它们,回填还能做吗?
  2. 为什么 DWS 比 ODS 快约 1000 倍,而 DWD 相对 ODS 只快 4 倍?
  3. 本机实测点查上 DWD 和 ODS 几乎一样快,这说明分层的收益集中在哪类查询上?
  4. 预聚合「把计算挪到写入路径」是什么意思?它换来的和付出的是什么?
  5. 口径要改,你从哪一层开始重算?为什么不是从业务库重新抽?

↓ 下一步:06 章 · OLAP 选型与管道运维 —— 预聚合的另一条路:列存和物化视图,以及上线后该盯什么。

进入 keel 阅读