1. 为什么我反复重构数仓:复用性差的代价
1.1 从一个本应很简单的需求说起
上周我又接到一个业务需求:统计近30天每个渠道的付费用户数、付费金额和付费率。听起来是个再普通不过的指标,按我的经验,这种需求在模型设计合理的数仓里,应该是十分钟就能跑出来的。结果我在模型层翻了一圈,发现用户维度和订单维度散落在三套不同的表里,渠道字段一会儿存在DWD层的事实表,一会儿单独挂在维表,还有一套老模型直接冗余在ADS层。最后我花了三个小时梳理口径,又花了两个小时写清洗逻辑,才把数据对平。
这不是第一次了。几乎每隔几周,我就会因为"找不着一个能直接复用的模型"而被迫从ODS原始日志重新开始。真正刺痛我的不是多花了几个小时,而是我发现:数仓里几百张表,真正被反复引用的可能不到二十张。大部分表建完之后,除了当初那个需求,再也没人用过。这就是复用作废——模型建了不少,资产却没沉淀下来。
1.2 复用性差的三类典型症状
我把自己经历过的、以及帮朋友排查过的数仓问题梳理了一遍,复用性差的症状基本可以归为三类。
第一类是烟囱式模型。每个业务方或每条产品线各建各的表,用户ID字段有的是user_id,有的是uid,有的是member_id;日期字段有的是dt,有的是stat_date,有的是partition_time。单看任何一张表都能用,但要把两张表join起来,先得做字段映射,再对口径。这种模型本质上是为单一需求定制的SQL视图,只是被固化成了表。
第二类是口径漂移。同一个"活跃用户"的定义,在A报表里是"当天有登录记录的用户",在B报表里是"当天有任意行为事件的用户",在C报表里又变成了"近7天活跃且当日有访问的用户"。三个口径都有道理,但数值就是不一样。业务方拿着三份报表来质问数据团队,我们只能一遍遍解释"统计口径不同"。这种解释本身就是复用性差的体现——指标没有在模型层统一,每个需求方都在用自己的方式重新定义。
第三类是过度复用。听起来有点反直觉,但确实存在。有人为了追求"复用",把所有业务逻辑全部塞进一个大宽表,几十个字段堆在一起,导致任何一个字段口径调整,整张表就要重刷,下游几十个任务全部受影响。这种表表面上被大量引用,实际上成了"公共单车"——谁都在用,谁都不维护,最后谁都不敢碰。
1.3 复用性就是数仓的"资产收益率"
如果把数仓比作一个工厂,ODS是原材料仓库,DWD是零部件生产线,DWS是标准组件车间,ADS是给业务方的最终产品展厅。一个工厂能不能高效运转,不取决于它囤了多少原材料,而取决于它的零部件能不能被不同产品线反复使用。数仓也是一样——评价一个数仓好坏,不能只看表数量和任务数量,而要看"每次新需求需要新建多少模型、改动多少模型"。
我把这叫作数仓的"资产收益率":投入同样的建设成本,产出能被复用的模型越多、每次需求改动越小,资产收益率就越高。高复用性的数仓,不是靠某一个天才模型设计出来的,而是靠一套关于分层、命名、口径、粒度的共识机制,让每个人在建模时都默认"这张表将来会被别人用,我要让它能被别人用"。这才是设计和构建一个高复用性数仓的真正核心。
2. 高复用数仓的地基:分层架构与公共层的取舍
2.1 分层不是越多越好:ODS/DWD/DWS/ADS各自的边界
很多人一谈数仓分层,张口就是ODS、DWD、DWS、ADS四层,觉得层数越多越专业。我自己见过一个极端案例,某团队分了七层,结果数据链路长得像盘山公路,一个指标从ODS到最终报表要经过六层调度,中间任何一层延迟,全链路都飘红。分层本身不是目的,分层是为了控制变更影响面和明确复用边界。
我通常建议把分层职责收敛为四句话:
- ODS层:贴源存储,保留原始数据,不做业务清洗,只做最简单的格式规整和分区管理。这一层是数仓的"底账",出了问题可以随时回溯。
- DWD层:明细层,负责把事实数据和维度数据按照业务过程重新组织,做清洗、脱敏、维度退化,形成一致的明细模型。这一层是复用性的第一道分水岭,几乎所有下游模型都建立在DWD之上。
- DWS层:汇总层,按照主题(用户、商品、商家、渠道等)对明细数据进行轻汇总,形成公共指标。这一层是复用的主战场,业务方的日常需求大部分应该在这里命中。
- ADS层:应用层,面向具体报表和应用,可以适度冗余,允许为了查询性能做宽表和预聚合。但这一层严禁出现复杂的业务逻辑加工——所有口径都应该在DWD/DWS定义好,ADS只是"取数"。
这套四层模型并不新鲜,真正容易出问题的是层与层之间的边界模糊。最常见的混乱是:有人把指标计算的逻辑直接写在ADS层,从ODS一路join过来,绕过了DWD和DWS。结果就是每张ADS表都在重复定义口径,一旦上游数据源变化,所有报表跟着遭殃。判断一个模型该放在哪一层,标准不是它的名字,而是它的复用程度——如果多个ADS表都会用到同一个汇总逻辑,这个逻辑就应该下沉到DWS;如果多个DWS表都会用到同一个明细清洗逻辑,就应该下沉到DWD。
2.2 什么该下沉到公共层,什么该留在业务层
公共层的设计是高复用数仓的核心动作。但"下沉"不是无脑做,下沉错了反而制造维护灾难。我总结了一个简单的判断规则:一个逻辑是否下沉,取决于它是否满足"三同"原则——同样的输入、同样的处理、同样的口径,并且被至少两个以上的下游引用。只被一个需求使用的逻辑,哪怕再简单,也别急着下沉。
举个例子,订单明细表里有一个字段叫"订单状态",业务上有待支付、已支付、已发货、已完成、已取消等多种状态。如果只有风控团队关心"取消原因",那就没必要在DWD层设计一套复杂的取消原因枚举映射,直接保留原始状态码,在风控建模时自行处理即可。反过来,像"订单是否为有效订单"这种几乎所有分析场景都会用到的判断逻辑,就应该在DWD层统一定义成字段,比如is_valid_order。否则每个需求方都会自己写一遍WHERE status != 'CANCELED' AND amount > 0,写的人多了,口径必然漂移。
公共层设计还有一个容易忽略的维度:数据时效性。有些逻辑适合在离线层下沉,但实时链路不一定能复用。比如用户在APP上的行为日志,离线DWD可以按照"会话结束"来划分session,但如果实时风控需要的是毫秒级会话判断,那离线的那套逻辑就完全不适用。这时候强行为了"复用"而要求实时链路也用离线口径,只会让实时任务变成定时批处理,失去实时意义。公共层的复用一定要限定在同一个处理范式内,离线归离线,实时归实时,跨范式共享的是维度定义和指标口径,而不是具体的加工节点。
2.3 命名规范与模型设计:让模型像乐高一样可拼装
复用的前提是"容易找到"和"容易理解"。一个数仓里如果表名随意起,字段名全靠猜,那就算模型设计得再合理,别人也不知道该不该复用、怎么复用。我强烈建议在设计数仓的第一天就确定一套命名规范,并且用工具强制约束。
我常用的命名规范是:层级_主题域_业务过程_粒度_累加类型。举个例子:
dwd_trade_order_detail_di:交易域订单明细,日增量dws_user_user_all_1d:用户主题域,全量用户信息,近1日快照ads_app_channel_retention_7d:应用层渠道留存报表,7日窗口
主题域要提前划好,不要等表建多了再补。一般电商类数仓可以划分为用户域、交易域、商品域、营销域、渠道域、流量域等。每个域的表在命名时都要带上域标识,这样下游看到表名就知道从哪找、怎么用。
字段命名同样要有规矩。user_id就统一叫user_id,不要一会儿uid一会儿member_id;日期分区统一叫dt,类型为STRING,格式为yyyy-MM-dd,不要一会儿YYYYMMDD一会儿yyyy-MM-dd HH:mm:ss。这些事看起来琐碎,但恰恰是复用性最基础的地基。我见过一个团队,因为日期字段格式不统一,导致大量join时要做TO_DATE转换,不仅性能差,还经常因为时区问题出数据错误。命名规范最大的价值不是美观,而是让每个模型都成为其他人不需要读文档就能使用的"标准件"。
关于模型设计,还有一个常被忽略的点:尽量避免使用"覆盖全宇宙"的大宽表,也尽量避免过度拆分的"碎片表"。大宽表的问题是变更影响面大,碎片表的问题是join成本高。我个人的经验是:同一主题域内,如果两个模型有超过50%的字段是重复的,就应该考虑是否可以把公共字段抽出来做一层底层模型;如果两个模型只有不到20%的字段重合,就完全没有必要强行合并。这个比例可以根据实际情况调整,核心思路是让每个模型都具备"单一职责",能够独立被复用,同时又是更大组装的零件。
3. 维度与事实的复用:一致性比覆盖面更重要
3.1 一致性维度的坑:一个用户ID引发的"罗生门"
在做数仓之前,我以为"用户"这个概念是天然的,所有表里的用户都是同一个人。真正做过数仓之后才发现,用户这个维度在不同业务系统里有完全不同的实体标识。订单表里的用户来自交易系统,用member_id表示注册用户ID;行为日志里的用户来自埋点系统,用distinct_id表示设备匿名ID;客服系统里的用户又有一套customer_no。三套ID对应同一个自然人,但业务方要求的是"同一个人的跨端行为串联分析"。
如果数仓不做一致性维度治理,就会发生这样的场景:运营部门要分析"注册用户中来自抖音渠道的转化率",数据团队从订单系统捞出member_id,从渠道系统捞出distinct_id,两者join不上,最后只能按照手机号模糊匹配。结果数据严重失真,业务方不敢用,数据团队不断被质疑。
解决这个问题的方式是在DWD层建立一套用户ID映射表,把所有业务系统的ID都统一映射到主ID上,同时把is_new_user、user_register_time、user_channel等常用属性冗余进来,形成用户维表。这样下游所有事实表在关联用户维度时,只需要通过这张统一的用户维表,而不需要关心底层ID来自哪个系统。这张维表就是一致性维度的核心载体,也是整个数仓复用性的命脉之一。
3.2 事实表的粒度声明:复用前必须钉死的契约
事实表的粒度是复用性最容易翻车的地方。所谓粒度,就是一张事实表中每一行代表什么。订单明细表的粒度通常是"订单行"(每行一个订单项),支付流水表的粒度是"支付流水",用户行为日志表的粒度是"一条埋点事件"。粒度不同,表之间的join、汇总、去重逻辑都会不同。
我见过一个事故:有同事在统计"每个用户的订单数"时,直接COUNT(*)了一张订单明细表,结果因为这张表每个订单有多个商品行,订单数被放大了一倍。这种错误的根源不是SQL写错,而是没有理解事实表的粒度。后来我们做了一个硬性规定:每张事实表在模型设计文档中必须明确声明粒度,并且在物理表上通过一个或多个业务主键字段来保证粒度的唯一性,比如订单明细表的order_id + sku_id联合主键。
更重要的一个原则是:同一张事实表不要混搭不同粒度。有的人为了省事,把订单明细和退款明细放在一张表里,用业务类型字段区分。初看没问题,但一旦某个下游需要"订单金额总和"时,就很容易把退款金额也加进去。这种表看似复用,实际是个定时炸弹。如果一定要放在一起,至少要在表名上明确标注是多业务过程混合表,并且用is_refund这类标志位约束清楚,绝不能让业务方误以为这张表只有订单明细。
3.3 退化维度与缓慢变化维:复用性和存储成本的平衡
维度设计里有个很现实的问题:维表要不要保存历史变化?比如用户从北京搬到了上海,商品从"食品"类目调整到"生鲜"类目,如果维表直接覆盖更新,那么分析"用户所在城市对购买行为的影响"时,历史订单会错误地关联到当前城市。这是缓慢变化维(SCD)问题。
高复用性的数仓通常采用SCD2方案:维表保留多条历史记录,每条记录带start_dt和end_dt,在事实表关联维表时按业务日期取当时的维度属性。这样虽然增加了存储成本和join复杂度,但换来了时间维度的正确性。我的建议是:只有真正会用于时间切片分析的属性才需要做SCD2,比如用户地域、用户会员等级、商品类目,其余高频变化的属性(如用户最近一次登录时间)用拉链表或快照表即可,不需要搞复杂的历史版本记录。
另外还要注意退化维度的处理。有些维度属性并不需要单独建维表,可以直接退化到事实表里,比如订单号、渠道ID、支付方式。退化的好处是减少了join,提升了查询效率。但退化维度的命名也要统一,否则还是会出现口径漂移。我们规定:凡是出现在事实表里的维度字段,都以dim_前缀开头,例如dim_channel_id、dim_pay_type,这样模型阅读者一眼就知道哪些字段是维度、哪些字段是度量,大大降低了理解成本。
4. 指标复用:从口径混乱到指标字典
4.1 一个指标两种口径,报表怎么敢信
有一次业务方开周会,市场部说"本月新增用户12.8万",运营部说"本月新增用户15.6万",两边都声称自己用的是数仓的数据。最后查下去,市场部统计的是"本月经由广告渠道进入且完成注册的用户",运营部统计的是"本月任意渠道首次启动APP的用户"。两个口径都有道理,但放在一起就会让管理层觉得数据团队不靠谱。
这就是指标口径失控的典型后果。单看每个团队内部,指标是合理的;从全局看,同一个指标名称却被赋予了不同的定义,复用完全无从谈起。指标复用不是把SQL封装成函数,而是把口径变成组织共识。这意味着,指标名称、计算公式、统计周期、筛选条件都要有唯一的、被各方承认的定义。
4.2 原子指标、派生指标与复合指标怎么分层管理
要做好指标复用,我建议把指标拆成三层来管理。
第一层是原子指标。这是最基础的度量,比如"订单金额""支付金额""新增用户数""活跃用户数"。原子指标绑定具体的统计粒度(比如订单明细的支付金额),不可再拆分。原子指标的定义要尽量贴近业务过程,不要加任何复杂的过滤条件。
第二层是派生指标。在原子指标基础上加上限定条件形成的指标,比如"近7天通过抖音渠道的支付金额""当天首次启动APP的用户数"。派生指标可以记录为"原子指标 + 维度限定 + 时间周期"的组合,方便在指标平台上通过配置生成,而不是每个人手写SQL。
第三层是复合指标。由多个原子指标或派生指标通过四则运算得到的指标,比如"支付转化率 = 支付用户数 / 活跃用户数""客单价 = 支付金额 / 支付用户数"。复合指标要基于前面两层来定义,不允许跳层直接写死数值。
这样分层的最大好处是:当业务口径变化时,优先修改原子指标或派生指标,下游所有基于该指标的复合指标都会联动更新。如果每个指标都是手工SQL,改口径就要逐张表排查,工作量呈指数级增长。
4.3 指标复用不是写死SQL,而是定义"可计算的能力"
我曾经看过一个团队的指标管理Excel,里面写了上百个指标,但真正到了建模阶段,发现Excel里的指标和模型里的字段对应不上。原因是Excel记录的只是"指标名称和计算逻辑",模型里却是一个个物理字段,两者之间缺乏映射关系。
我后来的做法是:把指标和模型字段做绑定,在指标字典里明确指出每个指标来源于哪张模型表、哪个字段、经过了什么样的过滤条件。比如"支付金额"这个原子指标,绑定的是dws_trade_pay_pay_1d表的pay_amount字段,粒度是"用户-日期-支付渠道"。一旦这个字段的加工逻辑需要调整,通过血缘关系一眼就能看到哪些下游指标会受影响。
在这个基础上,还可以把常用的过滤条件沉淀成"指标模板"或"口径标签"。比如"新用户"这个标签,定义为"首次支付时间在统计周期内",在任何模型中需要筛选新用户时,都引用这个标签,而不是各自写WHERE first_pay_date >= ...。标签和指标一样,属于数仓的公共资产,被复用得越多,口径一致性就越高。
5. 构建高复用数仓的实操路径与关键动作
5.1 从零开始搭建一套可复用数仓模型的七个步骤
如果你所在的公司还没有数仓,或者现有的数仓已经乱成一团,想要从零开始搭一套高复用性的模型,可以参考我常用的七个步骤。这套路径不依赖具体平台,Hive、Spark、Doris、ClickHouse都可以落地。
第一步,盘点业务过程。把公司核心业务流程画出来,从注册、活跃、下单、支付、退款到复购,每个业务过程涉及哪些业务系统、哪些核心实体、哪些度量字段。先画流程,再谈建模,不要上来就建表。
第二步,划分主题域。基于业务过程梳理出主题域清单,比如用户域、交易域、商品域、流量域、营销域。主题域之间要尽量低耦合,比如"交易"和"营销"有交叉,但各自的职责要清楚,营销域负责活动维度,交易域负责订单事实。
第三步,定义核心维度。明确每个业务过程中需要分析的维度,如用户、商品、商家、渠道、时间、地域。为每个维度确定主键、常用属性和SCD策略。这一步产出的维度清单就是后续所有事实表关联的"公共钥匙"。
第四步,设计事实表。按照业务过程拆解事实,确定每张事实表的粒度,列清度量字段和关联维度。尽量做到"一个业务过程一张事实表",比如支付单独一张、退款单独一张,而不是混在一起。
第五步,构建公共层模型。先从DWD层开始,建好清洗后的明细模型;再基于DWD层,按照主题域建设DWS层公共汇总层。公共汇总层的设计可以反推:把业务方最常用的20个指标列出来,反向设计能够支撑这些指标的公共模型。
第六步,沉淀指标字典。把指标和模型字段一一映射,建立指标分层(原子/派生/复合),并接入元数据管理系统,形成数据血缘。
第七步,建立模型评审机制。每次新建或修改模型,都要经过复用性评审,确认是否有重复建设、是否有更好的公共模型可以复用、命名和口径是否符合规范。这一步是保证数仓持续健康的关键,也是最容易被省略的一步。
5.2 建模工具选型、血缘管理与版本控制
工欲善其事,必先利其器。高复用性数仓不能只靠文档和口头共识,必须有工具支撑。我在实际项目里比较推荐这几个方向:
- 模型设计工具:可以用
draw.io或Enterprise Architect画ER图和分层架构图,也可以用专业的Erwin、PowerDesigner。如果团队偏好轻量方案,直接在Markdown里维护模型设计文档也可以,但一定要保证文档和真实表结构同步更新,否则文档很快就会失效。 - 元数据管理:建立数据字典和血缘关系,开源的
Apache Atlas、DataHub都可用。如果没有精力部署,那至少要在调度平台(如DolphinScheduler、Airflow)中维护好任务依赖关系,让每张表的上下游血缘清晰可查。 - 模型版本控制:所有的建表DDL、ETL任务代码,必须纳入Git管理。每次模型变更都要走MR/PR流程,由至少一位同事评审。表结构变更要同时更新模型设计文档和指标字典,绝不允许"改了代码没改文档"。
这里我特别想提醒的一点是:血缘管理不是等表多了再做,而是从第一张表开始就做。我在一个中型公司见过,因为有血缘关系图,一次上游接口字段调整,十分钟就圈定了所有受影响的下游任务,改了三个模型就完成了修复。而另一个团队因为没有血缘管理,同一个问题排查了一整天,最后还是业务方先发现了数据异常。这中间的差距,不是技术能力,而是基础设施的差距。
5.3 复用性评审:每次模型变更都要过五道关
很多团队的模型设计评审只关心"能不能跑通""性能好不好",很少关心"这个模型别人以后怎么用"。我在自己负责的数仓团队里,规定每次模型变更都要过五道关,任何一道不过都不能上线。
第一关:重复性检查。这个模型或字段是否已经存在于其他模型中?能不能通过改造现有模型来满足需求?如果没有做过模型检索就新建表,打回重做。
第二关:粒度分明。事实表的粒度是否明确?同一张表内是否存在多个粒度?如果粒度混乱,打回。
第三关:命名规范。表名、字段名是否符合命名规范?是否属于正确的分层和主题域?dws_开头的表如果放在ADS层使用,说明分层设计有问题。
第四关:口径一致。所有涉及指标的口径是否与指标字典一致?如果某个"活跃用户"的统计口径和已定义的指标不同,必须解释原因并更新字典。
第五关:影响面评估。这个模型变更会影响多少下游任务?是否需要同步修改依赖这个模型的ADS层报表?如果影响面超过10个下游任务,必须给出详细的变更方案和灰度计划。
这五道关看起来简单,但在日常工作里非常管用。它逼着建模的人在动手之前先想"复用",而不是先图自己一时方便。这也是我见过的高复用数仓团队和普通数仓团队之间最大的区别——高复用不是某个人设计出来的,而是整个团队在评审机制中"逼"出来的。
6. 写在最后:三点个人体会
做数仓这么多年,我最大的体会是:复用性不是一个技术问题,而是一个"反人性"的问题。人的本能是"尽快满足当前需求",而复用性要求"多花十分钟想想以后"。如果没有机制保障,绝大多数人会选择最快路径——新建一张表、写一段一次性SQL,然后把它抛在脑后。这也是为什么很多数仓一开始还算干净,半年后就变成一团乱麻。
第二点体会是:复用性要允许一定的冗余,但冗余必须是有意的。我不反对在ADS层做宽表或冗余字段,因为查询性能确实重要。关键是这种冗余要显式记录,要能追溯到它的来源,并且所有下游都清楚"这张表只是针对特定场景的加速,不是公共标准"。有意的冗余是优化,无意的冗余是灾难。
第三点体会是:高复用数仓的构建是一个持续的、滚雪球式的过程。第一套模型可能不够好,但只要你有指标字典、有血缘管理、有评审机制,每新增一个模型都会让整个体系更完整、更稳定。反过来,如果一开始不重视复用性,等模型多到一定程度再回头重构,成本和风险都会高得多。所以,如果你正准备开荒数仓,请在第一天就把"复用性"刻在建模流程里,而不是等到被业务方逼得走投无路时再想起它。