ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

数仓分层实战:ODS、CDM、ADS三层架构设计与落地避坑指南

2026/9/23 23:18:21 拓冰建站 浏览量
数仓分层实战:ODS、CDM、ADS三层架构设计与落地避坑指南 1. 数仓分层的本质不是技术炫技而是解决协作混乱刚入行做数据那几年我最怕听到的一句话就是“这个指标口径不对”。业务方说GMV应该是下单口径财务说必须是支付完成口径运营又跑来说退款要扣掉。三拨人拿着三份报表在会议室里吵我坐在角落翻代码发现根源在于所有逻辑都堆在一张宽表里谁都能改谁改了什么没人知道。数仓分层要解决的核心问题就一个让数据的加工过程有迹可循让每一层只干自己该干的事。这跟盖楼是一个道理——你不能把地基、承重墙、装修、家具全塞在一个平面图上那样谁来了都能砸一锤子。我见过太多团队上来就搭Hadoop集群、装调度工具、配监控告警结果数据模型一塌糊涂。三个月后表数量从20张膨胀到300张血缘关系像蜘蛛网新人接手直接懵掉。问题出在把工具当成了目的而分层恰恰是那个“先想清楚再动手”的环节。这篇文章适合三类人看刚接触数仓建设的数据开发、被口径问题折磨的分析师、以及需要向团队解释“为什么不能直接查原始表”的技术负责人。我会把ODS、CDM、ADS这三层的设计逻辑拆开讲包括每层该放什么、不该放什么、表怎么命名、字段怎么规范以及那些只有踩过坑才知道的细节。2. 分层架构的整体设计思路2.1 为什么是三层而不是五层网上流传的数仓分层方案有各种版本三层、四层、五层都有。我实际落地过三套数仓最终发现ODS CDM ADS三层结构对大多数中小团队最实用。原因很简单层数越多数据搬运次数越多存储成本和维护成本呈指数上升。先看一个典型的五层方案ODS原始数据层、DWD明细数据层、DWS汇总数据层、DWT主题宽表层、ADS应用数据层。这套结构在超大型企业里确实有它的价值因为部门多、主题域复杂、需要精细的权限隔离。但如果你团队只有五六个数据开发业务线不超过三条搞五层就是给自己找罪受。我现在的做法是把DWD和DWS合并成CDM层内部再用命名规范区分明细和汇总。这样既保留了分层的核心价值——隔离变化、复用逻辑、统一口径——又不会让链路长得看不到头。注意分层数量没有标准答案关键是看团队规模和业务复杂度。十人以下的团队三层足够三十人以上且有多个业务域可以考虑四层。2.2 各层的职责边界怎么划分层的核心原则是单向依赖ADS依赖CDMCDM依赖ODSODS依赖业务系统。反过来绝对不行ODS不能去查ADS的数据CDM也不能反向依赖ADS。这条规则听起来简单但实际项目中违反的情况比比皆是。我见过最离谱的案例一个ADS层的报表直接连了业务库的从库理由是“实时性要求高”。结果业务库一次DDL变更报表直接挂掉排查了半天才发现数据源根本不是数仓。这就是边界没划清楚。各层的职责我用一个表格来说明层级核心职责数据特征保留周期典型表名ODS贴源存储不做业务逻辑加工与源系统结构一致7-90天ods_订单表_增量CDM清洗、整合、维度建模、轻度汇总按主题域组织有主键约束1-3年cdm_交易_订单明细ADS面向具体应用场景的指标计算宽表为主直接可查按需通常1年ads_运营_每日GMVODS层的关键是“不动数据”。源系统是什么样ODS就存什么样最多加个etl_time字段记录抽取时间。有些团队喜欢在ODS层就做清洗把空值替换掉、把编码转换掉这其实是在给自己挖坑——一旦清洗逻辑有问题你连原始数据都找不回来了。CDM层是数仓的核心价值所在。这一层要做的事情包括字段标准化比如把“男/女”和“M/F”统一成“male/female”、维度关联把订单表和用户表打通、度量计算比如把金额单位统一成分、以及轻度聚合比如按天汇总。这一层的表应该能被多个ADS复用如果一张CDM表只服务一个报表那说明抽象层次不够。ADS层就是“怎么方便怎么来”。这一层不需要考虑复用性不需要考虑范式甚至不需要考虑存储成本——业务方要什么格式就给什么格式。宽表、大宽表、甚至把JSON直接塞进一个字段只要查询快、口径对都行。2.3 分层带来的实际收益说几个具体的数字。我之前负责的一个电商数仓项目重构前有180张表其中60%是直接从ODS加工到ADS的“直通车”表。重构后变成ODS 45张、CDM 38张、ADS 52张总表数反而少了。但更重要的是口径统一GMV只在CDM层计算一次所有ADS报表引用同一个字段再也没出现过口径打架排查效率数据异常时先查ODS确认源数据、再查CDM确认加工逻辑、最后查ADS确认展示逻辑三步定位问题开发复用新报表开发时80%的CDM层数据已经存在只需要写ADS层的查询逻辑存储优化ODS层只保留30天CDM层做压缩存储整体存储成本下降了40%这些收益不是自动产生的需要配合规范、工具和流程。下面我逐层拆解具体怎么做。3. ODS层贴源存储的细节与陷阱3.1 抽取策略全量还是增量ODS层的第一道选择题就是抽取方式。全量抽取简单粗暴每天把源表所有数据拉一遍增量抽取只拉变化的部分通常基于时间戳或自增ID。我的经验是小表全量大表增量特殊表用CDC。具体怎么判断看源表的数据量和更新频率。日增量在10万行以下的全量抽取完全没问题省事且不容易出错。日增量超过100万的必须走增量否则抽取窗口会越来越长最终拖垮整个调度链路。增量抽取最怕的是“断点续传”问题。比如昨天抽取失败了今天重新跑怎么保证不漏数据我的做法是在ODS层加一个etl_date分区字段每次抽取时记录抽取的时间范围。如果昨天失败了今天跑的时候把昨天的范围也带上用INSERT OVERWRITE覆盖对应分区。-- 增量抽取示例按天分区支持重跑 INSERT OVERWRITE TABLE ods_order PARTITION (etl_date2024-01-15) SELECT * FROM source_order WHERE update_time 2024-01-15 00:00:00 AND update_time 2024-01-16 00:00:00;提示增量字段的选择很关键。优先用update_time而不是create_time因为业务数据经常会有状态变更。如果源表没有更新时间字段考虑用CDC工具捕获binlog。3.2 字段命名与类型映射ODS层的字段命名我建议保持与源系统一致不要做任何重命名。源系统叫order_amtODS就叫order_amt不要改成order_amount。这样做的好处是排查问题时可以直接对照坏处是命名风格可能不统一。但两害相权取其轻可追溯性比美观更重要。类型映射是另一个容易踩坑的地方。不同数据库的类型系统不一样比如MySQL的TINYINT(1)通常表示布尔值但到了Hive里如果直接映射成TINYINT查询时就会显示0和1而不是true和false。我的做法是在ODS层保留原始类型在CDM层再做标准化转换。还有一个细节字符串长度。源系统的VARCHAR(50)到了数仓里我建议统一放大到VARCHAR(255)甚至STRING。因为业务发展过程中字段长度经常会不够用如果ODS层卡死了长度后面扩字段就要改表结构非常麻烦。3.3 ODS层的常见误区第一个误区是在ODS层做数据清洗。我见过团队在ODS层就把手机号脱敏了结果后来做用户画像需要原始手机号做关联发现数据已经不可逆了。ODS层应该保留最原始的数据形态清洗和脱敏放在CDM层做。第二个误区是ODS层保留太久。有些团队把ODS层当成归档库数据保留好几年。这会导致存储成本飙升而且ODS层的数据结构跟源系统一致查询效率很低。我的建议是ODS层保留30-90天足够排查问题就行历史数据归档到冷存储或者直接删除。第三个误区是ODS层表太多太碎。有些团队把源系统的每一张表都同步过来包括那些配置表、日志表、临时表。这会让ODS层变得非常臃肿。我的做法是只同步与业务分析相关的表配置类的小表可以按需同步。4. CDM层数仓价值的真正体现4.1 维度建模星型还是雪花CDM层的核心工作是维度建模。星型模型和雪花模型之争已经持续了二十多年我的观点很明确优先星型必要时雪花。星型模型就是一张事实表关联多张维度表维度表不再拆分。比如订单事实表关联用户维度、商品维度、时间维度、地区维度。这种结构查询效率高因为关联层级少。雪花模型是把维度表进一步规范化比如用户维度拆成用户基础信息表和用户等级表。这种结构节省存储但查询时要多关联几次。我实际项目中90%的场景用星型模型就够了。只有一种情况我会考虑雪花维度表特别大且存在大量冗余。比如商品维度表有5000万行其中类目信息只占很小一部分这时候把类目拆出去单独建表是合理的。维度表的设计有几个关键点代理键每个维度表都应该有一个与业务无关的自增主键叫代理键surrogate key。业务主键比如用户ID可能变更代理键不变。缓慢变化维用户改了手机号历史订单应该关联旧手机号还是新手机号这就是缓慢变化维要解决的问题。我通常用Type 2方式给维度表加start_date和end_date保留历史版本。一致性维度同一个维度在不同事实表中应该保持一致。比如“地区”维度订单事实表和用户事实表用的应该是同一张地区维度表。4.2 事实表设计事务型还是周期快照事实表分三种事务型事实表、周期快照事实表、累积快照事实表。事务型事实表记录每一次业务事件比如每一笔订单、每一次支付。这种表的特点是行数多、更新少、只追加不修改。周期快照事实表记录固定周期的状态比如每天的用户余额、每月的库存量。这种表的特点是行数相对少、按周期生成。累积快照事实表记录一个流程的多个阶段比如订单从下单到支付到发货到收货的完整链路。这种表的特点是行数少、但字段会不断更新。我见过最常见的错误是把事务型事实表当周期快照用。比如想查“每天的下单用户数”直接从订单事务表里按天去重统计。这样做在数据量小的时候没问题但数据量大了之后性能很差。正确的做法是建一张每日用户下单快照表在CDM层就做好聚合。4.3 CDM层的命名规范与数据管理命名规范这件事每个团队都有自己的偏好但核心原则是见名知义。我用的规范是这样的表名格式cdm_主题域_业务过程_粒度示例cdm_trade_order_detail_di交易域订单明细日增量后缀含义di日增量df日全量mi月增量mf月全量字段命名也有讲究维度字段用dim_前缀比如dim_user_id度量字段用mtr_前缀比如mtr_order_amount时间字段用_time后缀比如create_time布尔字段用is_前缀比如is_paid数据管理方面CDM层必须做数据质量校验。我通常会在ETL过程中加几个检查点主键唯一性检查事实表的主键组合是否唯一外键完整性检查事实表关联的维度键是否都能在维度表中找到度量值合理性检查金额是否为负、数量是否异常大数据量波动检查今天的数据量跟昨天比波动是否超过阈值这些检查不通过时ETL任务应该告警甚至中断而不是让脏数据流到ADS层。5. ADS层面向业务的最后一公里5.1 ADS层表的设计原则ADS层是业务方直接查询的层设计原则跟CDM层完全不同。CDM层追求复用和规范ADS层追求查询性能和易用性。具体来说ADS层的表应该宽表化把业务方需要的所有字段放在一张表里避免关联预聚合常用的汇总指标提前算好比如日GMV、周活跃用户数按主题组织比如运营主题、财务主题、供应链主题每个主题下的表放在一起命名直观业务方看得懂比如ads_运营_每日销售概览我见过一个反例ADS层的表名叫ads_rpt_001业务方每次查数据都要翻文档找表名。这种命名方式在表少的时候还行表多了之后就是灾难。5.2 指标计算的口径管理ADS层最核心的问题是指标口径。同一个“活跃用户”产品部门定义为“打开过App”运营部门定义为“有浏览行为”市场部门定义为“有下单行为”。如果不统一每个部门都要单独算一套。我的做法是在CDM层之上建一个指标字典把每个指标的定义、计算公式、数据来源、负责人写清楚。ADS层的表引用指标字典中的定义不自己造指标。指标字典的格式大概是这样指标名称英文名定义计算公式数据来源负责人日活跃用户DAU当日有登录行为的去重用户数COUNT(DISTINCT user_id)cdm_user_login_di张三支付转化率pay_cvr支付用户数/下单用户数支付用户数/下单用户数cdm_trade_order_di李四这个字典要定期维护指标变更时同步更新。我通常每个季度跟业务方过一遍确认口径没有变化。5.3 ADS层的性能优化ADS层直接面向查询性能是生命线。几个实用的优化手段分区裁剪ADS表按天分区查询时指定日期范围。我见过有人查一年的数据不写分区条件全表扫描查询跑了半小时。数据倾斜处理如果某个维度的数据特别多比如某个爆款商品按这个维度聚合时会出现数据倾斜。解决办法是加盐打散先局部聚合再全局聚合。预计算对于固定的报表可以提前把结果算好存成结果表。比如每天早上6点跑批把昨天的日报数据算好业务方上班直接查。缓存对于高频查询但变化不频繁的数据可以放在Redis或Kylin里做缓存。但要注意缓存更新策略避免数据不一致。6. 分层落地的常见问题与排查技巧6.1 数据血缘断裂怎么排查数据血缘是分层架构的“导航图”。如果血缘断了排查问题就像盲人摸象。血缘断裂通常有三个原因动态表名ETL代码里用变量拼接表名比如INSERT INTO table_${date}。这种写法血缘工具解析不了。解决办法是尽量避免动态表名或者用血缘工具支持的方式写。跨层引用ADS直接查ODS跳过了CDM。这种血缘关系在标准血缘图里显示不出来。解决办法是加代码审查禁止跨层引用。存储过程复杂的存储过程里嵌套了多层查询血缘工具解析不完整。解决办法是把存储过程拆成多个ETL步骤每一步的输出都落表。排查血缘断裂时我通常从ADS层的异常报表出发反向追溯先看ADS表的ETL代码找到它依赖的CDM表再看CDM表的ETL代码找到它依赖的ODS表最后看ODS表的抽取配置找到源系统。如果中间某一步找不到依赖那就是血缘断了。6.2 数据延迟的定位方法数据延迟是数仓运维的常见问题。用户反馈“报表数据没更新”可能的原因有源系统数据延迟业务库的binlog还没同步过来ODS抽取延迟抽取任务排队或失败CDM加工延迟SQL跑得慢或资源不足ADS刷新延迟调度依赖没配好定位方法是从后往前查先看ADS表的最后更新时间再看CDM表的最后更新时间再看ODS表的最后更新时间最后看源系统的数据时间。哪一层的时间戳不对问题就在哪一层。我通常会在每层的ETL任务里加一个“数据时间戳”字段记录这条数据对应的业务时间。这样排查时一目了然。6.3 常见问题速查表问题现象可能原因排查方法解决方案报表数据为空ODS抽取失败检查ODS分区是否有数据重跑抽取任务指标数值异常CDM逻辑变更对比历史数据和源数据回滚CDM代码查询超时ADS表未分区检查查询是否带分区条件加分区过滤或预计算数据重复主键不唯一检查CDM事实表主键去重或修正ETL逻辑口径不一致指标定义不统一对比不同报表的计算逻辑统一到指标字典6.4 几个只有踩过坑才知道的细节坑一时区问题。源系统用UTC时间数仓用北京时间如果不做转换跨天数据会错乱。我的做法是在ODS层就统一转成北京时间后面所有层都用同一个时区。坑二空值处理。源系统的空值可能是NULL、空字符串、或者特殊字符。如果不统一处理关联时会出现匹配不上的情况。我的做法是在CDM层统一把空值转成默认值比如字符串转成unknown数值转成0。坑三小数精度。金额字段用FLOAT或DOUBLE存储会出现精度丢失比如0.10.2不等于0.3。我的做法是金额统一用DECIMAL(18,4)存储计算时也用DECIMAL类型。坑四缓慢变化维的边界。用户改了手机号历史订单关联旧手机号还是新手机号我的做法是订单事实表关联的是下单时的用户维度代理键这样历史订单永远关联旧手机号新订单关联新手机号。坑五调度依赖。ADS任务依赖CDM任务CDM任务依赖ODS任务。如果依赖配错了会出现“数据还没跑完就开始跑下游”的情况。我的做法是用调度工具的依赖功能而不是用时间间隔来估算。7. 分层架构的演进与扩展7.1 从三层到四层的时机三层架构用了一段时间后如果出现以下信号可以考虑拆成四层CDM层表数量超过200张管理和查询都很吃力明细数据和汇总数据混在一起命名规范难以统一不同业务域的CDM表互相依赖耦合严重这时候可以把CDM拆成DWD明细数据层和DWS汇总数据层。DWD保持与ODS相同的粒度只做清洗和标准化DWS做轻度汇总按主题域组织。拆分的代价是ETL链路变长存储成本增加。所以不要为了“看起来更专业”而拆分要确实有业务需求才做。7.2 实时数仓的分层怎么做实时数仓的分层逻辑跟离线数仓类似但技术选型不同。ODS层通常用Kafka做缓冲CDM层用Flink做实时计算ADS层用ClickHouse或Doris做快速查询。实时数仓的难点在于数据一致性。离线数仓可以等所有数据到齐了再跑批实时数仓必须处理乱序数据和迟到数据。我的做法是在Flink里用Watermark机制处理乱序对于迟到超过阈值的数据写入侧输出流做补偿。另一个难点是分层边界。实时链路通常比较短ODS到ADS可能只有两步。如果强行套用三层结构会增加不必要的延迟。我的建议是实时数仓可以简化成两层ODS和ADS中间的计算逻辑在Flink任务里完成。7.3 数据湖与数仓分层的融合数据湖比如Iceberg、Hudi的出现让分层架构有了新的可能。传统数仓的ODS层通常只保留近期数据历史数据归档到冷存储。有了数据湖之后ODS层可以保留全量历史数据CDM层按需读取。这种架构的好处是灵活性需要历史数据时不用从归档里恢复直接查数据湖就行。坏处是管理复杂度数据湖的文件需要定期做compaction否则小文件太多会影响查询性能。我目前的实践是ODS层用数据湖存储全量数据CDM和ADS层用传统数仓存储。这样兼顾了灵活性和性能。8. 一些个人体会数仓分层这件事技术实现只是冰山一角水面下的是团队协作方式和数据文化。我见过技术很牛但分层混乱的团队也见过技术一般但分层清晰的团队。后者的数据质量往往更好因为规范被真正执行了。分层规范要落地靠的不是文档而是工具。比如用调度工具强制依赖关系用代码审查阻止跨层引用用数据质量平台自动校验。人是有惰性的只有工具才能保证规范不被绕过。最后分享一个我常用的检查方法随机挑一张ADS表沿着血缘往上追看能不能追到ODS层。如果中间断了或者绕过了CDM层那就说明分层规范执行得不够好。这个方法简单但有效我每季度都会做一次。