20-数仓分层与建模.md 21 KB

数仓分层与建模

本文档记录 poyee-data-warehouse 数据仓库的分层架构、建模方法论与总线矩阵。 与命名规范(数仓命名规范.md)配套阅读。

1. 架构原则

  • 横向分层:提高数据复用率,明确数据血缘
  • 竖向分域:指导指标体系的建设

2. 分层定义

二维视图(横向分层 × 公共维度侧柱):

                                                ┌────────────┐
┌───────────────────────────────────────────┐   │            │
│  ADS  应用层:业务指标、面向服务端宽表      │   │            │
├───────────────────────────────────────────┤   │            │
│  TDM  标签层:长表明细 + 宽表 + 人群包      │◄──┤    DIM     │
├───────────────────────────────────────────┤   │            │
│  DWS  汇总层:主题聚合、提供公共指标        │◄──┤  公共维度   │
├───────────────────────────────────────────┤   │            │
│  DWD  明细层:清洗加工 + 维度退化           │◄──┤            │
├───────────────────────────────────────────┤   │            │
│  ODS  贴源层:类型转换、脏数据识别          │   │            │
├───────────────────────────────────────────┤   └────────────┘
│  RAW  采集层:全字段 STRING,原样落盘       │
└───────────────────────────────────────────┘

  竖向分域(贯穿 DWD 及以上):交易 trd / 用户 usr / 商品 prd / 店铺 shp / 公共 pub

分层定义表:

代码 定位 典型操作
原始采集层 raw 从源系统/外部文件落地的原始数据,全字段 STRING,同步任务不做任何类型转换 DataX 直接同步、CSV 解析、字段原样落盘
贴源层 ods 类型转换、空值处理、脏数据识别的唯一入口,输出类型化的干净表 从 raw STRING 字段做 CAST/TRY_CAST、空值兜底、去重
维度层 dim 公共维度(时间、地区、渠道等),贯穿 DWD/DWS/TDM/ADS 维度退化、缓慢变化维
明细层 dwd 标准化、维度补全、异常处理;星型模型 宽表化、维度退化、数据质量校验
汇总层 dws 建立汇总宽表,提供公共指标 多维聚合、主题整合
主题域模型层 tdm 标签明细长表(EAV)+ 核心标签宽表(pivot)+ 人群包(远期 bitmap);按实体类型分表(usr/prd/shp),Hive 离线计算,远期可加 HBase(在线标签服务)/ ClickHouse(人群包交叉计算) 标签计算、pivot 宽表、bitmap 圈选
应用层 ads 提供数据展示、数据指标 面向消费端的定制聚合

数据流:

RDS PG / ES ──DataX──▶ RAW ──SparkSQL──▶ ODS ──▶ DWD ──▶ DWS ──▶ TDM ──▶ ADS
                                                                             │
                                                                             ▼ DataX / Broker Load
                                                            服务层(Doris / CK / ES / Mongo)

3. 主题域划分

当前划分为以下主题域,对应命名规范的 domain 代码:

代码 说明
交易域 trd 订单、支付、退款、购物车
用户域 usr 注册、登录、活跃、画像
商品域 prd 商品、SKU、SPU、价格
店铺域 shp 店铺、商家
公共域 pub 平台、日历、地理等

4. 数仓总线矩阵

总线矩阵用于指导维度建模和指标体系建设,列出各业务过程与公共维度/业务维度的关系。 以下是模板,还未整理成正式可用的矩阵

业务过程 说明 时间 用户 地区 渠道 商品 店铺 活动 支付
交易域 order_create 用户提交订单(未支付)
order_pay 用户完成订单支付
order_cancel 用户取消订单
refund 用户发起退款/售后
cart_add 用户加入购物车
用户域 user_register 用户注册账号
user_login 用户登录行为
user_active 活跃行为(浏览/点击)
商品域 product_expose 商品曝光(列表/推荐位)
product_click 商品点击进入详情页
product_favor 用户收藏商品
店铺域 shop_visit 用户访问店铺
shop_follow 用户关注店铺

5. 维度建模方法论

OLTP 业务库采用范式建模;OLAP 数仓特别是 DWD 层采用维度建模(Kimball)。 模型类型:星型模型,维度退化在 DWD 或 DIM 层完成。

5.1 建模五步法

步骤 说明 示例(发券业务)
1. 确定主题域 选取所属业务域 交易域 / 营销域
2. 选择业务过程 从业务流程中提取可度量的动作 券发放 / 曝光 / 点击 / 领取 / 核销
3. 声明粒度 明确事实表每行表示什么;优先原子粒度 "每个用户每次领券记录";主键 = 领取 id + 幂等 id
4. 确认维度 从哪些角度切分数据 权益类型、发放渠道、门槛、折扣、时间、用户
5. 确认事实 表示度量的数值字段 领取金额、核销金额、引导 GMV

5.2 关键原则

  • 同一事实表粒度必须一致。跨业务过程合并的合并事实表,各自度量必须有同等级粒度。
  • 维度退化:将常用维度属性冗余到事实表,减少运行时 JOIN。
  • 一致性维度:公共维度全局统一(见 dim_pub_* 系列)。
  • 原子粒度优先:最低粒度事实可以无限上卷,高粒度事实不可下钻。
  • 加工层框架字段:DWD / DIM / DWS / TDM 表统一带 etl_time TIMESTAMP + 分区 dt STRING + STORED AS ORC;raw / ods 字段与类型约定见 22-业务库raw建模.md §0。

5.3 建模示例

【业务过程】 券的核销
【粒度】      子订单 × 券 id (SKU 级)
【维度】      权益类型 / 领取场景 / 发放渠道 / 时间 / 用户 / 店铺
【事实】      核销金额 / 引导 GMV

5.4 从数仓规划到物理落地的推演

左侧是抽象的规划层级,右侧是一个零售业务的具体示例:

规划层级 示例(xxx 零售) 物理落地
业务板块 xxx 零售
数据域 交易域 Schema 划分
业务过程 支付 DWD 事实表切片
度量 下单金额 事实字段
维度建模 时间 / 区域 / 商品 维度表:订单表、商品表
原子指标 下单金额 order_amt_cny
派生指标 近 1 天 上海区域 支付下单金额 ADS 汇总表字段
汇总表 ads_trd_order_area_agg_inc_d 按区域聚合的下单金额日汇总

关键映射:业务过程 → DWD 事实表;度量 → 事实字段;维度 → DIM 表;派生指标 = 原子指标 + 时间周期 + 修饰词 → DWS/ADS 字段。

5.5 DWD 事实表设计(事件 vs 状态)

核心规则:DWD 事实表默认承载业务事件(不可变事实),实体当前状态进 DIM 拉链表(_zip_d

事件 vs 状态判别:

  • 事件:某一时刻发生的业务动作(下单、支付、发货、签收、提交审核、审核通过 / 拒绝),不可变,写入即固定
  • 状态:实体当前的属性或阶段(订单当前状态、用户等级、拼团当前状态),随时间演化,可变

建模规则:

类型 归属 命名(参考 40-命名规范.md 示例
业务事件 DWD 事实表,每个业务过程一张 dwd_{域}_{业务过程}_apd_d dwd_trd_order_create_apd_ddwd_trd_order_pay_apd_d
实体状态 DIM 拉链表 dim_{域}_{实体}_zip_d dim_trd_order_zip_d(含 current_status / 各阶段时间 / start_date / end_date

为什么这么拆

  • DWD _apd_d 只追加,与 Hive 列存(ORC)"只追加 + 分区"模型天然契合,不需要 UPDATE
  • 状态查询("截止 dt 各状态订单数")走拉链表 is_current 切片或按 dt 切片,直观
  • 跨表 JOIN("下单数 - 退单数 = 实际下单数")是维度建模的正常能力,不是代价
  • 这是 Hive / 列存数仓的事实标准做法(阿里 OneData / 字节 / 美团数仓主流)

典型场景:

场景 事件表(DWD _apd_d 状态表(DIM _zip_d
订单履约(下单 → 支付 → 发货 → 签收,单向) 4 张 _apd_d dim_trd_order_zip_d
拼团(发起 → 审核 → 审核拒绝 → 重新提交 → ……,循环) 每个动作一张 _apd_d(含"审核拒绝"、"重新提交"等可重复事件) dim_trd_group_zip_d,每次状态变更生成新拉链行
浏览 / 点击 / 加购等行为日志(埋点) 一张统一事件明细表(见下"行为日志域例外"),非各拆一张
订单 vs 订单明细行(一对多) 各一张 _apd_d(粒度不同必拆)

行为日志域(埋点)例外——不按业务过程拆,走统一事件明细表

上表"每业务过程一张"是 业务过程事实(交易/履约域:下单/支付/发货/退款) 的范式——过程少、语义重、结构各异,拆开干净。行为日志(埋点点击/浏览/曝光/分享)是另一个域,反着来

  • 行为事件种类极多(HS 现 197+ 且在长)、浅、结构同构(公共属性 + params)。沿用"每动作一张表"会表爆炸;且 event explosion 下同名"点击"含异构 params,拢不进一张 typed 表。
  • 正确做法:一张统一埋点事件明细表——event_name 作列承载行为大类,公共维度拍平成列,params 保持半结构化(map/json)、不 per-event 拍平。要"点击表/浏览表"用 WHERE event_name=... 过滤,不必物理分表。
  • 核心业务转化事件(支付/退款等真业务过程)从日志表毕业,按本节范式独立成 typed 事实表(如 dwd_trd_order_pay_apd_d)。
  • 前提:事件粒度已收敛为行为大类;未收敛前(当前 event explosion)用统一表 + params map 兜底。详见 93-架构决策.md ADR-13。

循环状态机的处理:业务流程若存在循环(如拼团审核可能多次反复:"提交 → 拒绝 → 修改 → 再提交"),事件流水(_apd_d)天然支持任意次重复——每次状态变更追加一行,完整还原过程;DIM 拉链表(SCD Type 2)每次状态变更生成新行,旧行 end_date 置变更前一天,状态历史完整可回溯。

何时可考虑 acc:固定线性里程碑场景(不循环、里程碑可枚举且单向推进,如严格不可逆的合同审批流程),如有具体场景按需单独评估,不一刀切禁用。新建 acc 表时在 PR / 设计稿里说明选型理由。

自检:建表想给字段加"时间戳 + 若干状态字段"时,停一步问:这是事件还是状态?事件 → _apd_d;状态 → _zip_d

6. 数据同步策略

ODS 层从业务库/埋点/爬虫数据接入。关键问题:数据是否存在物理删除,决定增量策略。

数据来源 接入方式 快照类型 备注
业务库 (PG/MySQL) DataX + CDC inc(增量) 如存在物理删除,后续推行软删除
埋点 (Sensors → Kafka) Kafka → HDFS/Hive apd(追加) 不可变事件流
爬虫数据 爬虫落库 → DataX fulinc 按源站特性决定
维度数据 手工上传 / 配置化 ful(全量) 如国家映射、汇率表
一次性历史 / vendor 单批交付 本地 CSV → bin/csv-to-hdfs-starter.py his(一次性历史) 永不调度,导入后入档;周期段固定 _o

快照类型决策:

  • 数据创建后会被修改 → inc(增量快照)
  • 数据不可变 → apd(追加)
  • 缓慢变化维、需要保留历史轨迹 → zip(拉链表)
  • 每日重刷全表 → ful(全量)
  • 一次性导入、永不再跑 → his(与 ful 严格区分:ful 是周期性调度的全量重刷,his 是导入后归档的一次性快照)

7. 分区与存储策略

  • 分区字段dt(必须),STRING 类型,格式 YYYYMMDD(如 20260101);hr(小时,按需)
  • 存储格式:ORC(列存)
  • 纠删码:当前关闭(保持 3 副本),等 Worker 节点扩容后对冷数据启用

7.1 组合快照示例

拼团 DWD/DIM 组合:每个动作(发起 / 审核 / 拒绝 / 重新提交 / 成功 / 失败)独立 dwd_trd_group_{动作}_apd_d 事件表承载不可变流水;实体当前状态进 dim_trd_group_zip_d 拉链表,每次状态变更生成新拉链行。详见 §5.5。

7.2 各层分区语义

各层 dt 分区键代表的时间含义、对漂移和重复的容忍度、分区间关系不同,是设计 ETL 任务和理解查询行为的基础。

分区键语义 时间字段来源 漂移容忍 重复容忍 分区间关系
Raw 批次日(系统时间) dt = start_date(业务日) 容忍 容忍(同 pk 同 dt 内可多条) 独立,可含跨日漂移数据
ODS 记录最后写入日(系统时间) DATE(update_time) 不容忍 分区内不容忍、跨分区容忍(保留 update_time 轨迹) 独立,同 pk 可跨多分区
DWD 事实表 业务行为发生日(业务时间) DATE(order_create_time) / DATE(event_time) 等业务字段 不容忍 不容忍(事件不可变) 独立,追加写
DIM 拉链表 不按时间分区(或 is_current 二级分区) —— —— 多版本非重复[start_date, end_date) 区间不重叠) 每行是状态生效区间
DIM 快照表 快照日(业务时间) dt = today 不容忍 分区内不容忍;跨分区同 pk 多次出现是"每日快照"而非重复 独立,每分区是该日全量
DWS 统计截止日(业务时间) 聚合口径的截止点 不容忍 分区内不容忍;分区间原始明细冗余 分区间冗余(滑动窗口重叠)
ADS 报表快照日(业务时间) 报表生成日 不容忍 不容忍 独立,一行可含多统计周期指标

两个容忍度的概念:

  • 漂移容忍 = "某条记录落在了错误的 dt 分区里,能不能接受?" 描述数据 vs 分区归属的正确性
    • Raw 容忍:9 号 update_time 数据落在 Raw dt=8 号 没关系,下游会归位
  • 重复容忍 = "同一个 pk 在分区内或跨分区出现多次,能不能接受?" 描述分区内 / 跨分区的唯一性约束
    • Raw 容忍:一次抽取窗口内同 pk 可能被捞多次,不需要去重
    • ODS 分区内不容忍(需去重),跨 dt 容忍(保留轨迹)
    • DWD 事实表双不容忍:事件就是一次,多一条就是错
    • DIM 拉链表"多版本非重复":同 pk 多行表示状态演化

两个容忍度的组合反映各层的核心取舍:

  • Raw 双容忍 → 抽取简单,绝不丢数据
  • ODS 漂移不容忍 + 跨分区重复容忍 → 归位 + 保留轨迹
  • DWD 事实表双不容忍 → 事件唯一,业务语义精确
  • DIM 拉链表特殊 → 多版本不是重复
  • DWS / ADS 双不容忍 + 分区间明细冗余 → 聚合结果唯一,存储冗余换查询速度

7.3 各层职责与设计要点

各层基础职责见 §2;写入模式 / 回算窗 / 选型等操作细则见各层建模文档,本节只留指针,不与层文档重复。

关键设计取舍 详见
Raw 48h 宽窗抓取、全落 dt=start_date、容忍漂移 + 重复 22-业务库raw建模 §0.1 · 21-时间语义 §4 · ADR-03
ODS 动态分区按 update_time 归位、双源 union、跨 dt 不去重、INSERT OVERWRITE 22-业务库raw建模 §0.2 · 21-时间语义 §5 · ADR-03
DWD 业务时间分区、滚动 N=30 回算、事件 vs 状态拆分(见 §5.5) 23-dwd建模 · ADR-09 / ADR-11
DIM 状态承载、ful_d 默认 / zip_d 按需、拉链不按业务时间分区 24-dim建模 · ADR-08
DWS 主题 × 粒度日聚合、冗余换查询性能、维度退化 25-dws建模 · ADR-10
ADS 多周期拼一行、报表快照日分区、可从 DWS/DWD/DIM 重放 (ADS 文档待建)

7.4 分区与建模设计原则

原则 1:Raw / ODS 是系统时间,DWD 及以上是业务时间

  • Raw / ODS:系统时间(update_time 及 ETL 批次时间)。贴源层不解释业务语义,只忠实反映"数据库里发生了什么"
  • DWD / DIM / DWS / ADS:业务时间(order_create_time 等)。面向分析,分区语义对齐业务问题("8 号下了多少单"指的是用户 8 号下单的行为,不是数仓 8 号抓到的数据)

原则 2:漂移容忍度和重复容忍度自下而上递减

  • Raw:双容忍(漂移 + 重复都接受)
  • ODS:漂移不容忍、分区内重复不容忍、跨分区重复容忍(保留 update_time 轨迹)
  • DWD / DIM / DWS / ADS:漂移不容忍、分区内重复不容忍(DIM 拉链的"多版本"不算重复)

原则 3:分区语义和查询语义对齐

查询条件里的 where dt='8 号' 应该和用户的直觉对齐:

  • 分析师问"8 号下单数" → 查 DWD 事实表 where dt='8 号',分区键是业务下单日
  • 运维排查"8 号这批抓了什么" → 查 Raw where dt='8 号',分区键是批次日

两种"8 号"语义不同,分别由不同层承载,不混淆。

原则 4:事实与维度解耦(DWD vs DIM 并列)

  • DWD 事实表:记录不可变业务事件,只追加,业务时间分区
  • DIM 维度表:记录实体状态,拉链或快照,按规模和变更率选型
  • 跨表计算(下单数 - 退单数 = 实际下单数)是维度建模的正常能力,不是代价
  • 不把两者混在一层 → 建模方法、分区策略、更新方式清晰分离

原则 5:ODS 为下游保留最大灵活性

  • ODS 跨 dt 不去重 → 保留 update_time 轨迹 → 支持 DIM 拉链表建设
  • ODS 不预设下游建模方式 → DWD 可按表特征自由选事件化,DIM 可按规模选拉链 / 快照

原则 6:上层数据可重放,下层数据不可重放

  • Raw / ODS:源头层,业务库覆盖式更新会导致历史无法还原,不可重放,要长期保留
  • DWD / DIM:规整层,理论可从 ODS 重放,实务按"不可重放"管理,长期保留
  • DWS / ADS:派生层,可重放(前提上游数据还在),用 TTL 控制存储成本
  • 保留周期自下而上递减(越接近派生数据的层,TTL 越短)

9. 相关文档