数仓分层架构设计:ODS、DWD、DWS、ADS、DIM 如何各司其职

一、引言

电商业务每天都会产生订单、支付、退款、商品、库存、用户行为等数据。如果直接在业务数据库或原始日志上开发指标,同一个"成交金额"可能出现多种口径,重复计算、链路混乱和数据不可追溯也会随之而来。

数仓分层的作用,是按照数据在分析链路中的职责,把原始数据逐步加工成可复用的明细事实、统一维度、公共汇总和应用结果。分层之后,链路可以拆成下面的结构:

lua 复制代码
业务系统 / 日志系统
        |
        v
+------------------+
| ODS 原始数据层   |  保存源数据,负责可追溯
+------------------+
        |
        | 清洗、去重、标准化、业务过程建模
        v
+------------------+        +------------------+
| DWD 明细事实层   | <----> | DIM 公共维度层   |
+------------------+        +------------------+
        |
        | 按公共分析粒度汇总
        v
+------------------+
| DWS 公共汇总层   |  沉淀可复用指标
+------------------+
        |
        | 面向具体产品组装
        v
+------------------+
| ADS 应用数据层   |  看板、报表、接口、画像
+------------------+

每一层解决一类问题:

  • ODS 回答:"源系统当时产生了什么?"
  • DWD 回答:"发生了什么业务事实?"
  • DIM 回答:"事实中的人、货、场分别是谁?"
  • DWS 回答:"按公共分析对象汇总后表现如何?"
  • ADS 回答:"某个数据产品最终需要展示或输出什么?"

二、五层职责总览

分层 核心职责 典型粒度 主要加工 服务对象
ODS 接入并保存原始数据 与源表或源事件一致 采集、落盘、基础格式校验 DWD、审计、重跑任务
DWD 构建标准化明细事实 一次业务过程或最细事实 清洗、去重、关联、状态解析、维度退化 DWS、明细分析、数据科学
DIM 建立一致性维度 一个维度成员或其历史版本 编码统一、属性整合、层级维护、拉链处理 DWD、DWS、ADS
DWS 建立公共汇总模型 主题对象 × 统计周期 聚合、派生指标计算、宽表化 ADS、分析人员、指标平台
ADS 服务具体应用 由消费场景决定 指标组装、排名、筛选、展示适配 BI、运营、风控、数据 API

这只能作为起点,真正设计分层时,首先要声明 粒度 。一张表如果不能用一句话准确说明"一行代表什么",后续指标很可能发生重复计算。

三、ODS:保留原貌,而不是放任混乱

ODS 是数据进入数仓后的第一站,定义为保存未经处理的原始数据、结构与源系统基本一致的数据准备区,同时承担记录基础数据历史变化的职责。

电商场景的 ODS 数据通常来自:

复制代码
MySQL / PostgreSQL
  ├─ order_info          订单主表
  ├─ order_item          订单明细表
  ├─ payment_record      支付记录
  ├─ refund_record       退款记录
  ├─ product_info        商品信息
  └─ user_info           用户信息

App / Web 日志
  ├─ page_view           页面浏览
  ├─ product_click       商品点击
  ├─ add_to_cart         加购
  └─ submit_order        提交订单

外部数据
  ├─ 广告投放数据
  ├─ 物流轨迹
  └─ 客服工单

离线链路一般通过全量或增量同步写入 ODS;实时链路常通过 CDC 读取数据库变更日志,再写入 Kafka。

ODS 表的粒度应尽量与来源一致:

ODS 表 一行代表什么
ods_order_info 源订单表的一条订单记录或变更记录
ods_order_item 一个订单中的一个商品明细
ods_payment_record 一次支付流水或支付状态变更
ods_page_view_log 用户的一次页面浏览事件
ods_product_info 源商品表的一条商品记录或变更记录

对于 CDC 数据,需要进一步区分两种存储语义:

sql 复制代码
日志型 ODS:一行代表一次 INSERT / UPDATE / DELETE 变更
快照型 ODS:一行代表某个时间点的最新业务状态

两者不能混用。日志型 ODS 适合追溯变化过程,快照型 ODS 适合读取当前状态。

ODS 可以执行有限的接入处理:

  • 增加采集时间、来源系统和批次号;
  • 记录 CDC 操作类型、源库位点等元数据;
  • 统一无法解析的文件格式;
  • 对敏感字段进行加密、脱敏或访问隔离;
  • 将异常记录送入隔离区。

ODS 不宜直接承担业务口径计算。例如,"支付状态为 20 表示成功"属于业务语义解析,更适合放在 DWD。

ODS 设计示例:

css 复制代码
表名:ods_trade_order_item_inc
粒度:订单明细表的一次变更
来源:MySQL trade.order_item
更新:CDC 实时接入,离线按天归档

关键字段:
  order_id              订单编号
  order_item_id         订单明细编号
  product_id            商品编号
  sku_id                SKU 编号
  quantity              购买数量
  original_amount       原始金额
  discount_amount       优惠金额
  actual_amount         应付金额
  status                源系统状态码
  db_operation          I / U / D
  source_ts             源库变更时间
  ingest_ts             数仓接入时间
  dt                    归档日期

Hive分区表会按照分区列值组合建立独立目录,查询可以利用分区缩小扫描范围。离线 ODS 常按dt 分区,但应注意:

  • dt 是存储组织方式,不等于业务发生时间;
  • 补数时,采集日期与业务日期可能不同;
  • 实时 ODS 还需要保留source_ts 和ingest_ts ,用于判断延迟和乱序。

四、DWD:把源记录转化为业务事实

DWD 即明细数据层,以业务过程为建模驱动,针对具体业务过程构建最细粒度的明细事实表,并可根据使用特点适当冗余重要维度属性。这里的关键不是"明细"两个字,而是 业务过程 。

电商常见业务过程包括:

  • 用户浏览商品;
  • 用户加入购物车;
  • 用户提交订单;
  • 订单支付成功;
  • 商家发货;
  • 用户确认收货;
  • 用户申请退款;
  • 退款成功。

一个业务过程通常对应一类事实表。

rust 复制代码
下单过程  -> dwd_trade_order_item
支付过程  -> dwd_trade_payment
退款过程  -> dwd_trade_refund
浏览过程  -> dwd_traffic_page_view
加购过程  -> dwd_traffic_add_cart

不要把所有过程强行压进一张"大而全"的订单宽表。下单、支付和退款具有不同的发生时间、生命周期和数量关系。合并过早会掩盖粒度差异。

以订单为例,可以存在多种合理粒度:

objectivec 复制代码
订单粒度:
  一行 = 一个订单

订单明细粒度:
  一行 = 一个订单中的一个 SKU

事件粒度:
  一行 = 一次订单状态变更

支付流水粒度:
  一行 = 一次支付尝试

退款明细粒度:
  一行 = 一个订单明细的一次退款

假设一个订单购买 3 个 SKU,并发生 2 次支付尝试和 1 次部分退款:

复制代码
订单主表       1 行
订单明细表     3 行
支付事实表     2 行
退款事实表     1 行或多行

如果直接把支付表与订单明细表关联,可能得到3 × 2 = 6 行,支付金额也会被重复累计。正确做法是先明确目标粒度,再把其他数据聚合到相同粒度后关联。

DWD 主要执行以下处理:

  1. 数据清洗
  • 过滤测试订单;
  • 识别无效主键;
  • 修正字段类型;
  • 处理非法时间和异常金额;
  • 标记脏数据原因。
  1. 去重与幂等

支付网关可能重复回调。可以按支付流水号和版本号保留有效记录:

复制代码
唯一业务键:payment_id
排序依据:source_update_time、binlog_position
保留规则:最新有效版本

去重规则应写入模型文档,不能只藏在 SQL 的ROW_NUMBER() 中。

  1. 状态标准化

不同渠道可能使用不同状态码:

yaml 复制代码
微信支付:SUCCESS
支付宝:TRADE_SUCCESS
银行卡:02
             |
             v
统一支付状态:PAY_SUCCESS
  1. 金额拆解

订单商品的金额关系应显式定义:

diff 复制代码
原价金额
- 商品优惠
- 店铺优惠
- 平台优惠
+ 运费
= 应付金额

如果平台券需要按商品分摊,应规定分摊算法、精度和尾差归属,否则订单级金额无法稳定下钻到商品级。

  1. 维度键映射

DWD 可以关联 DIM 获取代理键或稳定业务编码,例如:

rust 复制代码
product_id -> product_sk
user_id    -> user_sk
shop_id    -> shop_sk

但需要警惕实时链路中的维度延迟:事实已经到达,维度数据可能还没有同步。常见处理方式包括异步重试、临时未知键、维表回补和事实纠正流。

DWD 设计示例:

css 复制代码
表名:dwd_trade_order_item
粒度:一行代表一个订单中的一个 SKU

事实字段:
  item_quantity
  original_amount
  discount_amount
  payable_amount

维度键:
  user_id
  shop_id
  product_id
  sku_id
  category_id

过程时间:
  order_create_time
  pay_time
  cancel_time

治理字段:
  source_system
  source_record_id
  etl_time
  dt

DWD 表中的可加事实必须与粒度匹配。payable_amount 可以在订单明细粒度求和;"用户数"不是明细事实,通常需要在 DWS 中对user_id 去重统计。

五、DIM:让"人、货、场"拥有统一解释

DIM 是公共维度层,用于建立企业一致性维度,降低不同业务线在编码、属性和层级上的口径差异。

电商数仓常见维度包括:

objectivec 复制代码
人:用户、会员、商家、员工
货:商品、SKU、品牌、类目
场:店铺、渠道、终端、地区
时间:日期、周、月、季度、节假日
活动:营销活动、优惠券、广告计划

维度表保存描述性属性,即事实表保存交易或度量数据,维度表保存用于描述这些事实的数据属性。

DIM 的设计粒度:

objectivec 复制代码
dim_user_current
  一行 = 一个用户的当前信息

dim_product_current
  一行 = 一个商品的当前信息

dim_sku_current
  一行 = 一个 SKU 的当前信息

dim_product_history
  一行 = 一个商品在一段有效时间内的属性版本

dim_date
  一行 = 一个自然日

"商品"和"SKU"往往不是同一粒度:

objectivec 复制代码
商品:某款运动鞋
SKU:黑色、42 码的该款运动鞋

销售事实通常发生在 SKU 粒度,运营分析则可能按商品、品牌或类目汇总。

假设商品 1001 在 5 月 10 日从"运动鞋"类目调整到"户外鞋"。

如果只保存当前类目,那么查询 4 月销售额时,这笔历史交易也可能被归入"户外鞋"。如果分析要求按照交易发生时的类目归属,就需要保存维度历史。

yaml 复制代码
product_id | category | valid_from | valid_to   | current_flag
-----------+----------+------------+------------+-------------
1001       | 运动鞋   | 2026-01-01 | 2026-05-09 | 0
1001       | 户外鞋   | 2026-05-10 | 9999-12-31 | 1

事实表按业务发生时间匹配有效区间:

vbnet 复制代码
fact.product_id = dim.product_id
AND fact.order_time BETWEEN dim.valid_from AND dim.valid_to

是否采用历史维度取决于业务问题:

  • 查询当前商品归属:使用当前快照;
  • 还原交易当时归属:使用历史版本;
  • 监管或财务审计:通常要求保留历史版本。

订单域和流量域都使用统一的商品、用户和渠道维度后,跨域分析才有稳定基础:

rust 复制代码
流量事实 + 商品维度 -> 商品访问人数
交易事实 + 商品维度 -> 商品支付金额

访问人数、支付金额
        |
        v
商品支付转化率

如果两个域各自维护一套类目映射,"访问类目"和"支付类目"就可能无法对齐。

六、DWS:沉淀公共粒度和统一指标

DWS 是公共汇总层,以分析主题对象为驱动,构建公共粒度的汇总事实表,沉淀口径一致、可复用的统计指标。

DWS 不只是"把 DWD 做一次GROUP BY ",它要回答三个问题:

  1. 按什么分析对象汇总?
  2. 汇总到什么时间周期?
  3. 哪些指标值得被多个应用复用?

典型 DWS 粒度包括:

复制代码
用户 × 日
商品 × 日
店铺 × 日
渠道 × 日
活动 × 日
用户 × 商品 × 日
店铺 × 类目 × 日

表名可以体现粒度:

复制代码
dws_trade_shop_day
dws_trade_product_day
dws_user_trade_day
dws_traffic_channel_day
dws_shop_category_day

简单示例:

复制代码
表名:dws_trade_shop_day
粒度:一行代表一个店铺在一个自然日的交易汇总

维度:
  stat_date
  shop_id

指标:
  order_user_count
  order_count
  order_amount
  pay_user_count
  pay_order_count
  pay_amount
  refund_amount

DWS 中最好区分原子指标和派生指标。

原子指标通常可以表示为:

arduino 复制代码
业务过程 + 度量 + 统计口径


//示例
支付成功 + 订单金额 + 去除测试订单
= 支付金额

派生指标在原子指标上增加时间、维度或计算条件:

复制代码
店铺日支付金额
= 支付金额 + 店铺维度 + 自然日周期

店铺日客单价
= 店铺日支付金额 / 店铺日支付人数

比值类指标应避免先算比例再汇总。例如,不能直接把各商品转化率求平均得到店铺转化率;正确方式是分别汇总分子和分母:

复制代码
店铺支付转化率
= 店铺支付用户数 / 店铺访问用户数

一个指标进入 DWS,通常应满足:

  • 有明确且稳定的业务定义;
  • 被多个报表或应用使用;
  • 粒度能够清晰声明;
  • 可由 DWD 和 DIM 重算;
  • 统计周期与更新策略明确。

只为单个页面临时计算的字段不必全部进入 DWS,否则公共层会变成应用需求的堆积区。

七、ADS:面向应用交付,而不是再次建设公共模型

ADS 面向具体数据产品,存放个性化统计指标和应用结果。

典型消费场景包括:

  • 经营分析看板;
  • 商品排行榜;
  • 店铺诊断报告;
  • 用户标签接口;
  • 实时大屏;
  • 风控规则输入;
  • 营销人群圈选;
  • 对外数据 API。

ADS 粒度由消费场景决定:

复制代码
经营日报:
  一行 = 日期 × 店铺

商品排行榜:
  一行 = 日期 × 类目 × 商品

用户画像:
  一行 = 用户

实时大屏:
  一行 = 分钟 × 渠道,或一个指标的最新状态

数据 API:
  一行 = 接口约定的业务对象

简单示例:

ini 复制代码
表名:ads_shop_operation_dashboard
粒度:一个店铺在一个自然日的经营结果

字段:
  stat_date
  shop_id
  visitor_count
  pay_user_count
  pay_order_count
  pay_amount
  refund_amount
  avg_order_value
  pay_conversion_rate
  shop_rank


//指标统计
avg_order_value
= pay_amount / pay_user_count

pay_conversion_rate
= pay_user_count / visitor_count

ADS 可以为了查询性能预先计算比例、排名和展示标签,但必须保留分子、分母或对应公共指标来源,便于解释和复算。

ADS 不宜:

  • 直接清洗原始 CDC 数据;
  • 自行解释支付状态码;
  • 重新维护商品类目映射;
  • 复制一套与 DWS 不同的公共指标口径;
  • 成为所有下游需求的永久宽表。

ADS 中的逻辑应以"组装和交付"为主,公共业务语义应尽量在 DWD、DIM 和 DWS 中解决。

八、离线数仓与实时数仓如何共用分层思想

分层是一套逻辑架构,不等同于某一种计算引擎。离线和实时可以采用相同的数据模型,但实现方式不同。

8.1.离线链路

sql 复制代码
业务库 / 日志
      |
      v
批量同步到 ODS
      |
      v
Spark SQL / Hive SQL
      |
      +--> DWD 明细
      +--> DIM 维度
      +--> DWS 日汇总
      +--> ADS 报表

常见特点:

  • 按小时或按天调度;
  • 通过分区覆盖保证幂等;
  • 允许全分区重算;
  • 延迟较高,但逻辑容易复现;
  • 适合财务报表、经营日报和历史分析。

8.2.实时链路

lua 复制代码
数据库 binlog / 用户事件
           |
           v
     CDC / Kafka
           |
           v
   Flink / Spark Streaming
           |
           +--> 实时 ODS 事件流
           +--> 实时 DWD 标准事件
           +--> 实时 DIM 查询或广播
           +--> 实时 DWS 窗口聚合
           +--> 实时 ADS / OLAP / KV

实时数仓还必须处理三个额外问题:

  1. 事件时间

业务指标通常应按事件发生时间计算,而不是计算引擎收到数据的时间。

  • Processing Time:算子处理事件时机器上的系统时间;
  • Event Time:事件在业务端实际发生的时间。

支付事件发生在 23:59,但因网络延迟在次日 00:01 到达。如果按处理时间计算,它会进入次日;如果按事件时间计算,它应归属前一日。

2.乱序和迟到数据

Flink 使用 Watermark 表示事件时间的推进,并根据 Watermark 判断窗口何时可以触发。现实系统中可能存在迟到事件,过度等待则会增加结果延迟。因此实时指标需要明确:

复制代码
允许迟到多久?
窗口何时首次输出?
迟到数据是否更新历史结果?
超过允许迟到范围后进入哪里?

3.有状态计算

实时去重、窗口聚合和订单状态跟踪都需要保存状态。Flink 将窗口聚合、事件模式识别等操作归为有状态计算,并通过 checkpoint、savepoint 等机制支持故障恢复。Spark Structured Streaming 同样支持事件时间窗口和 Watermark,并用 Watermark 清理超过迟到阈值的历史状态。

8.3.离线与实时口径必须汇合

实时结果追求低延迟,离线结果追求完整性。两条链路如果各写一套业务逻辑,最终很容易出现:

复制代码
实时大屏支付金额:1000 万
次日离线报表金额:980 万

造成差异的常见原因包括:

  • 实时窗口尚未接收迟到数据;
  • 实时维度尚未更新;
  • 离线链路过滤了测试订单;
  • 退款回冲时间口径不同;
  • 去重键或状态判断不一致。

较稳妥的做法是:

  • 统一业务主键和事件定义;
  • 统一指标口径与维度编码;
  • 实时结果允许修正;
  • 离线链路负责最终校准;
  • 建立实时与离线结果的日终对账。

九、表格式、分区和分层不是一回事

数仓分层描述的是数据职责,Hive、Iceberg 等表格式解决的是数据如何存储、查询和演进。

sql 复制代码
逻辑分层:ODS / DWD / DIM / DWS / ADS
计算引擎:Spark / Flink
消息系统:Kafka
表格式:Hive Table / Iceberg
查询引擎:Trino / Spark SQL / Flink SQL
服务存储:ClickHouse / Doris / Elasticsearch / Redis

这些技术可以组合,但不能把某种表格式等同于某一层。

传统 Hive 分区表常按日期组织数据,Iceberg 支持通过元数据执行 Schema Evolution 和 Partition Evolution;旧数据可以继续使用原分区布局,新数据采用新布局,不需要立即重写全部数据文件。

这类能力可以降低表结构演进和回溯重算的成本,但不会自动解决业务粒度、指标口径和分层边界问题。

十、分层设计步骤

以一个电商主题为例,可以按下面的顺序设计。

第一步:识别业务过程。

rust 复制代码
浏览 -> 加购 -> 下单 -> 支付 -> 发货 -> 收货 -> 退款

每个过程是否产生可记录的业务事实?

第二步:声明事实粒度。

objectivec 复制代码
订单明细事实:一个订单中的一个 SKU
支付事实:一次支付流水
退款事实:一次退款明细

第三步:确定事实和维度。

复制代码
事实:
  数量、金额、次数、时长

维度:
  用户、商品、店铺、渠道、地区、日期、活动

第四步:设计 ODS 接入和追溯策略。

  • 全量还是增量;
  • 快照还是变更日志;
  • 数据保留周期;
  • 分区字段;
  • CDC 主键和位点;
  • 删除事件如何表达。

第五步:设计 DWD 标准化规则。

  • 唯一业务键;
  • 去重顺序;
  • 状态映射;
  • 金额关系;
  • 测试数据识别;
  • 维度缺失处理;
  • 数据质量规则。

第六步:建设一致性 DIM。

  • 维度业务主键;
  • 代理键是否需要;
  • 当前值还是历史版本;
  • 属性来源优先级;
  • 层级和编码标准。

第七步:提炼 DWS 公共粒度。

从多个应用需求中寻找稳定交集,例如:

复制代码
店铺 × 日
商品 × 日
用户 × 日
渠道 × 日

第八步:按消费场景建立 ADS。

最后再处理:

  • 页面字段;
  • 排名;
  • 比率;
  • 数据接口结构;
  • 查询性能;
  • 服务存储选型。

十一、小结

ODS、DWD、DIM、DWS、ADS 解决的是数据从产生到消费过程中的职责划分:

vbnet 复制代码
ODS:保留来源,支持追溯
DWD:统一明细事实
DIM:统一分析视角
DWS:复用公共指标
ADS:服务具体应用

设计分层时,表名和技术选型排在粒度之后。先确定业务过程,再声明"一行代表什么",随后定义事实、维度、时间口径和更新方式。只要粒度稳定、口径统一、依赖清晰,即使底层从 Hive 迁移到 Iceberg,从 Spark 批处理扩展到 Flink 实时计算,数仓模型仍然可以延续。

相关推荐
东鸿电子Eastron7 小时前
工厂节能降本落地指南:从精准能源计量搭建数字化管理基础
大数据·人工智能·能源·智能电表·能耗管理
吾AI科技7 小时前
基于Tez引擎的 Hive SQL 性能优化 —— 为什么脚本运行越来越慢
大数据·hive·性能优化·tez
P1Browser1 天前
账号触发平台安全审核?拆解标准申诉流程与材料清单
大数据·人工智能·安全·多账号管理·指纹浏览器·账户安全·运营合规
云南新人和数采集团1 天前
云南靠谱软件开发公司选型指南:以昆明新腾科技为例
大数据·人工智能·科技
AllData公司负责人1 天前
AI大模型数据开发底座系列说明书:AllData集成DB-GPT开源项目建设 AI 多模态分析底座,涵盖模态解析/智能大屏/MCP 扩展!
大数据·数据库·人工智能·gpt·数据采集·数据接入·模态解析
wno7041 天前
常用的Git命令清单
大数据·git·elasticsearch
YIN_尹2 天前
【git原理与使用】git基本操作及底层原理(二)
大数据·git·elasticsearch
武子康2 天前
H3-World 写了 CPU offload,为什么还不能保证塞进 48GB?
大数据·人工智能·深度学习
演艺大赏2 天前
Doris 数据导出实战:SeaTunnel、EXPORT 与 OUTFILE 性能对比及 9.5 亿行写入故障复盘
大数据