数仓分层从入门到实战:ODS、DWD、DWS、ADS核心架构与落地指南
2026/9/23 10:50:23 网站建设 项目流程

1. 为什么非要做数仓分层:前因后果与核心价值

在数据开发这个圈子里,数仓分层这四个字几乎每天都挂在嘴边。但说实话,真正能把分层讲清楚、把每一层的边界划明白、把分层的价值落到实处的团队,并没有想象中那么多。很多人只是照着别人的模板画了三层或者五层图,实际开发的时候还是各写各的,最后数据链路乱成一团。

先说个场景。某个业务方跑过来跟你说,我需要看一下最近三个月每天的新增用户数、活跃用户数和订单转化率。听起来很简单对吧?你打开原始日志表,然后开始写 SQL。刚写了不到五十行,你发现要对 user_id 做去重,要去掉测试账号,要过滤掉爬虫流量,要把 iOS 和 Android 的埋点口径统一,要join 订单表算出转化率。写完这条 SQL 花了半天,上线之后发现第二天数据对不上了,因为上游埋点又加了一个新的事件类型,你的过滤条件没覆盖到。

这就是不分层的后果。每一个需求过来,你都要从头开始面对这些脏活累活,每一张报表、每一个指标背后都重复着一套口径复杂、逻辑混乱的加工过程。数据仓库分层,本质上是把这种无序的、重复的、口径不统一的数据加工过程,变成一条有序的、职责清晰的、可复用可追溯的数据流水线。

这篇文章适合谁看?刚接触数据仓库、被领导要求搭建数仓体系的新手,写了两年 SQL 但没系统思考过分层逻辑的开发,以及团队里需要推动数仓规范落地的技术负责人。读完你能搞清楚数仓为什么分层、经典的分层架构每层到底干什么、具体落地的时候有哪些细节坑,以及小型团队怎么在自己有限的资源下做合理分层。

2. 经典数仓分层架构:每一层到底在做什么

2.1 ODS 层:源系统数据的忠实镜像

ODS 层,也就是操作数据存储层,是整个数仓的源头。这一层做的事情很简单:把各个业务系统、第三方平台、日志采集端的数据,尽可能原封不动地同步到数仓的存储引擎中,比如 Hive、Iceberg、Hudi 或者 ClickHouse 里。

ODS 层的数据一般分成两类。一类是业务库数据,通常通过 DataX、Flink CDC、MaxCompute 的同步任务,把 MySQL、PostgreSQL、Oracle 里的表结构完整地搬过来。另一类是日志数据,比如用户行为日志、服务器访问日志,通过 Flume、Logstash、Kafka 或 SLS 等工具收集,落成 Hive 表或者存成列式文件。

ODS 层的核心原则是忠实同步、不做或少做加工。为什么要有这一层?有三个原因。源系统的数据库不能随便查询,你直接连生产库跑一个大 SQL,可能影响人家的线上业务。数据格式和语义在源端是不稳定的,今天埋点的字段叫 item_id,明天可能改成 product_id,你必须有地方先存一份原始快照,才能追溯口径变化的历史。数据同步过程本身会有延迟、会有重复、会有失败,这些都需要一个缓冲层来做容错和重试。

实际操作中,ODS 层的表用每天全量快照还是增量分区,取决于源表的数据量、变更方式和业务需求。小表直接做全量,每天一个分区,简单粗暴。大表要做增量,用 insert_time 或 update_time 来识别新增和修改的数据。这里有一个经验:增量表最好额外加一个分区字段,一个分区跑当日新增,另一个分区跑当日更新,这样后期排查数据对不上时能节省大量时间。另外,每次同步任务跑完,要记录 row_count 到一张调度日志表里,这是之后数据质量校验的重要依据。

2.2 DWD 层:清洗与明细数据的战场

DWD 层是明细数据层,也经常被叫做数据清洗层。这一层是整个数仓分层里工作量最大、最考验功底的部分。DWD 做的事情,是对 ODS 层的原始数据做标准化、清洗、脱敏、维度退化,然后重新组织成面向业务过程的明细事实表。

很多人会问,DWD 和 ODS 的区别到底在哪里?ODS 是源表的搬运工,DWD 是源表的加工者。在 DWD 层,你至少要做这几件事:把公共字段统一,比如日期格式全部转成 yyyy-MM-dd,金额单位统一成元,性别枚举值统一成 0/1/2;做数据清洗,过滤掉明显为空的订单号、金额为负数或者超出合理范围的异常值;做完用户标识统一,把匿名用户 ID、注册用户 ID、设备 ID 之间的映射关系理顺,生成一个统一的全局用户标识。字段级别的脱敏也基本在这一层做,比如把手机号、身份证号替换成脱敏字符串,避免敏感数据继续往下游扩散。

到这里,“明细”两个字怎么理解?在 DWD 层,一行数据应该对应一个真实业务过程的最小粒度,比如一条订单记录对应一笔订单的一次购买行为,一条日志记录对应用户的一次点击。DWD 层不建议做汇总聚合,一旦做了聚合,明细就丢了,下游想从别的维度分析就没有抓手了。当然,如果你用的是星型模型,会在 DWD 层按事实表和维度表分别组织,事实表存业务过程的度量值,维度表存描述性信息。

DWD 层还有一个常被忽视的作用:口径固化。这里说口径固化指的是把业务上容易混淆的统计口径通过代码和结构,定型成一种固定的计算方式。比如有效订单怎么定义?是支付成功且未取消的订单,还是支付成功就算?不同团队如果各写各的,最后两张报表对不上,业务方就要来质问。在 DWD 层统一完成清洗和过滤之后,下游不管谁做分析,拿到的都是一套已经加工好的明细数据。

2.3 DWS 层:主题与汇总的核心

DWS 层,服务数据层,也有人叫汇总层、公共汇总层。这一层基于 DWD 层的明细数据,按照业务分析的主题域,做多维度的汇总统计。简单理解,DWD 是数据的最细粒度,DWS 是面向分析场景的轻度汇总结果。

为什么要有这一层?因为业务方看数据的时候,大多数人不会说“我要每天每笔订单每个商品的明细”,他们说“我要看看最近三十天每天的销售额趋势”、“每个省份的订单量排名”、“每个品类的退货率”。这些需求无一例外都是聚合查询。如果每次都从 DWD 层原始明细去跑聚合,一次还好,十张报表、二十个指标同时跑,对整个集群的算力消耗是巨大的,查询响应时间也会非常难看。DWS 层的价值,就是把这种高频的聚合查询结果提前算好、固化下来。

这一层的粒度通常是“维度组合 + 时间周期”的粒度。最典型的做法是建一张按天、按用户维度汇总的宽表,包含用户当天的 PV、UV、下单数、支付金额、加购数等指标。再比如按天、按商品维度汇总的销售宽表,包含销量、销售额、库存周转等指标。这样下游报表直接查 DWS,秒级出结果。

DWS 层的设计要跟着业务问问题的方式走。业务经常按用户维度分析,你就建用户汇总表;按地区维度分析,就建地区汇总表;按商品维度分析,就建商品汇总表。这里有一个容易犯的错误:试图让明细和汇总互相替代。汇总表替代不了明细表,因为现场出了运营问题,你需要钻取到具体哪条订单出了问题;明细表也替代不了汇总表,因为老是从明细跑聚合,性能扛不住。职责不同,各司其职,不要合在一起。

2.4 ADS 层:应用数据层,让报表和数据产品直接消费

ADS 层,应用数据层,也叫数据应用层。这一层直接对接下游的数据消费方,比如业务报表、数据大屏、用户画像系统、推荐系统的特征数据、算法模型训练样本等。ADS 是距离用户最近的一层,也是需求变动最频繁的一层。

ADS 层的数据形态比较自由,经常按应用场景单独建表。比如针对一个经营管理驾驶舱,你可能需要一张大宽表,把日活、新增、订单量、GMV、转化率、客单价、退款率等几十个核心指标都放在同一行记录里。这种表字段非常多,但每个字段都是DWS已经算好的结果,ADS 只是做组合和排列,查询时直接 select 出来,效率极高。

这一层还有一个特点,就是容忍一定的冗余。为了减少查询时的 join 次数,ADS 表经常会把指标值和对应的维度名称直接冗余在同一张表里,甚至可以存在 Redis 或者 Elasticsearch 这类OLTP或检索引擎中,而不是全部放在数仓引擎里。在实时场景中,Flink 计算的实时指标结果也经常会落到 ADS 层的存储中,供前台页面秒级刷新。

ADS 层的命名要和应用场景强关联,比如一句话需求标题是“管理层经营驾驶舱”,ADS 表名可能就叫 ads_management_dashboard_daily。表名跟着场景走,后期定位和维护会轻松很多。

2.5 辅助层:DIM 维表层与 TMP 临时层

除了 ODS、DWD、DWS、ADS 这四条主线之外,实际落地时几乎每个数仓还会有 DIM 维表层和 TMP 临时层。

DIM 维表层专门管理维度数据,比如用户维度表、商品维度表、门店维度表、日期维度表。维度表和事实表是星型模型的两个核心组成部分,事实表存放的是度量值和关联维度表的外键,维度表存放的是描述性属性。维表通常使用缓慢变化维策略来处理历史变化,比如用户所在的城市从上海变成了北京,你要决定是直接覆盖(SCD1),还是保留历史并新增一行(SCD2)。日常开发里,90% 的维度用 SCD1 就够了,只有像用户会员等级、地址信息这类需要追溯历史的场景才用 SCD2。

TMP 临时层是给开发人员放中间结果的,不需要长期保留。比如你在开发一个复杂的多层 SQL 任务,中间步骤产生了很多中间表,这些表生命周期短,数据量大,定期清理即可。很多团队会把 TMP 表用特殊前缀标识,方便调度系统统一清理。小型数仓可以不单独建 TMP 层,但大型团队最好有,否则开发者会顺手把临时表放在 DWD 或 ADS 里,破坏分层的纯净性。

2.6 数据流转的完整链路和各层衔接关系

把上面几层串起来,一条数据从源端产生到最终被业务消费,流程大概是下面这样:

业务数据库/日志 → 同步任务 → ODS 原始数据表 ODS → 清洗、标准化、维度退化 → DWD 明细事实表 + DIM 维度表 DWD/DIM → 按主题聚合计算 → DWS 公共汇总表 DWS → 按应用组装、组合 → ADS 应用数据表 ADS → 被 BI 报表、数据大屏、画像服务、算法特征读取

每层之间的衔接靠调度系统来保障。ODS 层的同步任务完成后,DWD 层的清洗任务才能启动。DWD 完成后,DWS 聚合任务才会运行。这就是为什么在调度平台上每条任务都要显式声明依赖上游任务,而不是靠时间定时。如果只是定时,前面同步延迟严重时,后面任务用到的数据还是旧的,整条链路上的报表都会出错。数据仓库分层搭配上严格的调度依赖,才是真正可靠的体系。

3. 分层落地实践:从需求到建模再到开发全流程

3.1 业务需求调研:分层设计的第一步

很多人设计数仓,上来就画分层图,画完就开始建表。这是本末倒置的。分层的最终目的是服务业务需求,所以第一件事是搞清楚业务方到底要什么。这里分享一个我实际踩过坑后的标准动作。

先收集业务方现在在用和未来半年想用的报表清单,至少包含这些信息:报表名称、使用部门、访问频率、需要哪些指标、指标的业务口径、支持哪些维度筛选、数据延迟的要求是 T+1 还是实时。把这些信息整理成一张需求矩阵后,再开始做主题域划分。

主题域划分是什么意思?就是把业务拆成几个大领域,比如一家电商公司,可以分成用户域、交易域、商品域、营销域、物流域、售后域。每个主题域对应一组业务流程和相关的事实表、维度表。这个划分直接决定了 DWD 和 DWS 层表的组织方式,也决定了后续数据团队的协作分工。主题域不要划得太细,一般中小型公司 5 到 8 个主题域就够,太细反而会让表和表之间的关系变得更复杂。

需求调研还有一个副产品,就是确定指标的原子口径和衍生口径。比如“GMV”是所有支付成功的订单金额,还是包含未支付订单的金额?这就是原子口径,必须在 DWD 层固化为统计条件。“客单价”是 GMV 除以支付用户数还是支付订单数,这就是衍生口径,应该在 DWS 或 ADS 层固定下来。口径统一这件事,拖到后面肯定炸,一定要在最开始就花时间理清楚,并且沉淀成数据字典文档,随代码一起维护。

3.2 维度建模方法:星型模型的实际应用

分层架构确定后,具体到每一层内部的表设计,核心方法论就是维度建模。你不需要把 Kimball 那本书背下来,但有几个核心概念必须掌握:事实表、维度表、粒度、度量值。

事实表记录业务过程,比如订单事实表、支付流水表、用户行为日志表。粒度是事实表每行记录代表的最小业务单位,比如订单事实表的粒度是“一个订单的一个商品子项”,也就是说如果一个订单包含三件不同商品,那这个订单会拆成三行,不是一行。粒度定得越细,事实表能分析的维度越丰富,但数据量也越大,查询也越慢。日常开发里,事实表粒度要尽量贴合业务的最小原子事件。

维度表提供描述性属性,比如商品维度表包含商品名称、所属类目、品牌、上架时间等。维度表设计时要注意,所有可能用于分析过滤、分组、下钻的字段都可以放在维度表里。但也不要一股脑塞几百个字段,那些没人用的属性字段只会增加存储成本和维护成本。

这里给一个实用技巧:对事实表做宽表化设计的时候,可以把常用维度的主键直接退化到事实表中。比如订单事实表只存 user_id,但业务上 80% 的需求都要用户性别、用户城市、用户会员等级,那在 DWD 层就直接把性别、城市、等级这几个属性字段冗余进来,避免下游每次聚合都要 join 用户维表。这种提前冗余的代价是存储空间变大,收益是查询性能和开发效率大幅提升,在数仓场景里非常值得。实际上,很多互联网公司的 DWD 层都是以宽表为主的,这是规模化数据开发的标准做法。

3.3 开发实操:从 ODS 到 DWS 的 SQL 示例与细节

看文字讲原理容易泛,直接上一个实际开发中经常写的 SQL 过程更有说服力。以一个电商订单数据为例,演示一条数据从 ODS 进到 DWS 的完整加工逻辑。

ODS 层同步过来的订单表 ods_order_info,包含字段 order_id、user_id、sku_id、order_amount、pay_amount、order_status、create_time、pay_time、province_id、is_test_flag。这是源系统里的样子,没有做任何标准化处理。

DWD 层写一条清洗 SQL,要点如下:

INSERT OVERWRITE TABLE dwd_order_detail_di PARTITION(dt = '${bizdate}') SELECT order_id, user_id, sku_id, CAST(pay_amount / 100 AS DECIMAL(10, 2)) AS pay_amount, -- 金额单位从分转成元 order_status, create_time, pay_time, province_id, CASE WHEN order_status = 'finish' AND pay_time IS NOT NULL THEN 1 ELSE 0 END AS is_valid_order, -- 固化有效订单口径 'etl' AS etl_source FROM ods_order_info WHERE dt = '${bizdate}' AND is_test_flag = 0 -- 去除测试订单 AND pay_amount >= 0 -- 过滤金额异常 AND order_id IS NOT NULL;

这是一条非常典型的 DWD 清洗逻辑:统一金额单位、过滤测试数据、剔除异常记录、用 case when 把业务口径固化成布尔或标志字段。注意,这里不做什么聚合,保持一条订单一行的粒度。这是 DWD 的核心要求。

DWS 层按用户日汇总的 SQL 长这样:

INSERT OVERWRITE TABLE dws_user_daily_aggr_di PARTITION(dt = '${bizdate}') SELECT user_id, COUNT(DISTINCT order_id) AS order_cnt, COUNT(DISTINCT CASE WHEN is_valid_order = 1 THEN order_id END) AS valid_order_cnt, SUM(CASE WHEN is_valid_order = 1 THEN pay_amount END) AS valid_pay_amount, COUNT(DISTINCT sku_id) AS sku_cnt FROM dwd_order_detail_di WHERE dt = '${bizdate}' GROUP BY user_id;

这条 SQL 按照用户维度做了按天汇总,产出用户当天的下单数、有效订单数、有效支付金额和购买商品数。这张 DWS 表下游可以直接接报表,也可以继续被 ADS 层用来做跨维度的过滤和排序。你发现没有,到了 DWS,聚合才开始出现,这就是分层给开发节奏带来的最大好处:每一层只关心自己职责范围内的事,逻辑清晰,bug 定位也块。

3.4 调度依赖与数据质量校验:分层体系的保障机制

开发完各层表的任务,只是完成了一半,另一半是调度和监控。数仓分层最终能不能稳定运行,取决于调度依赖是不是正确配置,以及每层数据跑完后有没有自动校验。

调度配置的核心原则是强依赖,也就是只有上游任务成功,下游任务才会启动。具体来说,ODS 同步任务完成后,立刻触发 DWD 清洗任务;DWD 清洗任务成功后,DWS 汇总任务开始执行;DWS 完成后,ADS 可以开始组装。在调度平台,比如 DolphinScheduler、Airflow、DataWorks 中,每条任务都要显式声明父任务 ID,不建议使用定时调度代替依赖调度,因为源系统同步的耗时并不是每天固定的。

数据质量校验应该嵌入到每一层的调度流程中。ODS 同步完成后,校验源表行数和目标表行数是否一致,不一致直接报警。DWD 清洗完成后,校验主键是否重复、核心指标字段是否有空值、金额合计是否在合理范围内。DWS 汇总完成后,校验汇总值与明细去重后的值是否对得上,这一步叫做明细对账,是很多数据口径争议的终极裁决手段。建议在调度任务里增加一个质检步骤,专门执行这些校验 SQL,校验失败时阻止下游继续执行,防止脏数据进入下一层。定期清洗 TMP 表和归档过期分区,持续控制数仓整体存储成本。

3.5 数仓分层常见问题与排查技巧实录

在实际项目的推进过程中,有几类分层相关问题我反复遇到,这里统一整理一下,方便你快速对照排查。

第一类是 OD 层和 DWD 层职责不分的混乱。很多人开发时会顺手把清洗逻辑写进 ODS 同步 SQL,或者在 ODS 就做了一些聚合动作。短期看省了一点时间,长期看历史数据没法追溯,因为源头已经被污染了。排查方法是看 ODS 表的加工逻辑,如果出现 join、group by、case when 清洗等非搬运逻辑,就该把逻辑挪到 DWD 层。

第二类问题是 DWS 层过度汇总,导致下钻能力丢失。比如 DWS 只做了按天汇总,但业务临时想按小时分析,你发现源数据的明细已经在 DWD 能查到,但 DWS 没有小时粒度,重新跑一批数据非常耗时。解决思路是多保留一份小时粒度的汇总,或者按需增量计算。这是设计层面对业务需求理解不充分导致的。

第三类问题是同一个指标在 DWD 和 DWS 各算各的,口径对不上。比如有效支付金额,DWD 层定义为一个字段,DWS 又用了一版逻辑,结果两张报表数值不同。发现这类问题的核心手段是明细对账,对账的常规做法是:从 DWS 汇总表随机抽几个维度组合,用 DWD 明细重新聚合计算一遍,比较两边结果是否一致。存档口径定义文档并跟代码一起走评审,是预防这个问题最有效的手段。

第四类问题是依赖关系配置不当。明明是下游任务,却每晚固定时间启动,上游数据同步一故障,下游拿到的是上一批次数据,还不报错。排查方式是看调度实例的启动状态与上游任务状态的先后顺序,要求所有任务严格声明上游依赖。

3.6 小型数仓怎么合理配置分层

很多刚起步的团队一听到五层架构就直接被劝退了,觉得自己的数据量又不大,好像没必要搞这么复杂。这里我要说一个自己的真实看法:小型数仓更要做分层,但可以做简化版,一般是 ODS、DWD、DWS、ADS 四层,并且 ODS 直接使用源表同步,DWD 只做必要的清洗和统一,DWS 承接核心通用报表的汇总,ADS 只针对特定应用场景建单个结果表。其实在这一步,你已经跑通了整套分层体系。

小型数仓不要做的:不要一上来就建几十张 DWS 汇总表,维度组合永远算不完,做最常用的用户、商品、地区三个维度就够;不要纠结缓慢变化维的复杂策略,用 SCD1 覆盖足够;不要搞一套看起来很重的元数据管理系统,用 Excel 或者 Notion 维护一张数据字典就好。小团队最重要的是先把链路跑通,把口径固化,后面数据量涨了再慢慢丰富细节。我见过很多小团队一开始贪大求全,结果运维成本把产出效率拖垮了,分层的好处一点没享受到,反而丧失了灵活敏捷的优势。根据团队实际资源量做取舍,这才是分层落地的正确心态。

4. 后续扩展与个人经验分享

4.1 实时数仓场景下的分层设计

从数据时效性这个角度看,实时数仓的分层逻辑和离线数仓不太一样,但整体思路是完全一致的。实时数仓也会分 ODS、DWD、DWS、ADS 这几层,只是底层的存储和计算引擎换成了 Kafka、Flink、Doris、ClickHouse 这类具备流处理能力的组件。

实时场景里,Kafka 往往承担了 ODS 的角色,原始埋点数据先进入 Kafka 的 topic, Flink 做清洗和 join 之后落入 DWD 层的流式明细表。DWS 层由 Flink 的窗口聚合产出秒级或分钟级汇总结果,最后在 ADS 层对接大屏、实时报警和在线服务。实时数仓不是要推翻离线数仓,而是和离线数仓互为补充,两条链路尽量复用同一套口径定义。实际操作中,建议用一套统一的指标定义文件,离线 SQL 和实时 Flink 作业都从这套文件生成,这样能极大降低口径冲突的概率。

4.2 我踩过的分层设计坑

最后分享几个我亲测有效的小经验。第一个是不要在 ODS 之上再建一层备份表,数据冗余只会增加存储成本,数据重建靠原同步任务重新执行就可以实现。第二个是命名规范要发布当天就开始强制执行,建表时省五分钟不写注释,三个月后你可能要花五小时去猜这字段是干嘛用的。第三个是定期清理 DWD 层的无效数据分区,不要出现一个日分区表,结果三年前的分区还存在,存储成本白白消耗。

还有一个容易被忽略的细节是分层表和数据权限的联动。ODS 和 DWD 属于明细层,数据敏感程度最高,建议只有数据开发的核心成员可以访问。DWS 和 ADS 作为汇总层,可以开放给数据分析师和业务人员。很多企业在这一块没有做区分,导致用户手机号、身份证号这些敏感信息被到处访问。分层不仅是逻辑上的分层,也应该成为物理权限的天然边界。建表以前提前规划好每个表的访问控制列表,后续的权限治理会轻松得多。

数仓分层这件事,本质上就是通过合理的职责划分,把复杂的数据加工过程拆解成多个可控的阶段,让每一层都能被独立维护、独立优化、独立追溯。不管团队大小、数据量多少,这套方法论都值得认真落地。希望这篇基于实际项目经验的分享,能帮你少踩几个坑,也欢迎在实际落地中多做一些适合自己的调整。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询