想当年我刚接触数仓那会儿,最懵的一件事就是——同一个人,一会儿跟我说“必须上维度建模,星型模型一把梭”,一会儿又有个老前辈语重心长地讲“第三范式才是数据库设计的根,你不懂范式就别碰数仓”。两边听起来都很有道理,但放在一起,又好像水火不容,我当时真的一头雾水。
后来在这行摸爬滚打久了才慢慢想明白,这俩压根就不是一个层面的东西,也不是非此即彼的敌人。维度模型和第三范式,一个服务于“查询和分析”,一个服务于“业务系统的数据一致性”,它们各自在不同场景下有自己的生态位。对一个刚入门的数仓新人来说,搞清楚它们分别解决什么问题、各自的长处和短板、以及在真实数仓工程里怎么配合,比单纯背概念重要得多。
这篇内容,我尽量抛开教科书式的说教,用我实际做项目的经验,把这俩东西掰开揉碎讲清楚。适合刚转数仓的开发、数据分析师,以及那些已经被维度建模和范式理论绕晕了的同学。看完你至少能搞明白:第三范式到底在防什么,维度模型到底在追求什么,以及为什么绝大多数离线数仓的分层架构,都是先“范式”后“维度”这么来设计的。
1. 从数据库设计的“不犯错”说起:第三范式到底在保护什么
很多人第一次接触“第三范式”都是在大学数据库原理课上。老师会告诉你,范式分为第一范式、第二范式、第三范式,甚至还有BCNF、第四范式这些高级货。但课堂上学完,出了社会一做项目,发现好像也没怎么用上,慢慢就把这东西还给老师了。
其实第三范式(3NF)解决的是关系型数据库里非常实际的问题:数据冗余带来的更新异常、插入异常、删除异常。用大白话讲,就是防止同一份数据在表里存了好几份,等你要改的时候,要么改不全、要么改错、要么根本没法改。
1.1 用订单表的例子理解范式为什么存在
我特别喜欢用一个电商订单的小例子来讲这个事,因为大家都买过东西,代入感强。假设你有一张订单表,里面有订单号、客户名、客户电话、商品名、商品价格、订单日期。看起来挺正常的对吧?
但问题来了:同一个客户下了十个订单,那么客户名和客户电话就在表里存了十遍。有一天客户换了手机号,你需要把这十行数据的电话字段全部更新。如果某个Update语句漏掉了一行,或者并发操作时有人正在读,就会出现同一客户在不同订单里存在不同电话的情况。这就是典型的“更新异常”。
更麻烦的是删除异常。如果这个客户只下过一单,而你把这张订单记录删了,那客户的基本信息也就跟着没了。你只是想删一条交易记录,结果把客户档案删干净了,这数据不乱才怪。
第三范式把这种问题拆解掉。它的核心思想很简单:非主键字段之间不能存在传递依赖,每一列都应该只依赖于主键,而不是依赖于表中的其他非主键列。落在刚才那个订单表上,就是把客户名、客户电话拆到客户表,把商品名、商品价格拆到商品表,订单表只保留客户ID、商品ID、数量、金额、日期这些真正属于“这笔交易”本身的信息。
这样一来,客户改电话只改客户表一处,商品涨价只改商品表一处,订单表里什么冗余都没有,数据始终一致。这个设计思路,对OLTP(在线交易处理)系统来说是命根子。高并发、频繁增删改的业务库,如果不讲范式,早晚会因为数据不一致而出大事故。
1.2 范式建模的优势和它的“笨重”
范式建模的优势,说白了就是严谨。数据一致性强,冗余小,更新成本低,这是它能在业务系统里长期占据主流位置的根本原因。
但范式建模也有一个不得不提的代价:表多,关联多,查询复杂。一个用户下单的行为,你至少得join用户表、商品表、店铺表、订单表、订单明细表好几张表才能把一条完整的业务链路捞出来。如果再加上支付、物流、优惠券这些环节,join个七八张表是家常便饭。
这种模型对于日常的增删改查系统没问题,因为业务系统每次查询的数据量小、条件明确,走索引很快。可一旦把它搬到数据分析场景里,数据量变成千万级、亿级,每条分析SQL都要关联一大串表,性能会非常难看。分析需求又往往是跨主题、跨业务的,让业务方自己写这种复杂SQL基本不现实。
这也是为什么数据仓库领域逐渐发展出了一套完全不同的建模思路——维度建模。它不追求消除冗余,反而“故意”保留冗余,为的是让查询更快、让业务方更好理解。
2. 维度模型:把数据摆成“人话”的样子
维度建模最早是由Kimball提出的,它的核心思路和范式建模正好相反。范式建模追求数据的“规整”和“不重复”,维度建模追求的是查询的“简单”和“直观”。
如果说范式建模是给机器看的,那维度建模就是给人看的。我听第一个师傅讲维度建模时打了这么个比方:事实表是流水账,维度表是字典。流水账记的是每天发生了什么事,字典负责告诉你账本上那些ID到底对应什么含义。你拿着流水账查字典,就能看懂业务全景。这个类比我到现在都觉得特别精准。
2.1 事实表和维度表,到底怎么分
维度建模里最核心的两个概念就是事实表和维度表。
事实表(Fact Table)存储业务过程的度量值,也就是那些可以加总、可以计算的数字,比如订单金额、销售数量、库存余量。事实表的行对应一次业务事件,比如一笔订单、一次点击、一次签收。它的特点是行数极多,膨胀速度快,而且几乎只做新增和查询,很少做更新。
维度表(Dimension Table)存储描述业务事件的上下文信息,比如谁买的、什么时候买的、在哪个渠道买的、买的是什么商品。维度表的行数相对少,但每行的字段通常比较多,包含各种用于筛选、分组、打标签的属性。
拿电商数仓最常见的订单事实表来说,事实表里就是订单ID、用户ID、商品ID、店铺ID、下单时间ID、订单金额、商品数量、优惠金额这些。而用户维度表里则放着这个用户ID对应的注册时间、性别、会员等级、所在城市等等。你在分析的时候想按“会员等级看消费金额”,不用去翻业务库里的用户表,直接join维度表就能搞定。
我见过很多刚入门的朋友搞不清“这个字段该进事实表还是维度表”,有个实用判断标准:字段是否可加总。金额、数量、件数这类能求和、能算平均的度量值,进事实表;地区、分类、时间点这种描述性、用来筛选分组的文本或标识,进维度表。如果它还能被“等于”“不等于”“在范围内”这样的条件过滤,那八成是维度属性。
2.2 星型模型和雪花模型:两条路线的取舍
把事实表和维度表组合起来,最常见的有两种形态:星型模型和雪花模型。
星型模型是维度表直接连着事实表,一张维度表一层,从上面看下去就像星星的四个角。它最大的好处是查询路径短,join层级浅,性能好理解起来也容易。你从订单事实表出发,连接用户维度表、商品维度表、时间维度表,各个维度之间没有依赖,随便你从哪张维度表下手都能直接跟事实表对上。
雪花模型则是在星型模型的基础上,把某些维度表做了进一步的规范化拆分。比如商品维度表,本来可以直接放商品分类、品牌、供应商信息,但雪花模型会把这些拆成独立的分类表、品牌表、供应商表,再逐级关联回去,结构看起来像雪花一样分叉。
这两种模型没有绝对的高下之分,完全看业务场景。大多数情况下我推荐星型,因为它简单、查询快、容易维护。雪花模型虽然减少了冗余,但join链条变长,查询性能和易用性都会打折。一般只有当维度属性本身极其复杂、层级很深、且对存储空间极度敏感时,我才会考虑雪花,但这种场景在现在的硬件条件下越来越少了。
2.3 为什么数仓偏爱维度模型
我把核心原因归结为三点,都是实战里实打实的需求。
第一,查询性能好。事实表无论多大,关联路径都是清晰的、短的,大数据引擎可以提前做好优化。第二,业务友好。一张事实表加几张维度表,业务同事自己拖拽BI工具就能分析,基本不用写复杂的多表join,学习成本大幅下降。第三,指标口径统一。所有团队共用同一套维度和事实,就能避免“一个订单金额,三个部门报出三个数”的尴尬。
我做过一个传统企业数仓的改造项目,之前他们用范式建模搭了一套面向报表的系统,结果固定报表还好,一遇到临时取数,开发就要写半小时SQL再跑半小时任务。后来切成维度建模,重建了核心的事实表和维度表,业务自己三天就能上手搭看板,给数仓团队省下了大量重复取数的工时。这个体验让我坚定了“数仓主题域内,默认维度建模”的原则。
3. 不是二选一:数仓分层的真实分工
如果你问一个做了十年数仓的人,到底是选维度建模还是第三范式,他会告诉你:小孩子才做选择题,成年人看场景混着用。这也是我认为理解这块内容最关键的一步——它们在不同层级各司其职。
我之前在一篇文章里看到有人把离线数仓比喻成“工厂流水线”,我觉得特别形象。原材料进门,经过清洗、加工、包装,最后成品出库。数仓分层就是这条流水线上的一道道工序,每一层用什么建模方式,取决于这一层的职责。
3.1 从ODS到ADS:各层到底干什么
离线数仓最常见也最通用的分法,是ODS、DWD、DWS、ADS这么几层。我逐个说它们在全流程中的定位。
ODS层,贴源层,也常常叫操作数据存储层。它的职责就一个:把业务系统的数据原封不动地搬过来。这一层基本不分建模的事,也不做深度清洗,表结构跟源系统保持一致,主要是为了保留原始痕迹,方便出问题时回溯排查。
DWD层,明细数据层。这一层就是维度建模大展拳脚的地方。核心任务是把ODS层的原始数据做清洗、去重、标准化,然后按照业务过程重新组织成事实表和维度表。比如把几十张结构混乱的订单相关源表,统一清洗后合并成一张标准的订单事实表,把散落各处的用户属性收敛成一张统一用户维度表。这一层尤其讲究“维度退化”和“一致性维度”的处理,后面实操环节细说。
DWS层,汇总数据层。到了这一层,关注重点从细粒度明细转向了“主题”和“指标”。比如按“用户”“商品”“店铺”这些主题,把DWD层的事实按照日、周、月等粒度预先聚合好,形成宽表。宽表里可能一个用户一行,里面放着近30天下单次数、近30天消费总额、最后下单时间,这类预汇总的结果,查询的时候不用现算,直接查就完了。
ADS层,应用数据层。这一层就是给报表、大屏、数据产品直接供数的地方。可能是一张主题宽表,也可能是一张指标看板专用的数据集。这里往往不再关心建模理论,关心的是“出数快不快”“查询稳不稳”“格式顺不顺产品方的意”。
3.2 为什么不能从头到尾只用一种模型
你可能会问,既然维度模型这么好用,为什么不从ODS开始一路维度建模到底?答案是:场景不允许。
ODS层如果直接按维度模型来设计,会带来两个问题。一是数据还原困难,一旦下游发现数据对不上,想对照源系统排查,找不到原始结构会很痛苦。二是ETL开发难度大,如果源头是个设计混乱的老系统,你希望在贴源层就强行改造成标准维度模型,工作量会巨大,而且一旦源系统变更,维护成本也会飙升。
同样,ADS层如果非要做成严格范式化的结构,报表查询就得层层join,极端的查询延迟会让产品方分分钟来找你喝茶。所以真正的工业级数仓,在底层贴源时保留范式化或近范式化的原始结构,在中间明细层和汇总层引入维度建模,在应用层走宽表和汇总表,这是一套最务实、最经过验证的组合拳。
这也是为什么我说“第三范式 vs 维度模型”是个伪命题。它们不是对手,而是流水线不同工位上的两把工具。第三范式在数据进入数仓前,保证了业务系统的干净可靠;维度模型在数仓内部,保证了分析查询的高效和易用。上面提到的Kappa架构之争、流批一体这些热门话题,其实也都是在这个大分工下面讨论的。
4. 实操落地:从业务过程到星型模型
讲了一大堆理论,不落地都是空中楼阁。这一节,我拿一个最经典的电商场景——订单下单,从零走一遍维度建模的操作流程。这套四步走方法也是Kimball在《数据仓库工具箱》里反复强调的,我做过好几个项目都是这套打法,稳得很。
4.1 四步建模法:选过程、定粒度、分维度、定事实
第一步,选择业务过程。你得先明确要分析的是哪一个行为。是下单、支付、发货、退款,还是加购?一个业务过程对应一张事实表,不要多不要少。比如我们这次建模面向“订单下单”这个过程,那事实表记录的就是每一笔订单成交时刻的快照。
第二步,声明粒度。这是整个建模过程中最关键也最容易翻车的一步。粒度就是“一行数据代表什么”。对订单过程来说,粒度可以是“订单头一行”,也可以精确到“订单明细行一行”。如果你的业务事实表里既要汇总整个订单金额,又要拆分到每个SKU的商品购买量,那粒度就应该是订单明细行,否则同一个订单的多行明细累加起来,总金额就重复计算了。
我见过很多踩坑案例,都是因为粒度没想清楚就开干,最后事实表里混着订单级和明细级的数据,金额算出来五花八门。所以这一步我建议跟业务方反复确认:你期望查询结果里最小可以被切到多细?答案就是你的粒度定义。
第三步,确定维度。在粒度确定后,围绕这个业务过程问自己:这些事实是通过哪些角度去看的?时间、客户、商品、店铺、渠道、优惠券,每一类角度就是一张维度表。维度的选择要让业务方常用的筛选和分析口径全覆盖。比如运营天天按地域分析,那地域相关字段就得在用户维度表里体现;如果运营还关心城市级别,那维度表里就不能只放到省份。
第四步,确定事实。也就是事实表里要放哪些度量字段。下单金额、商品数量、优惠金额、运费,这些都是典型事实。注意一点:事实字段必须是可加总的数字,而且是从业务过程里直接产生的。像“单价”这种不可加的、或者需要再计算的字段,一般会作为事实表里的冗余描述或单独处理,不要滥用。
4.2 细节决定成败:代理键、退化维度、一致性维度
建模的大结构搭好了,但真正让模型好用的是细节处理。我在这里分享几个直接影响使用体验的实操点,这些通常在入门教程里不会详细讲。
代理键。很多刚入门的朋友喜欢直接用业务主键当维度表主键,比如直接用源系统的用户ID。但源系统的用户ID可能会因为系统合并、数据修复发生变化,而且不同源系统的用户ID可能冲突。所以数仓里强烈建议每张维度表都自建一个自增代理键,跟业务键解耦。事实表只跟代理键关联,业务键变了不影响历史事实的正确性。这个习惯,能让你避免很多莫名其妙的关联失败。
退化维度。所谓退化维度,就是那些没有自己独立维度表的维度属性。最典型的就是订单号。订单号本身是事实表里的一个字段,但它几乎不会用来做分组分析,可又常常需要用它关联其他系统。与其单独建一张订单维度表,不如直接把这个订单号字段放在事实表里,这就是退化维度。省一张表,少一次join,查询更快。
一致性维度。不同业务过程的事实表之间,如果要能共同分析,就得共享同一套维度表。比如用户维度表只能有一张,所有事实表里关联用户的地方都指向这一张。如果不同团队各自建了“用户维度表”,里面性别、年龄的字段口径还不一样,联表分析时就会出现“同一用户多个性别”的笑话。一致性维度,是数仓能产出统一口径指标的前提。
4.3 缓慢变化维:历史还是要留的
业务系统里的维度属性是会变的,比如用户改了收货地址,商品改了所属类目。对于分析场景来说,有些变化我们需要追踪历史,有些不需要,这就引出了缓慢变化维(SCD)的处理策略。最常用的有三种。
第一种策略,直接覆盖。维度属性变化后直接覆盖原来的值,不保留任何历史痕迹。适合那些改错了也无所谓的属性,比如商品颜色描述。简单,开销小,但历史报表会失真。
第二种策略,新建一条历史维度记录。变化发生时给维度表插入一行新记录,新代理键对应新属性,旧记录保留旧属性。这样历史事实仍关联着旧值,新事实用新值,两全其美。缺点是维度表会有重复的业务键,查询需要小心过滤版本,稍微增加复杂度。
第三种策略,增加历史属性列。在维度表里同时保留原始值和当前值,比如“原始会员等级”和“当前会员等级”。适合那种既要追历史又要查当前的场景,但只能记录一次变化,多次变化就不好使了。
我在实际项目里最常用的是二型和一型的组合:重要属性、需要追溯历史的用二型,不重要的用一型。三型用得少,因为灵活性不够。选择前,一定先问清楚业务方:“这个属性如果变了,你对历史分析口径有什么要求?”答案直接决定你选哪个策略。
5. 常见问题与排查技巧实录
再成熟的建模方案,落地过程中都会遇到各种幺蛾子。这一节我整理几个我在维度建模实战中踩过、也帮同事排查过的典型问题,每一个都对应一个实际场景,希望能帮你少走弯路。
5.1 事实表与维度表关联不上:那些“消失”的维度记录
这几乎是每个数仓新手必遇到的问题。事实表里某个维度ID在维度表里找不到对应记录,导致join之后数据大量变少或者出现NULL。主要原因通常是:源业务系统存在脏数据,比如用户注册信息缺失,或者订单里的商品ID被软删除。
我的排查思路是三步。第一步,先检查关联结果,单独跑一条LEFT JOIN找出空值占比,确认问题规模。第二步,溯源业务系统,找对应的源头表查缺失ID是否存在,判断是同步的问题还是源系统的数据问题。第三步,决定处理策略。常规操作是在DWD层建模时对这类“孤儿外键”建立一张“未知维度”兜底记录,比如用户维度表里放一行“用户ID = -1,用户名 = 未知”,让事实表关联时永远有地方可去,不会丢数据。
5.2 聚合结果对不上:粒度混乱的锅
我之前有一次做订单分析报表,运营反馈说“后台显示120万营业额,看板却只有90万,差了30万”。排查了很久,最后发现原因就是事实表里同时存在订单级和明细级两种粒度的数据。同一个订单如果包含多个商品,明细级会有多行,订单金额在每个明细行里都重复出现,一按商品维度汇总求和,总额就被放大。
这个问题的根治方法只有两个字:治粒度。如果粒度定义是明细行,那订单金额就不能原样放进明细事实表,而是要拆成商品分摊金额,或者明确告诉业务方只从订单级事实表取总额。我的建议是,同一张事实表里永远只保有同一种粒度,宁可多建一张事实表,也不要硬把不同粒度的数据塞在一起。
5.3 维度表质量失控:谁来保证维度的准确性
维度表一旦建好,它就是全员共用的口径字典。可如果这张字典本身没人维护,里面脏数据越来越多,全公司的报表都会跟着遭殃。我见过非常夸张的例子:一个“渠道维度表”里,同一渠道的名称被注册了七八种写法,什么“APP”、“App”、“app”、“手机APP”,ETL不做标准化清洗直接进表,结果前端筛选时误以为有七八个渠道。
踩过这个坑之后,我在项目里定了一条规矩:维度表的构建和变更必须有明确的所有者和审批流程。谁提供原始数据、谁清洗标准化、谁批准新增维度属性,都要落到具体的人头上。同时,在ETL流程里设置字段质量校验,比如枚举字段自动做映射、统一大小写格式、校验必填字段不能为空。质量这东西,靠自觉不行,必须靠机制。
5.4 离线任务延迟:维度建模背不背这个锅
最后说一个偶尔会被人误解的现象。有人发现数仓任务变慢,就归咎于“维度建模join太多”。但以我的经验看,join多导致的慢,大多是因为事实表没做好分区裁剪,或者维度表设计冗余太重。
排查时我会先看执行计划,确认数据扫描量是不是已经控制在合理分区内。如果扫描量正常但仍慢,再检查维度表是不是过于宽大,比如一张维度表塞了两百多个字段,很多字段都还很长,拖慢了join性能。这时候可以做字段瘦身,把低频使用的长文本字段拆出去单独存一个扩展表,维度表本身保留高频字段。90%的情况下,优化完这两个点,任务性能都会有明显提升。
6. 最后聊点实在的
说了这么多,其实还是想强调那句话:维度模型和第三范式不是对立的,它们是两种不同目标下的产物。范式让系统“不出错”,维度让数据“好用”。做数仓,最忌讳的是把一种方法论焊死在所有场景上,灵活运用才是真正的能力。
我个人在实际项目中的体会是,入门阶段可以先不去纠结Kappa架构、流批一体这些更宏大的话题,先把离线数仓的分层职责和维度建模这套基本功吃透。因为不管是实时还是离线,最终都要面对“事实+维度”这套分析体系,都要处理一致性和粒度的难题。基本功扎实了,后面学什么架构都是在这个地基上盖楼。
最后再贡献一个我特别想推荐给你的小习惯:每次新建一张事实表或者维度表之前,先写一份不超过一页的设计说明文档,包含业务过程、粒度定义、维度清单、事实清单、更新频率和负责人。别觉得这是形式主义,这张纸在半年后,绝对能救回你因为遗忘设计判断而浪费的一整天排查时间。
数据建模这条路没有捷径,但在正确的方向上反复打磨,你会看到自己的设计越来越稳、越来越顺。共勉。