简介:一份聚焦数据仓库数据模型设计的系统梳理文档,面向数仓开发工程师、数据建模人员以及准备数据仓库相关面试的学习者。内容以数仓分层为主线,依次说明ODS操作型数据存储层、CDM通用数据模型层(含DWD明细层、DWS汇总层、DIM维表层)与ADS应用数据服务层的定位与边界;随后完整梳理数据调研、数据域划分、构建总线矩阵、明确统计指标、模型设计、代码开发、部署运维的七步开发流程,并针对分层边界不清、模型缺乏标准化、命名不统一、主键选择不当等常见问题给出解决思路与降噪建议。资源为单个DOC文档,大小77KB,便于快速阅读与打印。已有182人学习下载。读者可据此建立数仓模型设计的标准化认知,掌握公共层与应用层模型的设计原则,减少烟囱式开发带来的数据混乱与成本问题。
1. 数据模型设计:数据仓库从“能跑”到“好用”的分水岭
接手过数据仓库的人大概都有这种经历:数仓跑起来了,每日例行同步和调度都正常,但业务方提一个“上个月各渠道的客单价对比”,要排到两周后;再问一句“这个数为什么和昨天对不上”,没人能三分钟内说清口径。问题往往不在 SQL 写得差,而在数据模型没设计好。数据模型决定了数据仓库怎么组织、怎么存储、怎么被查询,它直接决定了后续所有报表、分析、算法取数的效率和准确性。本文要解决的,就是用一套可复现的步骤,把数据模型从“拍脑袋建表”变成“按业务过程设计、按查询模式验证”的工程方案。适合刚搭完数仓、正要建核心层的数据工程师,也适合被报表口径反复折磨、想从根上理清模型的人。
2. 范式建模与维度建模:先定路线,再谈设计
2.1 第三范式建模:OLTP 的优等生,为什么进了数仓就卡
第三范式(3NF)是关系型数据库设计里绕不开的一套规则:消除部分依赖和传递依赖,让每一张表只描述一个实体,字段之间不冗余。这套设计在业务交易系统里是合理的——订单表里不存客户姓名,只存 customer_id,客户姓名在客户表里只出现一次,这样新增、修改、删除都不会产生数据不一致。
但把同一套思路搬到数据仓库,问题立刻暴露。数仓的核心负载是分析查询,特点是扫描大范围数据、多表关联、聚合汇总。3NF 模型下,一个订单分析往往要 join 五六张表:订单表关联客户表、再关联产品表、再关联区域表、再关联销售员表。每次关联都是一次 IO 和计算开销。数据量在千万级时还能忍,到亿级、十亿级时,这类查询的延迟直接葬送报表体验。
另一个隐性问题:3NF 模型对业务人员极不友好。业务方想看“每个区域的销售额”,他需要知道订单表、区域表、销售员表之间的外键关系,还需要自己拼 join。而数仓的价值恰恰是把复杂查询封装掉,让业务方能直接面对一张“宽”表。让分析师每天写 6 表关联,等于让数仓团队变成取数客服。
我见过最典型的翻车案例是某公司把业务库 1:1 同步进数仓,表结构原封不动,共 40 多张表。第一个月报表还能跑,第三个月开始,任何跨 5 张表以上的查询都要 30 秒以上,业务抱怨“数仓比业务库还慢”。这不是数仓的错,是模型选型错了。
2.2 维度建模的核心:星型与雪花模型差在哪
维度建模是数仓领域最主流的设计方法,由维度建模的经典方法论提出,核心思想是围绕业务过程(下单、发货、支付)建立事实表,用维度表描述事实发生的视角(谁、何时、何地、何种产品)。
星型模型是它的默认形态:中间一张事实表,周围若干维度表,每个维度只关联一次,形成星型结构。事实表存度量值(金额、数量)和外键,维度表存描述性属性(产品名、分类、品牌)。查询路径清晰:先过滤维度,再回事实表聚合。
雪花模型则是在维度表上继续规范化。比如产品维度表拆成产品表、品类表、品牌表三层,查询时要多 join 一层才能拿到品牌名。听起来更节省存储,实际在数仓里存储成本远低于查询成本,这一点要掂量清楚。雪花模型的代价是查询 SQL 变复杂、优化器需要更多时间做 join 规划,在 BI 工具里还可能因为表关系过多而导致血缘图混乱。
两者的取舍没有绝对正确,但我有一个实用经验:维度表本身属性不超过 20 个字段时,全部放在一张表里做成星型;维度属性超过 20 个且属性之间有稳定的层级关系(如大区—省份—城市—门店),才考虑拆一层雪花。大多数中小企业数仓,星型模型可以覆盖 90% 以上场景。
2.3 选型不是拍脑袋:从查询模式与数据稳定性反推
选范式建模还是维度建模,不该由个人偏好决定,而应从三个角度反推:查询模式、数据更新方式、团队维护能力。
查询模式是最重要的判断依据。如果业务方要的是“灵活探索”型分析——随时拖字段、看任意维度的组合、没有固定报表格式,维度建模的宽表 + 星型结构最合适,因为 BI 工具对星型模型的支持最成熟,拖拽字段就能生成查询。如果数仓要支撑的是跨主题域的整合查询,比如把财务、供应链、生产的数放在一起做横向对比,且这些数来自多个异构系统,纯维度建模会有点吃力,这时候考虑范式建模或者 Data Vault 混合方案更稳妥。
数据更新方式也直接决定模型选型。业务库是 update 频繁的在线交易数据,进数仓后如果只做全量覆盖,历史维度值会丢;如果做增量合并,范式模型的更新逻辑比维度模型复杂得多。维度建模里,维度表可以设计缓慢变化维策略(下一章会细讲),事实表通常只追加不修改,这对数据链路稳定性是巨大优势。
最后是团队维护能力。范式模型的表数量多、关系复杂,新来的同事上手周期长;维度模型表少、结构直观,一个新人半天就能说清楚事实表和维度表的关系。对多数团队来说,可维护性往往比理论上的灵活性更值钱。
综上,我的默认推荐是:核心层用维度建模,以星型为主;只在跨主题域整合、且源系统标准化程度低的场景里,局部引入范式建模作为补充。Data Vault 可以作为长期演进方向,但不建议一开始就上——它需要更强的 ETL 开发能力和元数据管理基础。
3. 维度模型落地:从业务流程到事实表与维度表
3.1 确定业务过程和粒度:先回答“一行代表什么”
动手建表之前,最该想清楚的是:我们建模的对象是什么业务过程。常见的误区是把“销售”当成一个过程直接建一张超级大表,把下单、支付、退款、发货全塞进去。这会导致一行数据语义不统一——某些行代表订单、某些行代表支付流水,聚合时根本说不清口径。
正确做法是拆开:下单是一个业务过程、支付是另一个、发货是第三个。每个业务过程建一张独立的事实表。原因很简单:不同过程的粒度不同,下单一行是“一个订单的一次下单动作”,支付一行是“一笔支付流水”,混在一起就没法保证一致性。
业务过程确定后,声明粒度是建模里面最不能省的一步。粒度就是“事实表里一行代表什么”。这句话要写下来,写清楚。比如“订单明细事实表,一行代表一个订单中的一个商品明细行”,这就是粒度声明。粒度声明决定了这个表能回答哪些问题、不能回答哪些问题。
我一般会用一句模板强制自己写粒度:“每一行记录的是 ____ 在 ____ 时间发生的 ____ 动作,最小粒度到 ____”。填不出来,说明业务还没想透,不要建表。粒度声明有个额外好处:它天然能判断报表口径是否合理。如果两张事实表粒度不同,就不能直接 join 对比,必须先聚合到同一粒度。
3.2 事实表设计:可加性、半加性与退化维度
事实表是分析查询的聚合对象,它的核心是度量字段。设计度量时,先给度量分类:可加、半可加、不可加。
可加度量最常见的如销售金额、下单数量,可以在任意维度下直接求和。半加度量如库存余额、账户余额,可以按维度求和、但不能跨时间求和——上月末库存加上本月末库存不等于两个月前库存。不可加度量如比率、单价,不能直接聚合,必须先算出分子分母再重新计算比率。
这个分类直接影响建模。错误的做法是把“客单价”这个比率直接作为字段存进事实表,然后让报表去 SUM。客单价的正确建模是存“销售金额”和“订单数”两个可加字段,报表层再计算金额除以订单数。如果源系统里只有客单价没有分子分母,必须找上游补齐,否则这个指标永远没法做深层分析。
事实表里还有一类特殊字段叫退化维度。它没有对应的维度表,而是直接把维度属性冗余在事实表里。最典型的是订单号。每个订单只有一条主记录,单独建一张订单维度表价值很低,把它直接放在事实表里,反而方便按订单查明细、做对账。判断一个字段要不要做退化维度的标准是:这个属性是否会被多个事实表共享?如果只有一张事实表用它,就做退化维度;如果多张事实表都要按它分析,就要做成公共维度表。
事实表设计还有个细节:外键字段必须用代理键而不是业务主键。代理键是独立于业务系统的整数自增键,避免上游业务主键变更或跨系统主键冲突。这个细节会在维度表设计里再展开。
3.3 维度表设计:代理键、自然键与层级
维度表回答的是“事实发生的背景”。设计维度表时,最需要先解决键的问题:代理键与自然键的关系。
自然键是业务系统里的真实主键,比如产品编码、客户编号。问题在于自然键可能被更新、被复用、跨系统不唯一。比如客户编号在 CRM 里是 C001,进了 ERP 变成 K1001,直接拿自然键关联事实表会断裂。代理键就是为了解决这个问题,在数仓内部为每个维度生成一个无业务含义的整数主键,事实表只认代理键。
生成规则常见做法是维度表自增 ID,也可以用哈希值保证一致性。我一般用自增 ID 配合自然键唯一索引,既能保证 ETL 幂等,又保留排查问题的线索。
维度属性设计要遵循“宽表化”原则:能冗余的属性尽量冗余进维度表,不要在查询时再去关联别的表。比如产品维度表里,既可以放产品名称、编码,也可以放品类、品牌、供应商、产地、上架时间。冗余带来的是查询效率,代价是 ETL 同步时需要多取一些字段,这笔账在数仓场景下值得花。
维度层级也是容易忽略的点。很多维度有天然层级,比如时间维度:年—季度—月—日;产品维度:品类—品牌—SKU。层级的建模方式有两种:一种是把层级字段全部平铺在维度表里,比如产品表同时有品类、品牌、品类 ID、品牌 ID,查询时直接按任意层级过滤;另一种是把层级拆成一张独立的维度表关联。对多数分析场景,平铺的方式更实用——查询无需多层 join,而且 BI 工具做层级下钻时只需要指定字段层级关系,不需要表关联。
这里要提醒一个建模中常见的陷阱:维度表不要设计成“拉链表 + 每次全量覆盖”的组合。维度表需要考虑历史变化,这直接引出了下一章的缓慢变化维策略。
4. 缓慢变化维与命名规范:让模型能维护的关键操作
4.1 缓慢变化维的三种策略,以及什么时候用哪种
业务系统的 SQL 里,改客户地址、改产品分类都是稀松平常的 UPDATE。但数仓事实表里存的是历史事实,它关联的维度值如果被覆盖更新,历史数据就错了。“上个月华东区的销售额”必须用上个月的区域归属口径,而不是现在改了归属之后的区域名。
处理这种“维度缓慢变化”问题,业界最成熟的是缓慢变化维(SCD)策略,常用三种。
SCD1:直接覆盖原值。维度表里一条记录,属性变化后更新原记录,不保留历史。适用于对历史分析无关紧要的属性,比如产品补充说明、联系人电话。它的优点是实现最简单,ELT 里 update 即可;缺点是如果后来发现这个属性其实要回溯分析,就追不回历史了。
SCD2:保留历史版本,新增一行。维度表里同一个自然键存在多行,每行有生效开始时间、生效结束时间(用 9999-12-31 表示当前有效),事实表关联时按业务时间找到对应生效版本。这是处理可回溯属性的标准方案,比如客户所属地区、产品所属分类。缺点是同一维度的行数会膨胀,查询时要记得带生效时间条件。
SCD3:保留原值 + 新值两列。维度表里同时有“当前区域”和“历史区域”两个字段,适合只需要对比当前和历史两个版本的场景。优点是不用拉长表,缺点是只能看两个版本,再远的历史就没了。
实际建模时,我的习惯是给每个维度属性打标签:分析需要回溯的属性用 SCD2,不需要的用 SCD1,需要对比当前与上一版的少量属性用 SCD3。注意:SCD2 和 SCD3 不要混在同一个属性上,会导致 ETL 逻辑混乱。
实现 SCD2 的常见做法是在 ETL 里先按自然键查询维度表,判断属性是否变化。如果变化,先把当前有效行的结束时间更新为当前时间,再插入新行并把生效开始时间设为当前时间。这中间要处理一个竞态问题:大批量更新时,同一自然键的多条源记录同时到达会导致重复版本。解决方法是按自然键去重后再做版本变换,或者用目标表先聚合再更新。
4.2 表命名的三个统一:分层前缀、业务域、字段风格
从零搭建数仓时,命名规范是那种“现在偷懒,后面翻车”的事。跳过规范,三个月后表多到想不起来哪张是哪张。常见的做法是给表名加上分层前缀 + 业务域:
- ods_:操作数据存储层,和源系统结构基本一致;
- dwd_:明细数据层,按业务过程整理的明细表;
- dws_:汇总数据层,面向主题的轻度汇总表;
- ads_:应用数据层,面向具体报表的宽表。
在这个前缀基础上加业务域,比如 dwd_order_detail 表示“订单明细”,dws_sales_daily 表示“销售日汇总”。这样从表名就能直接判断表的作用和数据层级。
字段命名同样要统一风格。我用的约定是:一律小写 snake_case,时间字段带 _at 后缀表示时间点(created_at),日期字段用 _date 后缀(order_date),金额字段用 _amt 后缀(order_amt),数量字段用 _cnt 后缀(item_cnt)。类型上金额必须用 decimal(18,2) 或更高精度,禁用 float;日期统一 date 类型,禁止用 string 存日期。这些约定看起来细碎,但能让数据字典一目了然,也方便数仓工具自动生成元数据。
有个公司内部的经验值得参考:他们在建仓第一天就要求所有新表过命名规范评审,违反一次就退回。初期效率略低,但数据字典铺开后,团队基本上不用问“这张表是什么意思”,减少了大量无效沟通。
4.3 命名之外:公共维度要复用,不要各自造
数据仓库里最难受的事,是两张事实表里都有一列“区域”,但一张来自省市区县表,另一张来自运营手工维护的大区表。口径对不上,业务对账对出一堆问题。
这就是需要强调公共维度(一致性维度)的原因。公共维度的意思是:同一个维度(时间、产品、机构、区域)在整个数据仓库里只有一套标准定义和一套标准表,任何事实表用到该维度,都必须关联这一套标准表。
落地时通常由数仓团队维护核心维度的“主数据表”,比如产品主数据表、组织机构主数据表、区域主数据表,由专人负责更新。各业务线新模型引用维度时,一律通过主数据表的代理键关联,不允许各自从源系统再搞一套。
这个习惯短期内会增加协调成本——业务方可能觉得“我用我自己的表更顺手”,但长期看,这是避免口径混乱最有效的机制。模型设计到这个层次,就不再是写 SQL 的事,而是数据治理的事。数仓团队要把自己定位成“数据标准的守护者”,而不是“取数的工具人”。
5. 数据模型设计避坑指南:五个高频翻车现场
5.1 事实表粒度不一致,导致汇总翻倍
现象:某开发者把“订单事实表”和“订单支付事实表”直接 join 汇总支付金额,结果金额巨大,怎么算都不对。仔细排查发现,订单事实表一行是“一个订单一个商品”,支付事实表一行是“一笔支付流水”,前者 10 条明细,后者 1 条流水,join 的时候产生了 10 倍膨胀。
原因:两表的粒度声明没对齐,开发时也没有先看粒度。
解决:join 前必须先明确两张表的粒度,统一到同一粒度再关联。正确做法是先按订单号把支付事实表聚合到订单粒度,再去关联订单明细表;或者反过来,把订单明细表聚合成订单汇总,再和支付表关联。事后也给我提了个醒:每张事实表建表时把粒度声明写进表的备注里,ETL 开发必须读备注再动手。
5.2 维度表直接覆盖更新,历史报表全乱
现象:产品维度表每天从业务系统全量覆盖同步,某天上游系统把一个产品的“所属分类”从“食品”改成了“生鲜”,第二天跑历史月度报表,上个月的分类统计也变成了“生鲜”口径。
原因:维度表没有引入 SCD 策略,等于默认了 SCD1(全覆盖)。但“所属分类”其实是一个对历史分析很重要的属性。
解决:给“所属分类”这类属性上 SCD2,保留历史版本。修改后,历史事实仍然关联到正确的版本,新事实关联到新版本,历史报表自动恢复正确。这个改动不需要改事实表,只改维度表和 ETL 的更新逻辑,但这正是维度建模的一个优势——维度还在,事实无损。
5.3 雪花模型过度设计,查询性能集体雪崩
现象:某公司数仓团队为了“规范化”,把产品维度拆成了产品表、品类表、品牌表、供应商表,区域维度拆成地区、省份、城市。用户跑一个按品牌汇总的报表,SQL 要 join 6 张表,BI 工具每次渲染都要等待十几秒。
原因:设计时只想着“消除冗余”,没考虑查询模式和工具兼容。数仓里存储廉价,join 成本昂贵。
解决:把品牌、品类、供应商字段全部回收到产品维度表,区域也只保留一张区域表(大区、省份、城市作为字段平铺)。修改后一个 join 完成查询,耗时降到 1 秒内。这个踩坑说明:星型模型之所以成为数仓默认选择,是用有意义的冗余换取了简单和速度,不要为了理论上的一点“整洁”牺牲查询体验。
5.4 宽表“灵活”字段加太多,ETL 变成黑匣子
现象:某开发者做汇总层时,听说“宽表对业务友好”,便在销售汇总表里加了 60 多列,包括各类“将来可能要用的维度属性”。结果三个月后,这个表有 20 多列从没人查询过,但 ETL 每天还在算,推算时间越来越长。业务方要看时,也分不清哪些字段口径可信。
原因:建模时没有先问业务要什么指标,而是预先堆字段。加字段一时爽,维护火葬场。
解决:汇总层只建当前业务有明确需求的指标字段,字段的新增走变更流程,必须说明用途、口径、使用方。不需要“未雨绸缪”到把每个想象中可能的分析维度都塞进去。模型设计的价值在精准匹配需求,不在字段数量。
5.5 忽略血缘管理,改字段名后下游报表静默出错
现象:某团队把源系统字段cust_type在数仓里改成了customer_type,但下游报表还引用旧字段名。模型发布后,报表不再报错,只是字段值变成了 NULL——因为 SQL 里的旧字段名查不到数据,但查询本身不报错。
原因:字段改名没有同步更新下游消费方,也没有自动血缘校验。
解决:模型变更前先跑一遍血缘解析,找出所有引用旧字段的下游脚本,一并修改后再发布。如果数仓工具不支持自动血缘,至少要建一张字段映射表,由 ETL 开发维护,发布前人工检查。这个教训我记到现在:数据模型设计不只包含建表本身,还包含“变更如何传导到下游”的机制设计。
6. 验证模型质量的三个自查手段,替代“先上线再说”
设计完模型并不代表结束,上线前用三招自查能省下后面大量返工。第一招,看粒度是否可验证。对每张事实表,跑一条“按粒度字段去重计数”的 SQL 和源系统条数对比,对不上就说明粒度声明有问题或 ETL 有重复。第二招,做维度对账。对每张维度表,比较数仓行数和源系统去重数,特别注意 SCD2 维度表里“当前有效”的行数必须等于源表行数,否则说明版本变更逻辑有 bug。第三招,拿典型报表 SQL 走查。选 3 到 5 个核心报表查询,人工检查每条查询 join 的表是否都在模型设计范围内,过滤条件是否能用上维度表的索引,是否还需要额外 join 模型之外的表。如果查一句报表要绕三四层模型外路径,说明模型没贴近实际查询,需要回炉。
这三招我都踩过坑才总结出来。曾经有一个模型上线前我没做维度对账,结果 SCD2 版本膨胀导致同一客户关联了错误版本,历史销售额被算偏。从那以后我养成一个习惯:任何模型发布前,先把这三条自查 SQL 写进交付清单,跑不通绝不上线。模型质量不是靠“认真一点”保证的,而是靠一套可循环的验证动作。希望帮到你——下次设计好模型后,先别急着建表,把这三招跑完再动手。
本文还有配套的精品资源,点击获取