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层做宽表或冗余字段因为查询性能确实重要。关键是这种冗余要显式记录要能追溯到它的来源并且所有下游都清楚这张表只是针对特定场景的加速不是公共标准。有意的冗余是优化无意的冗余是灾难。第三点体会是高复用数仓的构建是一个持续的、滚雪球式的过程。第一套模型可能不够好但只要你有指标字典、有血缘管理、有评审机制每新增一个模型都会让整个体系更完整、更稳定。反过来如果一开始不重视复用性等模型多到一定程度再回头重构成本和风险都会高得多。所以如果你正准备开荒数仓请在第一天就把复用性刻在建模流程里而不是等到被业务方逼得走投无路时再想起它。