☰
数仓分层实践:从ODS到ADS各层设计要点与实时数仓落地解析
2026/10/5 7:31:56 网站建设 项目流程

做数仓这几年,被问得最多的往往不是某个SQL怎么写,而是“你们这数仓到底怎么分层的?ODS、DWD、DWS看着都明白,真到自己建表就不知道怎么切了”。很多人聊起分层架构头头是道,架构图画得比谁都漂亮,等真正落库建层的时候,连第一张ODS表该存什么都不确定。这篇文章我把实际落地数仓项目的过程捋一遍,把每一层为什么存在、怎么设计、边界怎么划、实时数仓有什么不同,一条一条讲明白。适合正在搭建数仓,或者刚接手数仓项目还没找到上手感觉的同学参考。

1. 数仓为什么要分层

1.1 分层的本质:跟做饭一个道理

我们经常说数仓分层,到底分的是什么东西?我先给个最直白的定义:分层就是把“从业务库到报表”这段漫长的数据加工过程,拆成多个职责单一的阶段,每个阶段只对上一层负责,不做越级的事情。

我习惯拿做饭来类比。业务系统数据库就是菜市场买回来的原始食材——带泥的土豆、没处理过的鱼,东西是真的,但没法直接上桌。报表和算法要用的数据相当于成品菜,要的是清洗切好的净菜、摆好盘的凉菜。如果从菜市场买回来直接倒进锅里煮,最后出来的东西大概率是没法吃的:泥还在、内脏没去、切得大小不一。数仓分层就是那间中央厨房,洗菜、切配、配菜、热炒各有各的工位,每道工序的人只管自己那一摊,最终才出一桌稳定的菜。

这个类比能解释一个关键问题:为什么不能业务库直接出报表?因为业务系统关注的是交易能不能跑通,它不关心数据能不能直接做统计。订单表里状态字段可能存了1、2、3、4,不同业务系统的含义还不一样;用户表里性别可能存了0、1、2,还可能存了null、空字符串。这些数据不经处理直接统计,出来的数字谁敢信。

1.2 不分层的代价,我全都交过学费

早期我刚接触数仓的时候,接过一个很“野生”的项目。当时的做法就是业务库抽一张表,数仓就建一张同样的表,报表要什么就直接查底层表,中间不经过任何加工。表面上看起来敏捷,实际上是一地鸡毛。

第一个崩掉的是口径。同一个订单金额,运营那边统计的时候把退款订单也算进去了,财务觉得应该剔除退款,销售觉得只算支付成功且未取消的。三个部门拿到的数字都不一样,天天扯皮,最后都来问数仓“到底哪个准”。问题不在他们,在于我们根本没有一个统一加工的地方,各查各的,口径当然乱。

第二个崩掉的是效率。底层明细表动辄几亿行,报表查询直接扫全表,凌晨跑批业务高峰的时候,Hive的任务排队排到天亮。那时候最怕听到的一句话就是“昨天的报表怎么还没出”,第二天一查,跑批超时了,因为每个报表都去扫了一遍原始表。

第三件事更头疼,人员流动以后,旧任务没人敢动。因为任务之间没有任何规范路径,全都是点到点的“人情链路”,只有写任务的那个人知道中间做了什么转换。他走了,那堆ETL就成了黑匣子,坏了只能重刷。

1.3 分层带来的直接收益

后来按标准分层重新搭了一遍,最直观的变化是三个词:解耦、复用、可控。

解耦很好理解,每一层只跟相邻层打交道。业务库加了个字段,只影响ODS同步,下游基本不用动;报表要加一个新指标,只需要在DWS层往上加,不用去碰底层逻辑。复用就更实惠了,DWD层把订单、用户、商品这些公共的明细加工好,下游十个应用都查它,一次加工多次使用,跑批的资源消耗直接降了一个量级。可控则体现在权限和数据质量上:业务库的账号权限收到最小,ODS只能被DWD读,DWS只能被应用和报表读,如果有人查数查得不对,顺着链路一层一层看,很快就知道是哪一步出了问题。

2. ODS、DWD、DWS每一层到底做什么

2.1 ODS层:先把原始数据原封不动地接进来

ODS层,全称是Operational Data Store,操作数据存储层。它是数仓和业务系统之间的第一道桥。核心职责就四个字:原样落地。从业务库同步过来的数据,是什么样就存什么样,字段名不改、类型不转、状态码不翻译、不做任何业务逻辑加工。

我说“原样”,但不代表“什么都不做”。有两件事ODS必须做。

第一是分区管理。绝大多数数仓用的是Hive或类似的大数据组件,ODS表必须按照日期做分区,一般用dt作为分区字段,每天一个分区。这样既方便管理,也方便后续回溯——某一天数据出了问题,直接重刷那一个分区就够了,不用全量推倒重来。

第二是数据同步策略。数据量小的表可以每天全量覆盖,反正快;大表一般用增量同步,每天只有变化的数据进来。这里有个容易踩的坑:只用增量的话,如果业务库发生了数据更新(比如订单状态从待支付变成已支付),单纯按创建时间增量同步会漏掉变化。所以要综合考虑业务库有没有更新字段、能否开启binlog、有没有自增主键这些条件。常用做法是增量+更新标记字段,或者干脆基于binlog做拉流同步,确保ODS里的数据跟业务库保持一致。

ODS层不建议做深度清洗。有人喜欢在ODS就过滤掉脏数据,我的建议是:过滤可以做,但只做物理过滤,比如去掉完全为空的记录、去掉分区值异常的记录;凡是涉及业务规则的转换,比如把状态码翻译成中文、把单位统一,都放到DWD层。原因很简单,ODS要尽量保留原始语境,万一后续发现之前清洗的规则错了,还能从ODS重新来一遍。

2.2 DWD层:脏活累活都在这层干

DWD层,Detail Warehouse Detail,明细数据层。如果说ODS是仓库的收货区,DWD就是加工车间。这一层要做的事情很明显:把ODS的原始数据变成干净、标准、好用的明细数据。

具体拆开说,DWD的核心工作有三块。

第一块是数据清洗。空值填充、格式统一、状态码翻译、非法数据过滤。比如订单金额可能有负数(业务上不该出现),维度表没有对应的用户——这些都在DWD处理掉。我常用的做法是建立一套清洗规则表,每个字段的清洗逻辑都写在配置里,代码和规则分离,改规则不用改代码。

第二块是维度退化。这是DWD的精髓,也是新手最容易困惑的地方。传统关系型建模喜欢规范化,用户单独一张表、商品单独一张表,然后靠外键关联。但在大数据场景下,几亿行明细去关联维度表非常耗资源。维度退化就是把常用的维度字段直接冗余到明细表里,用户性别、商品分类、店铺名称,都直接“退化”进订单明细表,查询时就省掉了一次join。统计一下就知道,绝大多数报表查询需要的维度字段就那么十几个,冗余进去,性能和易用性双双提升。

第三块是构建拉链表。业务库的数据很多是变化的:用户等级从普通升成VIP、订单状态从待付款变成已付款。如果只保存当前状态,历史就丢了;如果每次都全量覆盖,历史倒是有了但空间翻倍。拉链表通过记录每条数据“生效开始日期”和“生效结束日期”,既能查到当前状态,也能回溯历史任意时间点的状态。我在订单维度表上就这么干过,效果立竿见影,下游做留存分析、状态流转分析,直接查拉链表就行。

2.3 DWS层:给指标做“半成品”

DWS层,Data Warehouse Summary,汇总数据层。这一层干的事情一句话就能说清楚:把DWD的明细数据,按照常用维度,预先聚合成宽表,让它变成“开袋即食”的半成品。

为什么需要这么一层?拿电商数仓举例。运营每天要看各品类的销售额、订单量、支付用户数。如果每次都从几亿行DWD明细里group by,凌晨跑批的时间会非常感人。DWS做的事情就是提前把“天+ 品类”维度的汇总算好。品类有哪些、订单量多少、支付金额几何,统统放到一张宽表里。报表层直接查这张汇总表,秒出结果,跑批时间从小时级缩短到分钟级。

DWS设计的核心是“公共指标下沉”。什么意思呢?做数仓最怕的就是同一个指标在不同报表里算出来的数不一样,原因就是各算各的。DWS把这类公共指标统一定义好、计算好,比如“交易金额=过滤退款+过滤未支付+有效订单金额”,后续所有应用层指标都从DWS这张表取数,口径天然统一。

DWS的粒度通常有三档:天粒度、周粒度、月粒度。天粒度是最常用的,我通常先做这一档;周和月的指标可以用天粒度再聚合,不必单独建表。也要注意控制DWS表的数量,只沉淀高频使用、多维组合的指标,别把DWS做成“明细表的复制品”。

2.4 ADS层:容易被忽略的最后一块拼图

热搜词里没提到ADS,但完整的分层体系里它必须存在。ADS是应用数据层,直接对接报表、大屏、邮件推送、算法特征服务。这一层的特点是:结构简单、查询极快、面向特定应用。

ADS层的数据来源不一定是DWS,也可能是DWD甚至ODS,看具体需求。比如BI报表要展示某个促销活动每分钟的实时销售曲线,走的可能就是实时DWD到ADS的链路;月报里要统计跨三层业务的漏斗转化,可能需要同时读DWS和ODS做二次加工。

我见过很多团队把ADS省掉,报表直接查DWS,表面看少了一层很轻松,但时间久了问题就来了:报表需求五花八门,有的要聚合再聚合,有的要行列转换,有的要关联外部数据,全堆在DWS上,DWS的语义就会变得混乱。ADS的价值在于“需求隔离”——把五花八门的应用需求消化在这一层,核心层次不受干扰。

3. 分层设计规范与边界把握

3.1 各层之间的数据流向

分层架构最核心的规则只有一个:数据必须自下而上流动,不能跳层,更不能反向流动。ODS → DWD → DWS → ADS,这条链路是数仓的“默认路线”。

为什么要定这么死?因为一旦允许跳层,分层就形同虚设。报表缺一个指标,开发偷懒直接从ODS查,刚开始觉得没什么,时间一长,同一张报表在不同团队手里数据来源完全不一样,我们又回到“口径失控”的起点。还有一个很现实的理由:数据血缘。有了严格的层级关系,任何一张表往上追溯,依赖关系清清楚楚;哪天业务方问“这个数是不是包含了退款”,顺着血缘往下捋,五分钟就能回答。

当然,规矩实践久了会发现有些场景确实需要例外。比如一些特殊审计需求,必须直接查ODS原始数据,这种走独立通道就行,但不建造成常规路径。

表结构上,我还会约定每层表名,让人一眼看出它属于哪层:

层级表名前缀示例
ODS层ods_ods_order_info_di
DWD层dwd_dwd_order_info_di
DWS层dws_dws_user_order_1d
ADS层ads_ads_app_repurchase_1d

命名里的后缀也要有约定。我用的是“分区粒度+同步策略”,比如di表示日增量、df表示日全量、1d表示一天汇总、1h表示小时汇总。这套命名规范一开始就要定好,等整个数仓建起来以后再改前缀,代价非常大。

3.2 字段命名与开发规范

表名前缀只是骨架,字段命名才是血肉。字段规范这块,我踩过比较大的坑是“英文字段没有中文注释”。建表的时候觉得简单,order_status谁看不懂?三个月后回来看,谁都看不懂它里面存的1、2、3到底对应什么。现在我的标准是:每个字段必须有COMMENT,枚举含义必须在字段注释里写清楚;核心业务指标,注释里要写明计算口径和来源层,比如“退款金额(排除未支付订单,取自DWD层)”。

开发规范方面,每个跑批任务脚本开头必须写清“来源表、目标表、更新策略、责任人”。这不是形式主义,是出问题以后救命的稻草。遇到过凌晨跑批失败,任务挂了没人知道是哪一天的,因为脚本里没写日期参数;后来在规范里强制要求日期参数必须显式传递,失败重跑时才知道是哪个分区。

3.3 分层不是越细越好

说句得罪人的话,很多数仓不是设计得太粗,而是分得太细。我在一些团队见过七层八层的架构,每一层之间含义模糊,DDS、ADS、DIM、TMP全混一起,新增一张表要纠结半天该放哪层。

我的建议是:中小规模团队,四层就够。ODS、DWD、DWS、ADS,再额外加一个公共维度层(DIM)放维度表。不要为了“看起来专业”去加多余层次。分层本质是拆解复杂度,层次多了,调度依赖反而越来越复杂,链路越长,数据延迟越高,出了问题越难排查。等业务量真的增长到需要细分时再拆,也比一开始就铺开大摊子来得稳妥。

4. 实时数仓的分层变式与开发重点

4.1 实时和离线:分层思想一样,技术栈大有不同

最近“实时数仓开发工作内容”这个词挺火的,很多同学问实时数仓是不是就不用分层了。答案是:分层思想一样要保留,但每层的落地介质和加工方式变化非常大。

离线数仓的核心介质是Hive/Spark,数据以批处理为主,一天一跑,延迟小时级,胜在数据量大、稳定可靠。实时数仓的核心诉求是秒级或分钟级出数,比如实时大屏、实时风控、实时推荐特征,它关注的是“刚刚发生了什么”。所以介质上,实时链路一般是消息队列(Kafka)接入,Flink做流式加工,最终结果落在OLAP引擎里,比如Doris、ClickHouse,供查询端快速读取。

做个简单对比:

对比维度离线数仓实时数仓
数据时效小时/天级秒/分钟级
计算引擎Hive、SparkFlink、Spark Streaming
存储介质HDFS、Hive表Kafka、Doris、ClickHouse
开发方式SQL跑批为主Flink SQL + 流式状态计算
回溯能力强,重刷分区即可相对弱,依赖日志重放或落盘数据
适用场景报表、T+1分析、算法训练大屏、监控、实时营销、风控

4.2 实时数仓每一层怎么落地

实时链路的分层逻辑跟离线完全对应。

实时ODS,一般就是Kafka里的原始业务消息。业务库的binlog变更消息、埋点日志,原样接进来,保留所有字段,不加工。这里要注意消息格式规范化——统一JSON或Avro,统一消息key的策略,否则后续Flink解析会非常痛苦。

实时DWD,在Flink里做清洗、去重、维度补全。比如订单流和支付流要做双流join才能拼出一条完整订单;用户维度数据要维护在状态里或维表里,实时补充用户城市、会员等级;同一订单的重复消息要在窗口内去重。这是实时数仓开发工作内容里最核心的部分,也是最容易出问题的地方。Flink SQL写起来快,但join的语义、状态TTL、checkpoint策略都得仔细调,不然会出现数据延迟越来越大,甚至状态爆炸的情况。

实时DWS,在Flink里做秒级、分钟级的窗口聚合,结果写入Doris或ClickHouse。比如每5分钟统计一次各品类的订单金额,写进Doris明细表;OLAP引擎里再建好聚合模型,查询端做任意维度下钻。

一个常见的误区是以为实时链路不需要“清洗”,Flink处理就行了。实际上实时数据的脏数据问题比离线更头疼——字段解析失败、事件时间乱序、维度表数据缺失,这些在实时链路上都需要专门的策略处理。好一点的团队会把“实时脏数据流”单独隔离到一个Kafka topic,方便排查和回放。

4.3 Lambda架构:两条腿走路是常态

实操中,大多数公司不会把全部报表迁到实时,而是“实时每天都有,离线照样跑”。这就是Lambda架构:实时链路出快数,离线链路出准数。同样一个指标,实时大屏看趋势、离线报表看最终数,两边各自独立。

Lambda架构最大的痛点是对不上数。明明同是“今日销售额”,实时链路说3.5万,离线说3.52万,差在哪儿?其实就差在实时链路的数据截止时间和离线不一样。我的处理办法是:实时和离线共用同一套口径定义,并且实时结果只用于展示和告警,不进入财务、不进入对账,对账的事全交给离线。清晰的业务边界比技术统一更重要。

5. 实战演练:电商订单从ODS到DWS的完整链路

5.1 场景设定

拿一个最常见的场景:电商订单。业务库有一张order_info表,每天新增几十万订单,有状态变更,我们希望最终在DWS层得到“每个用户、每个品类、每天的下单金额和下单量”。

5.2 ODS落地脚本示例

ODS层第一步就是建表,保留业务库原字段:

create table if not exists ods.ods_order_info_di ( id string comment '订单ID', user_id string comment '用户ID', shop_id string comment '店铺ID', product_id string comment '商品ID', category_id string comment '品类ID', order_amount decimal(10,2) comment '订单金额(原始)', pay_amount decimal(10,2) comment '实付金额(原始)', order_status string comment '订单状态: 1待支付 2已支付 3已发货 4已完成 5已取消', create_time string comment '下单时间', update_time string comment '更新时间' ) comment '订单原始表ODS' partitioned by (dt string comment '日期分区') stored as parquet;

ODS的同步脚本我用DataX或Sqoop从业务库抽取,每天按dt分区写入Hive。关键点是:同步时不要做任何表结构变更。业务库字段是order_status我这边就叫order_status,哪怕知道这个命名不友好,也等到了DWD再改。

5.3 DWD轻清洗实现

DWD层承接ODS,做清洗和维度退化。我把用户信息、商品信息直接用left join退化进来,顺便把状态码翻译成可读文本:

insert overwrite table dwd.dwd_order_info_di partition (dt='2024-01-01') select t1.id, t1.user_id, t1.product_id, t3.category_id, t2.user_name, t2.user_level, t1.order_amount, t1.pay_amount, case when t1.order_status = '1' then '待支付' when t1.order_status = '2' then '已支付' when t1.order_status = '3' then '已发货' when t1.order_status = '4' then '已完成' when t1.order_status = '5' then '已取消' else '未知' end as order_status_desc, t1.create_time, t1.update_time from ods.ods_order_info_di t1 left join dim.dim_user_info t2 on t1.user_id = t2.user_id left join dim.dim_product_info t3 on t1.product_id = t3.product_id where t1.dt = '2024-01-01' and t1.user_id is not null and regexp(t1.order_amount, '^[0-9]+(\\.[0-9]{1,2})?$');

这里有个实操注意点:过滤非法金额我用的正则,有人喜欢用cast然后判断是否null,效果一样,但正则写在SQL里更直观。更重要的是,清洗规则写在注释里,不然后期没人知道这一步挡掉了什么数据。

5.4 DWS汇总实现

DWS层从DWD层取数,按用户和品类两个维度做聚合:

insert overwrite table dws.dws_user_category_order_1d partition (dt='2024-01-01') select user_id, category_id, count(1) as order_cnt, sum(pay_amount) as pay_amount_sum, sum(if(order_status_desc = '已支付', 1, 0)) as paid_order_cnt from dwd.dwd_order_info_di where dt = '2024-01-01' group by user_id, category_id;

不要小看这张DWS表,它承接了后续所有“用户维度”“品类维度”的分析需求。运营要看品类销售排行,直接查它;算法要算用户品类偏好,也查它。一张表养活一堆下游任务,这才是DWS该有的状态。

这条链路的隐藏价值在于:每一步都有明确的依赖边界。ODS挂了,重跑ODS;DWD脏数据多了,只调DWD逻辑;DWS要加指标,不碰其他层。各层独立重刷互不影响,这在实际生产中太重要了。

6. 构建数仓时我踩过的坑与排查经验

6.1 常见问题速查表

下面这些问题是我和身边同事在实战中真实遇到过的,整理成了一张速查表,代码里藏了很多年,不一定在教科书里找得到:

问题现象可能原因排查与解法
ODS表某天数据突然少一截业务库同步任务失败,但没有重跑;或分库分表新增了分库没加到同步任务里先对比ODS和业务库总行数;检查同步任务日志;确认同步配置是否覆盖所有分表
DWD字段大量为null关联的维度表数据没同步,或维度表主键冲突先查维度表数据量;用left join的语义检查关联条件是否正确;警惕维度表主键不唯一导致的数据发散
DWS汇总数值和报表对不上上游DWD存在重复数据;或DWS过滤条件跟口径不一致检查DWD是否有重复记录(可用count对比明细和汇总);逐个指标核对口径定义
实时链路数据延迟越来越大Kafka lag持续增加;Flink状态后端压力大;反压查看Kafka消费组lag;检查Flink的反压指标;调大并行度或优化Flink SQL join的key分布
回刷历史分区后,下游报表数据变了回刷时关联的维度表用的当前维度,历史维度不对使用拉链表——回刷历史分区时必须带上当时对应的维度快照数据,不能用当前维度反推历史

6.2 几条拿得出手的实操经验

第一个经验是“先跑通再优化”。我第一次搭数仓的时候,上来就画了一堆“未来要做”的功能,天天想着要不要用最先进的建模方案。后来发现,对一个业务量还没起来的项目来说,跑通数据链路比什么都重要。第一版哪怕只做ODS到DWD的全量同步,先把报表做出来,业务方看见了价值,才有后续迭代的钱和资源。架构设计慢慢演进,比一开始就追求完美靠谱得多。

第二个经验是“拉链表一定要早做”。很多团队一开始图省事,维度表直接用全量快照,觉得反正数据量不大,每天全量拉一遍多简单。等数据量涨到几千万行了,回头看,增量更新没做、历史变化也没留存,所有的分析都只能看当前状态,做留存、做状态流转全是瞎猜。拉链表一开始就要建,后面再补很痛苦。

第三个经验是“监控一定要挂在最底层”。我们经常关注最终表对不对,却忘了监控ODS的同步量。其实很多问题早期在ODS就有苗头:今天的订单量只有平时的一半、某张表新增分区为空、某个接口的延迟突然拉高。在ODS同步任务上加上行数波动监控和延迟监控,比在报表层发现问题提前好几个小时。数仓出问题不可怕,可怕的是业务方比你先发现。

第四个经验是“实时数仓的质量意识不能降”。很多人做实时Flink任务比离线Hive粗心很多,觉得实时流跑起来不出错就行。实际上实时任务的状态管理、数据回溯机制、脏数据隔离,每一项都需要和离线一样严谨。我在实时DWD里漏配过一次状态TTL,结果用户维度的状态一直不更新,推荐算法连续三天推的都是历史偏好,上线效果一塌糊涂。从那以后,实时任务的review严格度比离线还高。

踩过这些坑以后,我现在设计数仓的第一原则是:每一张表都要能回答“我是谁、我从哪来、我要到哪去”——属于哪一层、来源是哪里、下游是谁。链路清晰了,绝大多数问题都能在半小时内定位到根因。数仓分层这件事,看上去是技术设计,本质上其实是工程管理。把产出物的边界划清楚,把人力和责任也划清楚,才谈得上稳定和高效。

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

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

立即咨询