4、数据分层设计:ODS、DWD、DWS、ADS 逐层拆解
数据分层这件事,说白了就是给混乱的数据找个家。我刚开始做数仓时,接过一个项目,所有表都堆在一个 schema 里,业务方要个报表得翻半天。后来我痛下决心,按标准分层重构,效果立竿见影。
今天咱们就把 ODS、DWD、DWS、ADS 这四层掰开揉碎讲清楚。每一层该做什么、不该做什么,我踩过的坑也一并告诉你。
核心原则:每一层只解决一个核心问题。ODS 负责「存进来」,DWD 负责「洗干净」,DWS 负责「算好」,ADS 负责「给出去」。别让一层干两层的事。
4.1 ODS 层设计原则:先存进来再说
ODS 层,全称 Operational Data Store。我习惯叫它「数据码头」——所有数据先靠岸,别管干不干净。
核心原则就一条:保持源数据原样,不做任何业务逻辑清洗。
为什么?因为一旦在 ODS 层做了清洗,你就失去了回溯原始数据的能力。我曾经见过一个项目,ODS 层直接做了字段截断和类型转换,结果后来发现源数据有 bug,想查原始值已经找不回来了。
我的经验:ODS 层只做三件事——字段映射、类型兼容、分区落地。连空值都不要替换,保留原样。源库是 NULL 就存 NULL,源库是空字符串就存空字符串。
ODS 表的设计要点:
- 全量/增量策略:小表(百万级以下)每天全量拉取;大表用增量 + 分区合并。
- 分区键选择:按日期分区,dt 字段统一为 yyyyMMdd 格式。
- 字段命名:保持源字段名,加前缀 src_ 区分。比如 src_order_id、src_user_name。
- 数据质量标记:加一个 is_valid 字段,标记解析失败或格式异常的行。
-- ODS 层建表示例(Hive)
CREATE TABLE ods.ods_order (
src_order_id STRING,
src_user_id STRING,
src_product_id STRING,
src_amount DECIMAL(18,2),
src_order_time STRING,
src_status STRING,
etl_load_time TIMESTAMP,
is_valid INT COMMENT '1=有效 0=解析异常'
)
PARTITIONED BY (dt STRING)
STORED AS ORC;
注意:ODS 层不要建索引,不要做聚合,不要做关联。它的唯一使命就是「存」。查询性能问题交给下游层解决。
4.2 DWD 层设计:把数据洗干净
DWD 层,明细数据层。这是整个数仓最核心的一层,也是工作量最大的一层。
我个人的习惯是,DWD 层要完成三件事:清洗、维度退化、一致性处理。
清洗——把 ODS 层那些乱七八糟的数据规整好。比如时间字段统一成 TIMESTAMP 类型,金额字段去掉货币符号,状态码映射成中文描述。
维度退化——把常用的维度属性直接拉到事实表里。比如订单表里直接带上用户等级、商品分类、店铺名称,省得每次查询都要 join 维度表。
一致性处理——统一度量单位、统一编码规则、统一空值策略。比如所有金额字段统一为 DECIMAL(18,2),所有状态字段统一用 int 码表。
| 处理类型 | ODS 原始值 | DWD 处理后 | 说明 |
|---|---|---|---|
| 时间格式化 | 2024-01-15 14:30:22 | 2024-01-15 14:30:22.000 | 统一为 TIMESTAMP |
| 金额清洗 | ¥1,234.56 | 1234.56 | 去掉货币符号和千分位 |
| 状态映射 | 1 / 2 / 3 | 已支付 / 已发货 / 已完成 | 关联码表转义 |
| 空值处理 | NULL / '' / 'N/A' | NULL(统一) | 只保留一种空值语义 |
-- DWD 层建表示例
CREATE TABLE dwd.dwd_order_detail (
order_id STRING,
user_id STRING,
user_level STRING COMMENT '维度退化:用户等级',
product_id STRING,
product_category STRING COMMENT '维度退化:商品分类',
shop_name STRING COMMENT '维度退化:店铺名称',
amount DECIMAL(18,2),
order_time TIMESTAMP,
order_status STRING,
dt STRING
)
PARTITIONED BY (dt STRING)
STORED AS ORC;
避坑指南:我曾经在 DWD 层做过一次大范围的维度退化,把 20 多个维度属性全拉到事实表里,结果表宽超过 200 列,查询效率反而下降了。后来我总结:维度退化只保留查询频率最高的 5-8 个属性,其余的保持星型模型 join 即可。
4.3 DWS 层设计:公共指标预计算
DWS 层,汇总数据层。说白了就是「算好等查」。
为什么需要这一层?你想想看,如果每个报表都从 DWD 层几亿条明细里做聚合,那得跑多久?DWS 层就是把这些公共的、高频的聚合结果提前算好,存成宽表。
DWS 层的设计要点:
- 粒度选择:通常按天 + 维度组合。比如「每天每个用户的订单汇总」「每天每个商品的销售汇总」。
- 指标定义:所有指标必须有明确的业务口径和计算公式,不能有歧义。
- 公共性:只放被多个下游场景复用的指标。如果某个指标只有一个报表用,放到 ADS 层去算。
-- DWS 层建表示例:用户日汇总宽表
CREATE TABLE dws.dws_user_daily (
user_id STRING,
dt STRING,
order_count BIGINT COMMENT '下单次数',
order_amount DECIMAL(18,2) COMMENT '下单金额',
pay_count BIGINT COMMENT '支付次数',
pay_amount DECIMAL(18,2) COMMENT '支付金额',
refund_count BIGINT COMMENT '退款次数',
refund_amount DECIMAL(18,2) COMMENT '退款金额',
login_count BIGINT COMMENT '登录次数',
browse_count BIGINT COMMENT '浏览商品次数',
etl_update_time TIMESTAMP
)
PARTITIONED BY (dt STRING)
STORED AS ORC;
我的习惯:DWS 层表名统一加前缀 dws_,字段名用下划线分隔,指标字段后面加注释说明口径。这样新同事接手时不用猜「这个 amount 到底是下单金额还是支付金额」。
4.4 ADS 层设计:按需定制,灵活输出
ADS 层,应用数据层。这是离业务最近的一层,也是变化最频繁的一层。
ADS 层没有固定的设计模式,完全按业务需求来。可能是宽表、可能是多维分析表、也可能是接口专用的 KV 结构。
设计原则就两条:
- 按需构建:业务方要什么,你就给什么。不要提前造一些「可能有用」的表。
- 轻量化:ADS 层表尽量窄,字段控制在 20 个以内。太宽的表说明你还没想清楚业务到底要什么。
-- ADS 层示例:运营日报宽表
CREATE TABLE ads.ads_operation_daily (
report_date STRING,
new_user_count BIGINT,
active_user_count BIGINT,
order_user_count BIGINT,
total_order_count BIGINT,
total_order_amt DECIMAL(18,2),
avg_order_amt DECIMAL(18,2),
refund_rate DECIMAL(5,4),
etl_update_time TIMESTAMP
)
PARTITIONED BY (dt STRING)
STORED AS ORC;
注意:ADS 层不要做复杂的 ETL 逻辑。它应该只做简单的字段筛选和格式转换。复杂的业务逻辑放到 DWD 和 DWS 层去处理,ADS 层只负责「取数」。
4.5 各层对比与协作关系
最后,我用一张表总结四层的核心差异:
| 分层 | 核心职责 | 数据粒度 | 存储周期 | 使用方 |
|---|---|---|---|---|
| ODS | 源数据镜像 | 原始粒度 | 7-30 天 | DWD 层、数据回溯 |
| DWD | 清洗 + 维度退化 | 业务明细 | 30-90 天 | DWS 层、临时查询 |
| DWS | 公共指标汇总 | 轻度聚合 | 90-365 天 | ADS 层、BI 工具 |
| ADS | 应用定制输出 | 按需聚合 | 按业务需求 | 报表、接口、大屏 |
嗯,四层设计讲完了。记住一句话:ODS 存、DWD 洗、DWS 算、ADS 给。各层职责清晰,协作才能高效。下一层只依赖上一层,别搞出循环依赖,那是灾难。