简介:这是一份面向数据仓库工程师与大数据开发者的数据模型设计方法论文档,聚焦数仓建设中分层混乱、模型不规范、命名不统一、主题域划分不完整等典型痛点。文档以实战视角系统梳理了ODS操作型数据存储层、CDM通用数据模型层(含DWD明细层、DWS汇总层、DIM维表层)、ADS应用数据服务层的定位与边界,并给出从数据调研、数据域划分、构建总线矩阵、明确统计指标,到一致性模型设计、代码开发、部署运维的完整实施路径。同时针对分层边界不清、模型设计缺乏标准化、代码不规范、主键选择不当等常见问题提供了具体解决思路。资源为单个doc文档,整体约77KB,内容结构清晰,适合从事数仓建模、ETL开发或数据平台建设的初中级读者作为参考。已有182人学习下载,可作为数仓分层设计与模型规范的实用对照手册。
1. 数据仓库数据模型设计:为什么ETL任务总在半年后翻车
很多团队一开始做数仓,注意力都放在同步工具和调度平台选型上,跑通几条ETL链路就觉得数仓建好了。但真正的问题往往藏在后头:业务需求越接越多,报表对不上数、指标口径打架、任务链路越拉越长,最后连数据负责人自己都说不清某张表到底该从哪层取数。这份关于数据仓库数据模型设计的资料,核心就是在解决ETL流程里“数据搬完之后怎么放”的问题。分层边界怎么定、模型怎么设计、命名怎么统一、公共逻辑怎么下沉,这些看起来是架构理论,实际上决定了你写的每一条ETL任务能不能稳定活过半年。本文用一线工程视角,把这套方法论拆成可落地的分层策略、模型设计选择和避坑经验,帮新手建立正确框架,帮熟手查漏补缺。
2. 先把仓库分好层:ODS、CDM、ADS的边界与ETL任务归属
2.1 三层架构的工程定位:同步、加工、服务
任何一份数仓资料,第一步都在讲分层。原因很简单:不分层,ETL任务就没办法划分职责边界。你一个任务到底该做清洗还是该做汇总,取决于它落在哪一层。
这份资料把数仓分成ODS、CDM、ADS三层,其中CDM又拆成DWD、DWS、DIM。从ETL工程角度看,这三层的定位非常清晰:
ODS层负责同步,几乎不做业务转换。数据从源系统拿过来,原样落地,最多做编码统一、去重、分区裁剪这类基础处理。它是ETL的入口,也是数据核对的原点。CDM层负责加工,DWD做明细清洗与维度冗余,DWS做公共汇总,DIM做维表统一。这层是ETL任务最密集、最复杂的区域,几乎所有业务逻辑都在这层实现。ADS层负责服务,按报表、产品、专题的需求做最后加工,表结构面向消费者,通常一个需求对应一张表,专表专用。
这个定位直接决定了ETL任务怎么写:同步任务只做同步,不要顺手把指标算了;明细层任务只做清洗和标准化,不要做汇总;汇总层任务只做指标加工,不要反向去依赖ADS表。一旦任务越界,整个数仓的层次依赖就乱了。
我自己在带团队时有个习惯:每个ETL任务先标层级,再写逻辑。任务名里带ODS、DWD、DWS、ADS前缀,一眼就知道这个任务该做什么、不该做什么。这个习惯看似简单,但能避免大量“顺手处理”带来的边界混乱。
2.2 反向引用:ETL任务里最常见的依赖陷阱
资料里明确提到,DWD理论上只可引用ODS、DIM和部分DWD数据,DWS只可引用DIM、DWD数据,不可引用下游ADS层数据。很多读者看到这句话觉得是理论约束,但它在ETL工程里是一条硬性检查项。
我见过最典型的翻车场景是:某张DWS汇总表需要补一个指标,开发图省事,直接从ADS层的报表表里join数据。当时跑得通,但一个月后报表表改了口径,DWS的数据对不上了,下游十几张BI报表全部受影响。排查到最后,发现链路里藏了一条“反向引用”的依赖,花了整整一天才拆干净。
所以我在做ETL代码评审时,会强制检查每个任务的依赖清单。具体做法是:
# 以Hive为例,检查DWS任务是否引用了ADS层表 grep -E "from|join" dws_order_summary.sql | grep -E "ads_|app_" # 如果输出为空,说明没有反向引用;如果有输出,需要立刻整改这段脚本的逻辑很简单:把DWS任务SQL里的from和join对象捞出来,过滤出ADS层表名前缀。一旦命中,就说明存在反向引用。这个过程可以用在任意两个层级的依赖检查上,把“禁止反向引用”从口头规范变成自动化检查。
参数说明:grep -E是扩展正则匹配,ads_|app_是你要检查的ADS层表名前缀,实际使用时要按你团队的命名规范替换。这种方法对DWD查ODS、DWS查DWD同样适用,只需要改一下目标前缀。
2.3 DIM层:维表设计的ETL实现与更新策略
DIM层在资料里被定位为“一致数据分析维表”,用来降低口径不统一的风险。但从ETL工程看,DIM层的核心问题不是设计,而是更新策略。
维表更新有几种常见做法:全量覆盖、增量合并、拉链更新。全量覆盖最简单,适合数据量小的维表;增量合并适合变化频繁、数据量大的场景;拉链更新适合需要历史溯源的场景,比如用户等级变更、商品类目调整。
实际开发中,我一般会根据维表的使用场景做选择:
公共维表(如日期维、地区维)用全量覆盖,每天一个分区,ETL任务简单直接。业务维表(如商品维、用户维)优先拉链更新,保证历史可回溯,同时支持“按天取当时的维度状态”这种分析需求。高频变动维表(如价格维、库存维)用增量合并,每天只处理变化数据,降低同步压力。
这里给一个拉链更新的简化实现思路:
-- 以Hive SQL为例,拉链更新用户维表 INSERT OVERWRITE TABLE dim_user_zip SELECT a.user_id, a.user_name, a.level_id, CASE WHEN b.user_id IS NULL THEN a.end_date ELSE '9999-12-31' END AS end_date FROM dim_user_zip a LEFT JOIN ( SELECT user_id, user_name, level_id FROM ods_user_update WHERE dt = '${bizdate}' ) b ON a.user_id = b.user_id AND a.end_date = '9999-12-31' WHERE b.user_id IS NULL OR a.user_name != b.user_name OR a.level_id != b.level_id;这段SQL的逻辑是:找到当前仍有效的维表记录,和当天增量变更数据做关联。如果某用户有变更,就关闭旧记录的有效期;如果没有变更,保持不变。核心思路是“保留历史、新增版本”,适用于所有需要历史溯源的维表。
参数说明:${bizdate}是调度日期参数,9999-12-31是拉链表的最大有效期。实际项目中,你还需要单独Insert当天的新变更记录,这里只展示了关闭旧版本的逻辑,完整实现要考虑新记录插入和有效性拼接。
3. 公共层设计:DWD与DWS的任务拆分、沉淀判据与宽表化
3.1 什么是公共层:抽象、复用与沉淀的工程含义
公共层在资料里被描述为“通过抽象、复用、沉淀物理或逻辑的模型,以提升整体架构的数据效率并确保口径一致性”。这句话是方法论的核心理念,但在ETL工程里,它的落地方式是判断一个逻辑该不该下沉、一个指标该不该沉淀。
常见的做法是:先跑通需求,再回看链路,找到多处重复、口径不一致的地方,抽出来做成公共层。但如果你等到出事才回头做,往往已经晚了。我更推荐在设计阶段就做公共层规划,拿这张表做判据:这个逻辑被几个下游任务引用?如果只有1个,就留在应用层;如果有2个以上,就下沉到公共层。
下沉的过程也有讲究。资料里说“公共处理逻辑下沉”,意思是相同的清洗规则、口径计算,应该在DWD或DWS层统一实现,而不是在每个ADS任务里各写一遍。一个很常见的反例是:订单金额口径,A任务用GMV,B任务用实付金额,C任务用退款后净值,最后BI报表对不上。这种问题不是SQL写错了,而是口径没有在公共层收敛。
3.2 DWD层设计:维度建模、属性冗余与宽表化
DWD层在资料里被定义得很清楚:对ODS层数据进行清洗转化,以业务过程作为建模驱动,基于维度建模思想构建最细粒度的明细事实表,同时将重要属性字段做适当冗余,构建明细宽表。
从ETL角度看,DWD层任务要解决三个问题:一是怎么清,二是怎么冗余,三是怎么保证易用性。
先说清洗。ODS层的数据是原样的,可能有重复、缺失、格式混乱。DWD层要做的不是“深度加工”,而是“标准化”:字段命名统一、枚举值统一、时间格式统一、脏数据剔除。这些规则应该固化在DWD任务里,而不是每个下游任务都处理一遍。
再说冗余。DWD表做维度冗余,核心目的是减少下游频繁join维表。比如订单明细表,可以把商品标题、类目名称、店铺名称冗余进来,这样下游做分析时单表查询即可,不用每次join商品维表。冗余的原则是“冗余相对不易变的维度属性”,商品标题、类目这种低频变化字段适合冗余,价格、库存这种高频变动字段则要谨慎,否则会导致DWD表数据频繁刷新。
我写过很多DWD层的宽表任务,最常用的做法是把维度冗余和事实表构建放在一个SQL里完成,逻辑大体如下:
-- DWD订单明细宽表构建示意 INSERT OVERWRITE TABLE dwd_order_detail_wide SELECT o.order_id, o.user_id, o.shop_id, o.order_amount, o.pay_amount, o.order_status, o.order_time, p.product_id, p.product_name, p.category_name, s.shop_name FROM ods_order_detail o LEFT JOIN ods_product_info p ON o.product_id = p.product_id LEFT JOIN ods_shop_info s ON o.shop_id = s.shop_id WHERE o.dt = '${bizdate}' AND o.order_status != 'cancelled';这段SQL的要点有三个:一是事实表和维表分开join,每个join只干一件事;二是维度冗余字段和事实字段明确区分,方便下游识别;三是清洗逻辑前置,在DWD就过滤掉无效状态。这样下游任务拿到的是已经标准化、可直接聚合的明细数据。
参数说明:${bizdate}是调度日期,order_status != 'cancelled'是常见的业务过滤条件,实际场景可能还有更复杂的过滤规则,比如删除标记、测试数据、异常大单等,这些都应该在DWD层统一处理,而不是留给下游各自判断。
3.3 DWS层设计:汇总粒度、指标可加性与空间换时间
DWS层的定位很明确:基于指标需求构建初步汇总事实表,以宽表化手段物理化模型,为上层提供公共指标。工程上叫“空间换时间”:把高频访问的汇总结果预先算好,下游查询直接读,不用每次从头跑。
但“沉淀DWS”不能拍脑袋。资料里说得非常务实:如果某个指标只在一个地方使用,就没有沉淀DWS的必要。反之,如果这个指标在多个报表、多个应用中被反复查询,就应该沉淀。
DWS层设计有两个关键决策:粒度和可加性。
粒度的选择是DWS设计的核心。资料里提到“以最大公约数的逻辑抽象复用”。举个例子:上游ADS1是子公司粒度,ADS2是一级类目粒度,那就可以设计出SKU粒度的DWS表。SKU粒度是这两张表的“最大公约数”,既能支撑子公司维度分析,也能支撑类目维度分析。
可加性同样重要。资料里说“转变不可累加指标为可累加指标,如比率型建议保留分子分母”。这条在ETL实现里很容易被忽略。很多DWS表直接存了“转化率”“占比”这种比率指标,表面看很方便,但一旦下游想按维度汇总,比率不能直接加,只能重新计算,非常被动。正确的做法是在DWS层同时保留分子分母,让下游各取所需。
下面给一个DWS层聚合任务的简化示例,说明粒度和可加性怎么落地:
-- DWS商品粒度汇总示意 INSERT OVERWRITE TABLE dws_sku_summary_1d SELECT product_id, category_id, COUNT(DISTINCT user_id) AS uv, COUNT(order_id) AS order_cnt, SUM(pay_amount) AS gmv, SUM(pay_amount) / COUNT(DISTINCT user_id) AS arpu FROM dwd_order_detail_wide WHERE dt = '${bizdate}' GROUP BY product_id, category_id;这段SQL的核心是“先聚合、后派生”:口径一致的核心指标(uv、订单数、GMV)直接聚合,派生指标(arpu)基于原子指标计算。下游如果要做多粒度汇总,可以基于原子指标再加工,不受派生指标限制。
参数说明:COUNT(DISTINCT user_id)算UV,SUM(pay_amount)算GMV,arpu是人均付费,属于派生指标。实际项目中,这类DWS表往往需要按1d、nd、td拆成多张,避免一张表承担太多时效性需求。
3.4 公共层迭代与效率权衡:大表拆接、粗粒度优先、水平切分
资料里有一段关于公共层迭代原则的描述:“对于数据量比较小的场景,可以优先构建DWD,后构建DWS;对于数据体量比较大的情况,可以优先构建粗粒度的DWS,对DWD构建采用水平拆分的方式。”
这条原则在ETL工程里非常实用。
数据量小的场景好理解:先有明细,后有汇总,链路清晰。但数据量大的场景,如果你先构建细粒度DWD宽表,再构建粗粒度DWS,会遇到一个现实问题:DWD宽表刷得很慢,导致DWS迟迟出不来,下游报表一直在等数。
这时候我会调整顺序:先构建粗粒度DWS,把核心指标跑通,保证下游有数可用;再回头补DWD明细,供需要细查的场景使用。同时,DWD宽表可以采用水平拆分,比如把“attributes扩展字段”这种半结构化数据拆出来单独存一张表,主表只保留高频使用的核心字段,降低存储和刷新成本。
另一个被很多人忽略的点是“垂直拆分”。资料里以DIM为例,说“将低热度的扩展维度属性与核心维度属性进行拆分”。这个原则同样适用于DWS:一张DWS大宽表,核心维度属性放在主表,保证每天准时产出;扩展维度属性放在辅表,随业务需要补充。这样即使辅表刷新失败,主表也能准时产出,下游不会受牵连。
我在做模型设计评审时,常问一个问题:如果这张表产出延迟了1小时,下游会发生什么?如果下游会直接失败,那就需要做稳定性设计,比如拆表、降级、优先级调度。公共层设计不是一次做完的,而是在业务迭代中反复调整的。资料里说“数据域不是一成不变的”,这句话同样适用于模型本身。
4. 避坑指南:数仓数据模型设计的四个高频翻车点
4.1 分层边界不清:ODS直接做ADS,小烟囱遍地
现象:业务方要一个取数需求,开发为了赶工,直接从ODS原始表开发对应的ADS表,绕过了公共层。短期看需求交付很快,但这类“烟囱开发”越积越多。
原因:业务需求紧急时,没人愿意等公共层建设。而且很多开发对公共层“复用”价值感知不深,觉得多写一层是浪费时间。本质上还是没有把“表设计逻辑”当成数仓架构的一部分。
解决:至少要守住一条底线——从ODS直接到ADS的任务,必须有评审流程。我会在调度系统里加一个依赖检查规则,凡是任务依赖关系中出现“跳层”,即从ODS直接产出ADS表的,自动打标记,需要责任人确认原因。同时,把公共层建设纳入需求迭代节奏,每次有新需求,先看公共层是否已有可复用模型;如果没有,评估是否值得沉淀,而不是直接建烟囱。
4.2 指标口径不一致:同一指标在不同表里叫不同名字
现象:A报表里的“GMV”和B报表里的“GMV”对不上,原因可能是A的GMV含退款订单,B的GMV不含,或者A用的是订单创建时间,B用的是支付时间。两边开发都觉得自己没写错。
原因:口径没有在公共层统一,每个任务各自实现,理解不同,结果自然不同。更麻烦的是,指标命名也不统一,同一个指标在不同表里叫“gmv”“pay_amt”“order_amount_agg”,分析时还得挨个对字段语义。
解决:强制在DWD层统一字段命名,规范定义里叫什么,表里就叫什么;在DWS层统一指标口径,分子分母在公共层固化。我在项目里用一张维表维护指标口径定义行:指标名、口径描述、计算逻辑、字段来源、所属层、负责人。每次新任务引用该指标,先查这张维表,确保口径一致。这听起来有点繁琐,但它能挡住绝大多数口径冲突。
4.3 主键非唯一:汇总结果虚高,下游数据对不上
现象:DWS表按订单ID聚合,下游跑出来一条订单被统计了两次,数据虚高。
原因:ODS明细表存在重复记录,DWD层没有去重;或者DWD层主键设计时没考虑多业务过程,同一个订单ID在不同环节产生多行记录。
解决:在DWD层构建事实表时,必须明确主键的唯一性。订单明细表的主键一般是订单ID加商品ID加操作时间戳的组合;如果业务过程本身是“一笔订单可能拆成多个子订单”,那主键要按子订单维度设计,不能简单用父订单ID。我一般在DWD任务最后加一个去重检查:
-- 检查DWD表主键是否唯一 SELECT order_id, product_id, COUNT(*) AS cnt FROM dwd_order_detail_wide WHERE dt = '${bizdate}' GROUP BY order_id, product_id HAVING cnt > 1 LIMIT 10;如果这个查询有结果,说明主键不唯一,需要回到上游排查重复来源。这个检查看起来简单,但能在数仓建设早期就兜住“数据虚高”这种最难查的问题。参数说明:HAVING cnt > 1是核心条件,输出的行数通常很少,但只要有,就必须处理。
4.4 公共层随意下沉:没有判据就沉淀,越沉越乱
现象:开发者觉得某个逻辑“可能复用”,不管有没有实际需求,先往公共层塞。结果公共层表越堆越多,但大部分没人引用,反而拉长了任务链路,增加了调度压力。
原因:公共层建设没有需求导向。资料里反复强调“是否具有共性”是核心判据,但实际执行时,很多人把“共性”理解成“我觉得有用”。
解决:给公共层下沉设一个硬条件——至少有2个真实下游任务引用,才做沉淀。沉淀前先确认口径已被多张表使用;只有1个使用场景的,留在应用层。同时,每季度做一次公共层“引用审计”,找出长期无下游引用的表,标记下线。这件事需要调度系统配合,但哪怕用SQL定期扫描依赖关系,也能做得很准。
5. ADS层设计:需求驱动、集市划分与减少对ODS的依赖
ADS层的定位是“面向应用的数据服务层”,直接对接数据消费者。工程上,ADS层设计的核心原则是“需求驱动,专表专用”。贴近业务的分析师和报表要什么,你就给什么,不要过度设计,也不要试图在ADS层做公共逻辑。
5.1 需求驱动的最小设计原则
资料里说“按需最小原则设计,除非有明确的业务延续,否则不做过度的扩展设计”。这句我在实践里体会很深。很多ADS表做着做着就变成“通用大宽表”,什么字段都往里塞,想着以后能用上。结果是字段冗余、表越来越大、产出越来越慢,而80%的字段从建表到现在都没被查询过。
正确做法是:需求方提出明确的报表或明细需求,你只设计支撑该需求的最小字段集合。如果后续需求扩展,再通过新加字段或新表解决。这个原则能让ADS表始终轻量灵活,也不会让逻辑散乱成烟囱。
5.2 集市划分与共性抽象
ADS不是一潭死水,它也应该有边界。资料里说“以高内聚低耦合的原则对集市进行划分,让单集市数据研发聚焦在某一个领域的业务需求实现”。更直白一点:按业务领域建集市,比如交易集市、流量集市、用户集市,每个集市之间尽量独立。
集市之间避免互相依赖,这一点很重要。如果我交易集市的ADS表引用了流量集市的数据,那交易集市的任务就受流量集市的调度影响,等于把两个业务的稳定性绑在了一起。真正需要跨集市的数据,应该在DWS层通过公共层计算好,ADS只负责读取,而不是在应用层做join。
ADS层同样可以有自己的公共逻辑,比如把多个需求共用的一段过滤规则抽出来,但范围要严格控制,只抽“领域内跨需求”的部分,不抽“全仓级”的公共层逻辑。
5.3 减少对ODS的直接依赖
资料里这段写得非常实在:“减少直接引用ODS表,降低源系统变更带来的改造成本。当源系统变更时,通过公共层适应性改造屏蔽下游变更。”
这句话展开说就是:你直接在ADS层引用ODS原始表,一旦源系统改表结构或字段含义,你要改的是所有引用ODS的ADS任务;如果你在DWD层做了兼容适配,源系统变更只需要改一个DWD任务,下游ADS全部屏蔽影响。
我在实际项目里遇到过一个经典场景:某源系统字段由“1位枚举”升级为“2位枚举”,值含义彻底变了。直接引用ODS的报表全部花了整整两天排查修复,而走了公共层的报表只改了一个DWD映射任务就全部恢复正常。从那以后,凡是设计ADS层表,我都会强制检查依赖清单,禁止直接引用ODS,必须经由DWD或DWS。
下面是ADS层任务的一个标准结构示例:
-- ADS报表任务:只从DWS/DWD读取,不直接引用ODS INSERT OVERWRITE TABLE ads_trade_daily_report SELECT bizdate, product_id, SUM(gmv) AS gmv, SUM(order_cnt) AS order_cnt, ROUND(SUM(gmv) / SUM(order_cnt), 2) AS unit_price FROM dws_sku_summary_1d WHERE dt = '${bizdate}' GROUP BY bizdate, product_id;这段SQL的要点是数据来源只有DWS表,没有跨ODS取数;所有口径、清洗、汇总逻辑都在DWS层完成,ADS只做最终的格式化和场景化处理。这样即使源头字段发生变化,只要DWS层适配好了,ADS任务完全不需要动。
6. 模型设计的最后一步:命名规范与运维闭环
前面几章把分层、公共层、ADS层都讲完了,但模型设计真正落到ETL工程里,还有最后一公里要打通:命名规范、依赖监控和数据验证。这三个环节决定了模型“能不能长期活下来”。
6.1 表命名的工程规范
资料里说“命名清晰可理解”,但很多团队的命名是随意草率的:a_temp_01、test_123、tmp_xxx。这种命名对当时写SQL的人很清楚,三个月后连他自己都看不懂。
我建议的命名规范是四段式:层级前缀加业务域加主题加粒度后缀。举个例子:
dwd_trade_order_detail_di:明细层DWD、交易域、订单主题、日增量dws_trade_sku_summary_1d:汇总层DWS、交易域、SKU粒度、1日累计ads_trade_daily_report:应用层ADS、交易域、日报表
有了这套规范,任何一张表,不用打开元数据,只看表名就知道它属于哪一层、哪个业务域、是什么粒度。ETL任务的日志排查、依赖分析、性能定位都会快很多。
6.2 DQC监控与数据验证
没有数据质量监控的数仓,本质上是一颗定时炸弹。数据可能一个月都没问题,但某天一个简单字段格式变了,就会导致下游所有指标偏掉,而数据本身根本不会报错。
我一般会给核心表配上三条DQC规则:
- 行数波动监控:当日分区行数与近7天均值偏差超过50%就告警;
- 主键唯一性监控:核心DWD表的明细记录不允许重复;
- 空值率监控:关键业务字段空值率超过阈值就告警。
以Hive为例,一个简单的主键唯一性DQC查询可以这样写:
SELECT COUNT(*) AS duplicate_cnt FROM ( SELECT order_id, product_id FROM dwd_order_detail_wide WHERE dt = '${bizdate}' GROUP BY order_id, product_id HAVING COUNT(*) > 1 ) t;这个查询的结果如果大于0,DQC就触发告警,调度平台会自动拦截下游任务,防止脏数据扩散。
6.3 元数据管理与文档沉淀
数仓建设到一定规模,最大的问题不是“没有数据”,而是“找不到数据”。分析师不知道DWS层有没有能直接用的表,开发不知道有没有人已经算过某个指标。
我习惯在每次模型上线时,同步维护一份元数据文档和指标字典。字段含义、计算口径、负责人、更新频率、下游引用方,全都在文档里写明。团队内部把它当成“最小可行文档规范”,不做长篇大论,只保证所有关键信息在一个地方可查。
数据模型设计在最后,从来不是“画几张ER图”的功夫,而是把模型变成任务、变成表、变成文档、变成监控规则的全过程。从那以后,我每次接手新数仓项目,都会强制走一遍“分层检查—依赖检查—命名检查—DQC检查”四步流程,哪怕这一步看起来只有几分钟,也能在早期拦住大部分翻车现场。希望这份方法论对你有用,也帮你在下一次数仓改造时少填几个坑。
本文还有配套的精品资源,点击获取