01|数据湖架构:ODS、DWD、DWS、ADS 怎么分层
大数据架构的第一原则是让数据从“原始事实”逐层走向“业务可消费资产”。很多团队踩的坑不是技术选型,而是分层混乱:一张表里既有原始日志字段又有业务口径,下游谁都不敢动,最后变成一坨没人维护的“屎山表”。本文以一套电商用户行为数据为例,讲清楚 ODS、DWD、DWS、ADS 每一层的边界、判断标准和湖仓一体的落地方式。
背景:为什么必须分层
假设业务方提了一个需求:统计每个商品每天的“加购转下单转化率”。如果不分层,你可能直接写一个几百行的 SQL,从 Kafka 落地的原始 JSON 日志一路 join 到结果。三个月后埋点字段变了、口径要调整、另一个团队要复用“加购明细”,这个 SQL 就成了灾难。
分层的本质是用存储换可维护性:每一层只做一件事,下游永远基于稳定的上游构建。
核心概念:四层分工与数据量级
| 层级 | 目标 | 典型内容 | 更新方式 | 量级参考 |
|---|---|---|---|---|
| ODS | 保留原始数据 | 日志、业务库 CDC、第三方数据 | 增量追加 | 单日 TB 级原始日志 |
| DWD | 清洗成明细事实 | 用户行为明细、订单明细 | 分区覆盖 | 比 ODS 小 30%~50%(去脏数据) |
| DWS | 面向主题汇总 | 用户日汇总、商品日汇总 | 分区覆盖 | 比 DWD 小 1~2 个数量级 |
| ADS | 面向应用交付 | 看板表、推荐特征、报表宽表 | 分区覆盖/全量 | MB~GB 级,直接给应用查 |
ODS 尽量少加工,DWD 统一口径,DWS 聚焦复用,ADS 追求交付效率。在一个真实电商场景里,ODS 行为日志单日约 1.2TB、80 亿行,到 DWD 用户行为明细约 0.7TB、46 亿行(过滤了爬虫和无效埋点),DWS 商品日汇总只剩约 1200 万行,ADS 看板表通常不到 50 万行。越往上层数据越小、查询越快、稳定性要求越高。
分层判断标准
一张表该放哪层,问三个问题:
- 是否保留原始字段和原始粒度。
- 是否已经表达稳定业务概念。
- 是否直接服务某个应用或看板。
如果答案依次是“是、否、否”,通常是 ODS;如果是“否、是、否”,通常是 DWD 或 DWS;如果是“否、是、是”,通常是 ADS。
实战 Demo:电商行为数据四层落地
下面是一套可直接在 Hive/Spark SQL 中运行的最小四层示例。
ODS 层——原始埋点日志,只做落地不做清洗:
-- ods_user_event_log:直接映射 Kafka 落地的 JSON,字段全保留
CREATE TABLE IF NOT EXISTS ods.ods_user_event_log (
raw_json STRING,
kafka_offset BIGINT,
ingest_time STRING
) PARTITIONED BY (dt STRING) STORED AS PARQUET;DWD 层——解析、清洗、统一口径,得到明细事实表:
INSERT OVERWRITE TABLE dwd.dwd_user_action_di PARTITION (dt='${dt}')
SELECT
get_json_object(raw_json, '$.user_id') AS user_id,
get_json_object(raw_json, '$.item_id') AS item_id,
get_json_object(raw_json, '$.action') AS action_type, -- view/cart/order
CAST(get_json_object(raw_json, '$.ts') AS BIGINT) AS event_ts
FROM ods.ods_user_event_log
WHERE dt = '${dt}'
AND get_json_object(raw_json, '$.user_id') IS NOT NULL -- 去脏数据
AND get_json_object(raw_json, '$.is_robot') = 'false'; -- 去爬虫DWS 层——按商品+日期主题汇总:
INSERT OVERWRITE TABLE dws.dws_item_action_1d PARTITION (dt='${dt}')
SELECT
item_id,
SUM(IF(action_type='view', 1, 0)) AS view_cnt,
SUM(IF(action_type='cart', 1, 0)) AS cart_cnt,
SUM(IF(action_type='order', 1, 0)) AS order_cnt
FROM dwd.dwd_user_action_di
WHERE dt = '${dt}'
GROUP BY item_id;ADS 层——直接产出业务指标,给看板查:
INSERT OVERWRITE TABLE ads.ads_item_convert_1d PARTITION (dt='${dt}')
SELECT
item_id,
cart_cnt,
order_cnt,
ROUND(order_cnt / NULLIF(cart_cnt, 0), 4) AS cart_to_order_rate
FROM dws.dws_item_action_1d
WHERE dt = '${dt}';运行命令(以 Spark 为例):
spark-sql --master yarn -d dt=2026-05-10 -f build_item_convert.sql预期输出:ads_item_convert_1d 分区 dt=2026-05-10 生成约 1200 万行,单行包含 item_id / cart_cnt / order_cnt / cart_to_order_rate,看板查询单商品耗时从未分层时的分钟级降到 200ms 以内。
进阶:湖仓一体与表格式选择
传统 Hive 表只能分区覆盖,无法行级更新,CDC 场景很难处理。湖仓一体(Lakehouse)用 Iceberg/Hudi/Delta 这类表格式解决了这个问题:
- 写入频率高、需要 upsert(如订单状态变化):选 Hudi MOR 或 Iceberg,支持主键 upsert,避免每天全量重刷。
- 以追加为主、查询为重:Iceberg COW 或 Delta,文件合并和元数据管理更省心。
- 小文件治理:Iceberg 的
rewrite_data_files可把单日产生的上万个小文件合并成 128MB~512MB 的大文件,下游扫描元数据耗时可下降 60% 以上。
建模建议:
- 明细层尽量使用事实表和维表,不要提前塞太多业务判断。
- 汇总层要控制粒度,避免按所有维度做全组合,否则 DWS 行数会爆炸。
- 应用层可以冗余,但要注明来源和刷新频率。
- 数据湖表格式要结合写入频率和更新模式选择,不要无脑全用一种。
小结
分层不是为了好看,而是为了让数据在出问题时能定位、在变化时能复用。记住一句话:ODS 不加工、DWD 统口径、DWS 重复用、ADS 拼交付。湖仓一体只是把这套分层的存储底座换成了支持 upsert 和小文件治理的现代表格式,分层思想本身不变。

评论功能暂未开放
还没有评论
快来发表第一条评论吧