十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

数据建模实战:业务口径、数仓分层与维度建模全解析

数据建模实战:业务口径、数仓分层与维度建模全解析 接到一个数据建模需求最怕的不是不会写SQL而是上来就按业务方口述的几个字段开始建表。我见过太多人一听说“做个用户主题模型”就立刻画ER图、定主键结果做到一半发现底层数据对不上业务口径又改了整个模型推倒重来。数据建模这条路上的核心矛盾从来不是技术而是“没想清楚就动手”。这行干了这些年我越来越确认一件事建模工作七成在建模之前。这篇文章我按自己的实战经验把数据建模从需求拆解、分层规划、方法论选择到具体建模实操和上线后的治理优化整个链路梳理一遍。内容偏数仓方向适合刚入行的数据开发、正在做数据建模相关毕业设计或面试准备的读者也适合那些已经被混乱的报表和口径折磨到不行、想系统搭建数仓模型的同学。文的实操部分我尽量把每一步的为什么讲透这样你拿过去能直接用用的时候也知道自己是站在哪个岔路口做的选择。1. 接到建模需求先别急着建表业务口径比ER图更重要我见过太多新人拿到需求的第一反应是打开Navicat开始设计表结构。这个顺序是错的。数据建模的第一步是先把业务方的“话”翻译成“可计算的定义”。翻译错了后面所有结构都白搭。1.1 同一个指标三种口径三套数字举一个实际例子。假设业务方说“我要看用户转化率”你能直接建表吗不能。你先要搞清楚几个问题用户指什么是注册用户、启动过App的用户还是下过单的用户转化指什么指从注册到首次下单还是从浏览到加购分母是什么是当天的活跃用户还是当天有曝光行为的用户时间口径是什么自然日、工作日还是业务自定义的“昨日21点到今日21点”同一个“用户转化率”不同业务方可能给出三个完全不同的口径定义。你在建模文档里写“转化率转化用户数/活跃用户数”这个字段模型就埋下了一颗雷。等到了报表层A部门说你这个数不对B部门说你这个口径跟我们不一致光是吵架就能耗掉你一周。建模之前的对齐工作业内一般叫“指标口径梳理”。你需要把每个指标拆成原子指标、业务限定、统计周期、聚合粒度。原子指标是“加购次数”“支付金额”这种不可再拆的度量业务限定是“仅限安卓端”“排除测试订单”这种过滤条件统计周期是“自然日”“近30天”聚合粒度是“按用户”“按店铺”“按商品SPU”。我习惯在动手建模前先输出一份指标口径清单表格长这样指标名称原子指标业务限定统计周期聚合粒度导出公式支付金额订单实付金额剔除测试订单、剔除退款订单自然日用户SUM(实付金额)成交用户数用户ID且有支付成功订单自然日用户COUNT(DISTINCT user_id)客单价支付金额同上自然日用户支付金额/成交用户数这份清单让业务方确认签字后面所有模型设计和指标开发都以此为准。别嫌这一步繁琐它替你省掉的是未来无数个“这个数怎么对不上”的深夜。1.2 主题域划分先有坐标系才能给表“定位”口径理清之后下一步不是建表而是划分主题域。主题域是数据分析的最高层级分类它决定了你的数仓从宏观上是如何组织的。怎么理解主题域把企业想象成一棵大树主题域就是主要枝干。电商公司一般可以拆成用户域、商品域、交易域、营销域、流量域、会员域、供应链域。每个域内部做高内聚域与域之间通过“公共键”做关联。为什么要先划分主题域因为如果不先定这个“坐标系”后面每来一张表你都可能纠结它该放哪。订单表既跟用户有关又跟商品有关如果你没有域的概念就可能导致用户域放了一张包含商品明细的大宽表交易域又隔三差五建出几张字段重叠的“伪明细表”。时间一长整个数仓变成一锅粥数据血缘乱成一团。主题域划分完还要做一件事总线矩阵。总线矩阵描述的是“哪些业务过程跟哪些公共维度相关”。它是一个二维矩阵行是业务过程比如“下单”“支付”“退款”列是公共维度比如“日期”“用户”“商品”“店铺”交叉点打钩表示该业务过程涉及这个维度。这张矩阵的作用是保证你后续每个事实表都能和既有维度表对齐。想象一下如果你做交易分析时发现用户维度表里还没有覆盖“新老客标志”而营销域又依赖这个标志去圈人链路就断了。有了总线矩阵你就可以在建事实表时一眼看出日期维度是否齐备、用户维度是否能关联上、商品维度是否覆盖到。1.3 这一步的实操输出物不要觉得建模前的分析阶段是“虚”的。好的分析阶段应该能输出三样东西缺一不可指标口径清单——上面举例的Excel或者在线表格所有参与方确认过。主题域划分文档——树状图或者列表形式标清楚每个域的边界和责任人。总线矩阵——业务过程 x 公共维度的关系矩阵。这三样东西一出来你的模型骨架就已经在脑子里成形了。接下来去画逻辑模型、物理模型你会发现顺得不可思议因为每个表的定位都是清晰的。这就像盖楼之前先出建筑图纸你可以说图纸阶段“什么都没盖”但没有图纸直接垒砖垒出来的大概率是危楼。2. 分层架构是数据建模的骨架ODS、DWD、DWS、ADS每层该干什么很多没系统做过数仓的同学会把数据建模等同于设计几张表。实际上在正规的数仓体系里任何一张模型表都必须先找到自己的“层级坐标”。没有分层的概念所有表平铺在一起就谈不上建模了。2.1 为什么要分层可管理、可复用、可追溯我给非数仓方向的读者打个比方。分层就像餐厅后厨的分工ODS层是“食材收货区”市场上买回来的菜先堆在这里基本不动。DWD层是“清洗切配区”把菜洗干净、去根、切成标准大小。DWS层是“预制菜区”把配好的料按套餐装好比如“宫保鸡丁套餐”。ADS层是“出菜口”哪个桌点了菜直接从这里端出去。如果后厨没有这些分区所有菜、肉、调料堆在一个房间厨师炒菜时要自己去找葱姜蒜配菜员洗菜砧板和成品切配混在一起整个厨房的效率就会灾难性地低甚至食品安全都会出问题。数仓分层也是同一个逻辑ODSOperational Data Store贴源层存放从业务库同步过来的原始数据不做或少做加工保留最细粒度用于数据追溯和审计。DWDData Warehouse Detail明细层对ODS做清洗、转换、降维、标准化以业务过程为单位建模保持明细粒度。DWSData Warehouse Service汇总层面向分析主题做轻度汇总通常是按维度预先聚合比如“用户当天的下单金额”这种按天、按用户汇总的宽表。ADSApplication Data Store应用层面向具体报表或业务取数结果一般是一张可以直接查询的窄表或宽表。你去看招聘网站的“数据建模”岗位要求几乎都要求候选人能说清楚这几层的职责。大数据面试题里出现“数仓分层架构”这类问题本质上就是考察你有没有全局视角而不仅仅是会写SQL。2.2 每层的建模目标、表命名和字段设计要点分层的意义在于让每层有明确的“加工边界”。我在实际项目中给每层定了四类约束方便团队照做。先看ODS层。这一层的建模目标是“原样接入留痕可查”。ODS表结构与业务源表保持一致通常加五个管理字段etl_time抽取时间、data_date数据日期、source_system来源系统、is_deleted逻辑删除标记、etl_version批号。设计ODS层最容易犯的错是“顺手就把数据清洗了”这样做会让数据追溯非常痛苦出问题时根本分不清是源系统的问题还是ETL的问题。然后是DWD层。这一层的建模目标是“统一清洗、标准化、维度退化”。DWD表通常按业务过程建模比如下单、支付、退款、收藏、加购各一张明细表。字段上需要把ODS中各种异味的枚举值做统一编码把时间字段统一为yyyy-MM-dd HH:mm:ss格式把同义字段比如user_id和uid合并成同一个命名。一个常见做法是把“用户昵称”“商品名称”这类描述性文本退化到事实表里避免每次查询都要去JOIN维表这就是维度退化后面第4节会详细说。DWS层的建模目标是“面向分析主题的公共汇总”。和DWD不同DWS通常是宽表按主题把一个用户、一个商品、一个店铺的核心指标聚合到一行。典型设计是dws_user_nd用户n日汇总表每个用户一行行为包含近1日、近7日、近30日的下单次数、下单金额、支付金额、退款金额等。这样做的好处是下游95%的报表都可以直接查这一张表不用每次从DWD全量跑聚合。ADS层就没什么建模好讲了它就是为具体需求服务的“取数表”设计上以直接满足页面展示为目标。但要注意的是不要让大量临时取数逻辑沉淀到ADS层。ADS每天被业务查询最多的字段应该反向推动DWS层补字段而不是任由ADS层无限膨胀。各层的命名规范我贴一下方便你直接抄分层前缀示例粒度ODSods_ods_trade_order_di源系统原始粒度DWDdwd_dwd_trade_order_detail_di业务过程明细粒度DWSdws_dws_user_order_stats_1d主题维度的汇总粒度ADSads_ads_user_gmv_ng报表需求粒度命名里后缀di表示增量df表示全量1d表示按天累计nd表示n日累计。这些约定不复杂但是统一之后团队沟通成本会低很多。2.3 各分层之间的质量“守门”分层不只是物理上一张张表更重要的是层与层之间要有数据质量校验。我见过最糟糕的情况是DWD算出来一个用户有300多笔订单结果ODS源头只有280笔且没人发现报表直连DWD最后业务方拿着错误数据去做了经营决策。所以我在团队里强制实行“逐层质量校验”ODS到DWD主键唯一性校验、非空约束校验、枚举值合法校验、时间字段合法性校验。也就是保证明细不重不漏。DWD到DWS关键指标对比比如DWS的当日支付金额汇总是否等于DWD按日聚合的结果。DWS到ADS抽样对比随机抽10个维度项比对最近30天的汇总值是否与DWS一致。这些校验可以写成自动化脚本挂在调度系统里每天跑完数据自动执行。一旦校验不通过要能第一时间告警阻塞下游任务。分层的作用本质上是把一个巨大的数据处理问题拆成多个可以在局部做质量管控的小问题。每一层把自己的边界守住整个数仓的数据质量才有保障。别觉得分层“多了几张中间表浪费了存储”坏数据导致决策失误带来的损失比存储费用高几个数量级。3. 维度建模和范式建模为什么数据工程师最终都倒向维度建模选数据建模方法论是很多刚接触数仓的人第一个纠结的问题。学校数据库原理课教的是三范式3NF要尽量减少冗余消除传递依赖。结果工作后一看公司的数仓根本不遵循范式事实表、维度表满天飞还能看到大量冗余字段。这里面的原因值得展开讲清楚。3.1 范式建模的适用边界OLTP的救星OLAP的灾难范式建模核心是“降低冗余、保证一致性”。在OLTP在线事务处理系统中每一次订单插入都要更新库存、扣减账户余额如果数据冗余就可能导致同一份数据在多处不一致。所以事务库要做范式化这是为了保证写入的一致性。但到了数仓场景你的核心负载变成了OLAP在线分析处理。读多写少查询动辄扫描几亿行分析维度和度量经常跨多张表聚合。这时候如果还死守3NF会导致两个现实问题查询性能差。一次分析要JOIN几十张规范化的小表MapReduce或Spark在处理这些Shuffle时代价极高响应时间根本扛不住业务方的“我要实时看数”。业务不可理解。范式模型的ER图有几百张表产品经理看一眼就头大更别说自己写SQL去做探索式分析。我并不是说范式建模一无是处它在数据仓库架构中也有一席之地特别是作为企业级数据模型的标准视图。但从实际交付角度看绝大多数公司的数仓主体是用维度建模搭起来的因为分析场景的响应速度和易用性胜过了理论上的规范性。3.2 维度建模为什么“香”星型模型和雪花模型怎么选维度建模Dimensional Modeling由数据仓库大师Ralph Kimball系统化提出。它的核心思想很直白用“事实表维度表”的框架来组织数据把业务过程拆成可度量的“事实”每个事实用一组“维度”来描述。事实表里放的是度量值比如金额、件数、次数。这些度量有一个重要属性叫“可加性”。金额是完全可以跨任何维度相加的但有些指标做不了跨维度相加比如“下单用户数”如果把两个用户ID相加就是无意义操作。所以你需要给每个度量打标签完全可加、半可加可以按时间维度叠加但不能跨某些维度、不可加。理清这点是做汇总表设计时的底层逻辑。维度表里放的是描述性属性比如用户的性别、年龄、注册渠道商品的类目层级、品牌门店的城市、商圈。维度表通常是“胖”的也就是冗余度较高。这跟范式建模的理念相反但为了减少JOIN、提升查询效率这个牺牲值得。在具体表结构形态上有两种流派星型模型事实表在中间维度表在四周每张维度表只和事实表关联一层。特点结构简单清晰查询性能好是数仓里的绝大多数。雪花模型某些维度表继续规范化拆成二级维度表。比如商品维度表关联到类目表类目表再关联到一级类目表。特点减少了冗余但查询需要多级JOIN。我自己的实践经验是数仓建模几乎无脑选星型。因为以现在的存储成本和计算引擎能力冗余几个维度的压力根本不算什么而查询减少JOIN层数带来的性能收益是肉眼可见的。雪花模型让ER图好看但让查询变慢也让下游的分析师更容易写错SQL。3.3 Kimball和Inmon以及很少用但需要知道的Data Vault方法论如果往更上层看还有一个绕不开的对比Kimball和Inmon。Inmon主张“自上而下”先建整个企业范围的标准化的数据仓库通常用范式建模再从这个仓库派生出各个数据集市。这种模式适合非常成熟的大企业有专门的企业数据架构团队有足够长的建设周期。Kimball主张“自下而上”直接按业务过程构建维度模型每个数据集市就是一个业务过程或一个主题的分析基础多个数据集市通过“一致性维度”联系起来。现实是绝大多数公司的数据团队是拿Kimball的思路起步的。先做交易域再做用户域、商品域通过总线矩阵保证各域之间维度一致。这样能快速见效第一个数据模型两到三周就能上线业务方马上能看到数据分析价值。对一个KPI压力很大的公司来说这是最实际的选择。Data Vault建模是这些年小圈子里比较流行的一种方法它把模型拆成Hub中心表、Link关联表、Satellite卫星表非常适合数据来源多、近源数据实时性要求高、历史追踪要求严格的场景。但它的模型很碎查询时需要大量JOIN不太适合直接用为报表服务。我的意见是你要知道有这样一个流派面试问到能说清楚它的基本思想即可。真正在业务数仓里大面积铺开Data Vault的我几乎没有见过。方法论没有绝对的“最正确”只有“最合适”。对数据开发工程师而言在一家公司从零搭数仓最顺手的组合是以维度建模为中心用Kimball的增量迭代方式推进ODS和DWD层处理好复杂度DWS/ADS层面向业务直接产出这就是我在实操中最常用也推荐你首选的路径。4. 从订单主题出发完整走一遍维度建模实操演练这节我来完整演示一次维度建模的落地过程。选订单域是因为订单几乎是所有电商公司最核心的业务过程而且它牵涉到用户、商品、商家、店铺、日期等多个维度信息量丰富很适合用来展示建模思路。4.1 第一步定业务过程和粒度订单域里有多个业务过程提交订单、支付订单、发货、确认收货、申请退款。不是一个业务过程建一张表就完了而是要根据分析需求决定建模范围。这里建议把“下单”“支付”“退款”三个过程分开建模而不是把三个过程塞进同一张“订单流水大宽表”。原因很简单三个过程的事实度量不同粒度和敏感字段不同。下单关心的是订单数、下单金额、商品件数支付关心的是支付成功金额、支付时长退款关心的是售中退款、售后退款金额。混在一起字段爆炸可读性下降ETL也会变得极难维护。有的订单表你看到一张表里出现了order_amount、pay_amount、refund_amount三组金额同时还有几十个冗余状态字段就是因为建模时没想清楚粒度问题。正确做法是拆开。确定好业务过程后接下来要给事实表声明粒度。粒度就是“一行代表什么”。订单明细事实表一行是“一个订单中的一个商品子项”订单支付事实表一行是“一个订单的一次支付流水”退款事实表一行是“一个订单商品项的一次退款申请”粒度声明是事实表建模的锚点。只要粒度确定后续所有指标都必须能落户到这个粒度上。比如你在“明细粒度一个订单一个商品子项”的事实表上要算“用户订单数”那就必须先去重订单ID再计数否则就会把同一订单里的两个商品项算成两单。这种错误在模型设计期容易埋下到写SQL时就表现为COUNT和COUNT(DISTINCT)结果的巨大差异。4.2 第二步事实表字段设计度量类型先分清楚确定粒度之后开始列事实表的度量字段。订单明细事实表的度量字段大概是这些order_id订单ID维度外键的组成部分user_id用户IDsku_idSKU粒度商品IDspu_idSPU粒度商品IDshop_id店铺IDorder_amount下单金额可加item_count商品件数可加actual_pay_amount实际支付金额可加coupon_amount优惠券抵扣金额可加order_status订单状态不可加通常放在DWD层而非DWS字段里可以加一些“退化维度”。比如order_source订单来源H5/App/小程序、pay_type支付方式微信/支付宝/银行卡这些其实是维度属性但为了减少JOIN直接把它们退化进事实表。这里注意退化维度要选择取值有限、相对稳定、维度表本身很薄的那些属性比如渠道、支付方式、订单类型。像“用户所在城市”这种属性就绝不该退化进订单事实表因为它的值可以通过用户ID关联并且用户维度会在很多分析场景里被反复使用一旦进了订单表反而造成不一致。度量可加性这里再强调一个坑订单金额按时间维度有可加性但是“优惠券抵扣金额”如果以“领取优惠券”为业务过程去分析时就不具备跨时间可加性了因为一张券可能在生命周期里被多次使用。定义口径时就要把“券分摊金额”和“券抵扣金额”分开建模。我见过因为混合这两个口径导致退款报表里“优惠券金额对不上”的典型事故排查起来极其痛苦。4.3 第三步维度表设计至少把日期、用户、商品这三张做好维度建模的核心除了事实表就是维度表。一个订单事实表至少要关联这些维度日期维度、用户维度、商品维度、店铺维度。这里我把这四张维度表的设计要点都过一遍。日期维度。日期维度是数仓里最常被忽略但最需要精心设计的表。不要直接让下游用order_time去关联事实表而是建一张dim_date表一行代表一天包含date_id日期格式yyyy-MM-dd主键day_of_week星期几week_begin_date本周开始日期month_id所属月份quarter_id所属季度is_workday是否工作日is_holiday是否节假日为什么这么设计因为在分析场景里你需要做“今年中秋和去年中秋对比”“第X周环比第X-1周”这类分析如果没有一张标准的日期维度表你就需要在SQL里写一长串日期计算逻辑每张报表写一遍非常痛苦。有了它所有日历语义统一。用户维度。用户维度表一行代表一个用户要包含用户的静态属性和分区属性。静态属性用户ID、性别、注册时间、注册渠道、会员等级分区属性年龄区间、城市等级、新老客状态。这里要注意“新老客状态”这类属性它其实是会随时间变化的比如一个用户在1月是“新客”但他到了6月就是“老客”了。如果你把它当着静态字段存在一行用户维表里就会出现历史回溯不准确的问题。这个问题的标准解法叫“多变维度的处理策略”。常见策略有三种直接覆盖不保留历史最简单的做法适合变化频率低或不需要追溯的口径。拉链表保留历史全量记录每条记录的有效起始时间和结束时间。快照表每天保留当时维度的全量快照适合需要精确还原历史的场景。在实际数仓场景中用户维度我会拉链表和快照结合在主用户维表dim_user里只放不变的属性或可覆盖的属性然后保留一张用户历史拉链表用于回溯分析。商品维度类似类目归属很容易变化类目调整是电商常有的事如果不加处理历史订单会全部归到新类目下导致同比分析失真。所有维度的缓慢变化处理都需要在建模设计期业务确认好哪些维度属性变了历史要跟着变哪些要保留历史口径。商品维度。商品维度通常比较“深”。一个SKU会关联到品牌、类目层级、SPU、商家。类目层级要处理好一级类目、二级类目、三级类目。我建商品维度时会直接冗余出多个字段category_1_name、category_2_name、category_3_name这样下游要按一级类目分组时直接GROUP BY一个字段就行不用去递归JOIN类目表。店铺维度。相对简单包含店铺ID、店铺名称、开店时间、店铺等级、主营类目。注意店铺维度和商家维度是两个概念一家公司商家可以拥有多个店铺分析营销活动时会按商家维度看而看流量转化时会按店铺维度。粒度不要混。维度表最重要的设计原则是一致性同一个“用户ID”在任何事实表里只能关联同一张用户维度表同一个“商品ID”同理。这是总线矩阵的落地。如果订单域里用dim_user_basic关联用户而营销域里又用了另一张dim_member_info两边对“新客”的定义不同那这两个域的表永远无法拉通分析。4.4 第四步一张DWS宽表是怎么从DWD加工出来的事实表、维度表建模完成相当于原料备好了但业务方看数不会直接查DWD和维表的JOIN结果那样查询效率低、SQL门槛高。我们要在DWS层做主题汇总。以“用户订单主题”为例。我们要建一张dws_user_order_1d表一行代表一个用户某一天的订单汇总。字段包括user_iddate_idorder_count下单次数去重订单IDorder_sku_count下单商品件数order_amount下单金额pay_count支付次数pay_amount支付金额refund_amount退款金额这张表的加工SQL核心思路是INSERT OVERWRITE TABLE dws_user_order_1d SELECT user_id, TO_CHAR(order_time, yyyy-MM-dd) AS date_id, COUNT(DISTINCT order_id) AS order_count, SUM(item_count) AS order_sku_count, SUM(order_amount) AS order_amount, SUM(CASE WHEN pay_time IS NOT NULL THEN 1 ELSE 0 END) AS pay_count, SUM(actual_pay_amount) AS pay_amount, SUM(refund_amount) AS refund_amount FROM dwd_trade_order_detail_di GROUP BY user_id, TO_CHAR(order_time, yyyy-MM-dd);有了这张1日汇总表再要“近30天用户下单金额”就很容易了SELECT user_id, SUM(order_amount) AS order_amount_30d FROM dws_user_order_1d WHERE date_id DATE_SUB(CURRENT_DATE, 30) GROUP BY user_id;如果当时没有做DWS汇总而是每次从DWD里取30天全量明细聚合同样的查询可能要扫几十亿行。DWS的价值就是把高成本的预计算提前完成让下游每一次查询都轻量。4.5 事务事实表和周期快照事实表订单事实是典型的“事务事实表”每个事务一行当事件发生时记录一行之后不更新。事务事实表用来统计“发生了什么”比如支付了多少笔、金额多少。但很多分析场景还需要回答“截至某个时刻的累计状态”。比如“今天有多少人持有有效优惠券”“当前库存还有多少”。这类瞬时状态型的指标事务事实表做不了。那就需要周期快照事实表按固定时间间隔每天/每小时记录当时的完整状态。一行代表“某用户截至今天累计消费金额”“某商品今天的库存快照”。订单域里也要用快照用户累计消费金额表、累计下单次数表。这类表的价值在于计算会员等级、用户分层的RFM模型时不用再从DWD做全量历史累计。事务事实表和周期快照事实表不要混用我看到有人把“累计到昨天的金额”加到当天事务记录后面这样把累计逻辑和新增逻辑放在同一行导致回溯又重新计算时结果不一致。正确做法是事务表管过程快照表管状态两者分开建模要做“存量增量”时再去JOIN。5. 宁可慢一点也要避开的建模坑字段口径、粒度、NULL和命名前面讲的是建模“正向设计”的路径。这一节我想说一些更细的坑。这些坑几乎每个数据团队都踩过但绝大多数不会写进官方文档、更不会出现在教科书里。5.1 同一个字段两张表里两种含义这是最隐蔽的坑我曾接手过一个项目用户域里有个字段叫is_new_user在A表里定义为“该用户是否为当天首次启动App”在B表里定义为“该用户是否为近30天首次下单”。两张表都叫is_new_user结果数据产品在同一个看板里同时选了这两个字段做交叉分析算出来“新用户居然有90%都下单了而且很多老客也被算成新客”差点做出错误运营决策。这个问题就叫“同名字段不同口径”。根治办法有两个层次建模层字段命名要带业务限定。比如is_new_start_user是否新启动用户、is_new_purchase_user是否新成交用户不要用模糊的is_new_user。口径层建立公共指标字典每个指标有唯一编码在数仓元数据里挂上口径说明。指标一旦确定所有系统都使用统一指标标识字段命名保持一致。这两个办法我都踩过才明白第二个比第一个更重要。因为只要指标字典清晰即使字段名有歧义也能通过元数据找回真实含义。反之指标字典混乱字段名再规范也治标不治本。5.2 粒度问题不要把明细和汇总混在同一张表我见过最让人揪心的表结构是一张“订单主题宽表”里面既有订单明细级别字段order_id、sku_id又有用户汇总级别字段user_30d_order_amount、user_total_amount。这张表的设计者本意是“宽表一张表搞定所有需求”下游查询时也方便不用JOIN。但灾难在于如果你按SKU维度聚合时user_30d_order_amount会被重复累加多次。例如一个用户买了5个SKU的订单这个字段就在行里重复出现5次SUM时被加5遍产生错误的汇总。一旦有下游把这张表当成聚合表用数据就会错得一塌糊涂而且很难查出来。建模时必须保证一个表只有一个粒度明细就是明细汇总就是汇总。如果你真的需要一个“既有明细又有汇总的报表查询”那请把汇总指标拆到另一个ADS层表里在查询端通过子查询去关联而不是物理上合并成一张宽表。任何需要“同时看多个粒度”的分析都应当是通过多张正确粒度的表来组合完成。5.3 日期字段的类型、时区和NULL三个细节点日期字段的坑每天都在发生而且很烦人类型不统一有的表order_time是string类型存的是2024-05-20 10:30:00有的表是timestamp类型。用来做过滤时字符串比较和日期函数转换来回切换极容易出错。我强烈建议数仓公共层的所有业务时间字段统一为timestamp类型所有分区字段统一为string类型且格式固定为yyyy-MM-dd。时区不统一业务库可能记录的是北京时间埋点日志可能记录的是UTC时间。如果建模时不统一转成北京时间统计出来的“当天订单量”就会出现偏差尤其是凌晨零点到八点这两个时段之间差异明显。建模时就应该在明细DWD层把所有时间字段统一转换为业务标准时区后面一切基于该标准时间。时间字段为NULL很多订单有“下单时间”但没“支付时间”因为订单最终未支付。下游在计算“支付时长支付时间-下单时间”时就会碰到大量NULL。处理NULL的策略要在建模期就定清楚是保留NULL让下游通过COALESCE处理还是直接置为1970-01-01这种祭天值我推荐保留NULL并且用字段注释写清楚“该字段在下单未支付时为空”。用祭天值会污染计算而且很难分辨是“真的没有”还是“值为1970”。5.4 金额字段相关为什么建模里头一定要用“分”或精度可控的小数关于金额字段很多从业务库同步过来的数据是DECIMAL(10, 2)表示到“分”。进入数仓后有人图省事直接转成DOUBLE这是个大坑。浮点数在计算机中不是精确存储的当你把0.1和0.2相加时可能得到0.30000000000000004。金额如果用浮点数做累加累计到千万级别时会产生无法忽略的误差对账时财务那边一毛钱都不放过你会被“金额差一分”折磨到怀疑人生。正确做法是金额字段尽量保持DECIMAL类型如果必须转DOUBLE至少保留足够精度并且禁止在浮点数上直接做等值比较。如果需要用整数计算建议金额统一转换成分整数也就是乘以100后存储为BIGINT。这样既避免浮点误差SUM也快速准确。下游需要展示元时再除以100。顺带说一句Hive/Spark里对DECIMAL的SUM结果类型转换规则在不同版本间有细微差异最好在开发自测时就拿一个千万级数据量样本做“金额总和”测试避免上线后发现精度异常。5.5 主键唯一性校验写进调度里而不是靠自觉数据建模的上线只是开始。模型要持续可靠必须有“关卡”守护。我负责的DWD层所有事实表都配置了主键唯一性检查SELECT order_id, COUNT(*) AS cnt FROM dwd_trade_order_detail_di WHERE data_date ${bizdate} GROUP BY order_id HAVING COUNT(*) 1;如果这个查询返回了结果调度系统就立刻阻断下游任务并发出告警。有了这张“安全网”模型里的主键重复问题就能第一时间暴露而不是等报表数据发出去几天后由业务方发现总数对不上才回头排查。6. 模型上线只是一个开始血缘、资产盘点和公共模型优化很多开发者的习惯是模型调通了、报表能看到数了就认为建模工作完事了。其实上线才是最需要盯的时候因为真实运行中你才会发现模型被用得对不对、怎样被用得不好。6.1 数据血缘模型出问题时靠它快速定位数仓模型一张接一张层与层之间通过ETL任务串联。当某张报表出现一个异常值时你必须能顺着链路倒回去查ADS这张表的这个字段是从DWS的哪个字段算来的那张DWS表的这个字段又是从DWD哪几张表聚合来的没有数据血缘你只能在全公司数千张表里大海捞针。有血缘你可以几分钟内定位“源头在哪一层、哪张表、哪个字段、哪一段SQL逻辑”。现在主流的调度平台、元数据平台都支持自动解析SQL血缘建议在建仓初期就把依赖关系收集起来。我现在维护的每个模型都要求在元数据平台里能看到完整血缘链路。当模型做了字段调整我还可以通过血缘自动找到所有下游批量通知他们避免悄无声息地在某个报表里埋雷。6.2 资产盘点别让小众临时查询绑架了DWS/ADS模型建数仓一年以后你会有几百张DWS和ADS宽表。这里最大的风险是模型膨胀——“每来一个需求建一张表”一年多后表数量暴增但真正被高频查询的表没几张。我的经验是每季度做一次“模型资产盘点”。从元数据平台拉出每张表的查询频率、读取行数、产出耗时、对应下游任务数量列一个清单出来。按“高频高价值”“低频高价值”“高频无价值”“低频无价值”四象限分类。对“低频无价值”的表可以直接下线或者合并对“高频无价值”的表要看看它查的内容是不是DWS或公共层缺失某些核心字段如果是就去补公共模型而不是忍着一张笨重的个性化宽表继续跑。这个工作我称它为“数据模型的还债”因为前期的设计缺失总有一天要用维护成本来偿还越早还利息越低。6.3 公共层模型的横向合并与纵向拆分公共层模型优化的两个方向我分享下实操中的经验。第一横向合并。当多个DWS表都包含“用户ID日期支付金额”这种相同粒度相同业务过程的组合可以考虑合并成一张宽表这样下游只需要查一张表跨指标计算就不用JOIN多张DWS。常见做法是把用户域的下单、支付、退款、加购、收藏指标合并到一张“用户交易汇总宽表”里。SELECT user_id, date_id, SUM(order_amount) AS order_amount, SUM(pay_amount) AS pay_amount, SUM(refund_amount) AS refund_amount, SUM(fav_count) AS fav_count FROM ( -- 这里是DWD各明细表的联合 ) t GROUP BY user_id, date_id;合并的前提是粒度一致、口径一致。粒度都是“用户-天”口径都是经过指标字典确认的才可以合并。反过来说如果仅因分析师个人喜好把粒度不一致的指标硬塞一起那就会重新掉进5.2讲的坑。第二纵向拆分。当一张DWS表的字段超过80个甚至100个下游查询却经常只看其中少数列时可以考虑拆成“用户核心交易表”“用户流量行为表”“用户营销触达表”等子集避免每次查询都要读取一堆用不上的列减少I/O和扫描成本。当然纵向拆分也不是拆得越碎越好拆太碎会导致下游跨表JOIN太多查询反而变慢。一般以50到80个字段为一个主题的健康区间超过或者字段间主题关系太强时就不拆。6.4 任务稳定性和数据产出的“最后一道防线”模型优化调整之后必须有回归验证。我一般保留一批“数据质量基线SQL”比如某个汇总表里的核心指标必须等于另一张独立来源表的对应统计值。模型改动之后自动跑一遍基线只要基线通过才允许合入生产。真正的交付不是“我建好了表”而是“表里的数据每天稳定产出、口径清晰、下游用得放心”。数据建模这份工作很多功夫在SQL之外但最终又都会体现在数据质量和模型效率上。这也是整个大数据从业链路里最有成就感的部分你建设的不只是一张张表而是整个团队做数据决策时最值得依赖的路基。那些在业务方开月会时不用再为一个数字的出入吵来吵去的安稳时刻就是数据建模工作最好的回报。
返回列表