最近在团队内部整理数仓建设的一整套实践,很多同事问我:公司上了 Hadoop、上了 Flink、甚至把 ClickHouse 都部署好了,为什么取数还这么费劲?报表口径为什么还是对不上?业务方管我们要一个“GMV”,结果三个报表出三个数,还都觉得自己没错。这种问题,不是靠加机器、换引擎能解决的,根子在于数据仓库的地基没有打牢。数据仓库不是一套工具,而是一整套面向分析场景的工程体系,建模方法、分层架构、口径管理、数据质量,一层套一层,哪个环节偷懒,后面都会成倍还回来。
我准备写一个系列,围绕数据仓库建设过程中的底层思考展开,从建模方法论、分层架构、数据质量,到实际排障经验,一篇一篇聊透。这是第一篇,先把思路和地基讲清楚。这一篇适合刚入行的数据开发、天天被业务方催数的数据分析师,以及准备从零搭数仓的架构师阅读。看完你会明白:数仓真正的难点,从来都不是技术,而是选择、权衡,以及那些踩过才知道的坑。
1. 一个看似绕不开的好问题:我们到底为什么需要数据仓库
1.1 直接查业务库,十个团队有九个会翻车
很多人在刚接触数仓时会有一个朴素的想法:数据都存在数据库里,业务方想看什么,直接写 SQL 查生产库不就行了?还费劲搭一套数仓做什么?这个想法听起来简单直接,但在真实场景里几乎走不通,原因不在 SQL 写得好不好,而在业务库和数据仓库的定位根本不一样。
业务系统用的数据库是 OLTP,它是给流程用的,订单录入、库存扣减、用户注册,每个操作都要求短平快,事务要一致,锁要管好。为了达到这个目的,业务库通常按照三范式设计或者做适度反范式,表拆得细,数据冗余少,但对分析极不友好。你想统计一个“近 30 天复购用户数”,可能要根据用户表、订单表、支付表、商品表、优惠券表 join 一圈,每条 SQL 都是在跟数据库的性能极限博弈。
更重要的是,业务库里的数据是“当前态”,不是“历史态”。订单状态从“待支付”变成“已支付”,再变成“已发货”,业务库里订单表的那一行只有一个最新状态,中间过程被直接覆盖了。可分析恰恰需要看过程,想分析转化率就得知道每个环节停留多久,想分析流失就得知道用户最后活跃时间,这些历史轨迹在业务库里往往拿不到。还有更头疼的:多个业务系统的数据标准不一致,比如用户 ID 在订单系统里是字符串,在会员系统里是数字,在日志系统里又带了前缀,直接在源系统做分析需要大量清洗,而清洗逻辑放哪、谁维护,又会成为新的扯皮点。
1.2 数据仓库的本质:面向主题、集成、时变、非易失
数据仓库解决的就是上面这堆别扭。经典教材把数仓的特点概括为四个词:面向主题、集成、时变、非易失。这四个词听起来像考试重点,但放到真实场景里特别好理解。
面向主题,意思是数仓里的数据围绕业务过程组织,比如订单、用户、商品、流量,而不是围绕某个系统的功能表。集成指的是把多个来源的数据统一成一套口径,码表对齐、单位统一、字段类型拉齐,形成“标准件”。时变意味着留历史,数仓里每个事实都有时间维度,今天看昨天的快照、看一个月前的状态,都是常规操作。非易失则是指数据进了数仓之后不会因为业务系统的增删改而随意变化,写入就是沉淀,保证分析口径稳定。
可以打个比方:业务库是菜市场买回来的整颗菜,新鲜但带泥、带根、带烂叶子,而数仓是后厨操作间里的净菜,按菜系需求洗好、切好、分好,炒菜师傅拿过去就能下锅。BI 系统是那个炒菜师傅,它只负责把配好的料快速变成菜端上桌,但如果后厨根本没有净菜供应,师傅再快也没用。这也是为什么很多团队买了顶级 BI 工具,照样被取数需求活活拖死,因为问题不在炒菜,而在备菜。
1.3 数仓不是工具,是一套“面向分析的工程体系”
这些年跟不少团队聊过数仓建设,有个很常见的误区:以为上了某大厂的分布式计算平台、买了一台高性能分析型数据库、部署了一堆同步工具,就算建了数仓。真不是。这些只是“食材运输车”和“厨房电器”,并不是厨房本身。数仓是包括数据模型设计、ETL 流程、调度依赖、血缘关系、指标字典、数据质量规则、权限治理在内的一整套工程体系。
换句话说,数仓的核心资产不是程序,而是“模型”和“口径”。模型决定了数据怎么组织、怎么被查询,口径决定了业务方拿到的每个数字是什么意思。技术选型反而相对容易,今天选 A 引擎,明天换 B 引擎,都有成熟迁移路径;但口径乱了、模型烂了,想收拾就得推倒重来。我在实际工作中看过的数仓项目,凡是烂尾或者沦为“取数工具”的,基本都不是因为技术选型失败,而是从一开始就没有把数仓当成体系来设计。这也是这个系列第一篇最想强调的观点:先想清楚为什么建、解决什么问题,再动手建分层和模型。
2. 数仓建模方法论,怎么选才不会翻车
2.1 四大建模流派,先认清各自的家底
数仓建模的方法论在业内演进这么多年,主流的就那几套:范式建模、维度建模、Data Vault 建模、Anchor 建模。很多人一听名字就头大,其实可以这样简单理解:它们之间的差异,核心是“冗余和灵活、查询性能和开发成本”怎么取舍。
范式建模,也就是第三范式建模,通常用在企业级 ER 模型设计里。它强调消除冗余,一个字段只存一份,表结构高度规范化,好处是数据一致性极强,坏处是查询时表连接爆炸,一个报表动不动 join 十几张表,性能压力大,开发周期被拉得很长。这类模型通常适合数据仓库底层的整合层,不适合直接面向分析。
维度建模则是当今数仓界的主流,Kimball 那一套,核心就两个东西:事实表和维度表。事实表记录业务过程的度量值,比如订单金额、下单数量;维度表描述看问题的角度,比如时间、用户、商品。它天生面向分析,查询时可以直接沿着维度钻取、切片。
Data Vault 建模则是一种更重型的方案,分为 Hub(中心)、Link(连接)、Satellite(卫星表),强调可追溯性和扩展性,适合多源系统集成复杂、审计要求高的大型企业。代价是模型复杂,普通团队没有专业工具支撑很难维护。
Anchor 建模是另一个极端,用极度规范化的方式支持灵活变化,加一个字段不需要改表结构,但查询时往往要关联几十张表,分析场景基本没法用。这类方法在现实项目里落地极少,更多停留在研究层面。
2.2 大多数人请直接拥抱维度建模
如果团队不是特别大型或者没有强制合规要求,我一般建议直接选用维度建模起步,因为它最贴近业务直觉。业务方问“华东区上个月卖了多少件商品”,这句话里“华东区”是维度(地区),“上个月”也是维度(时间),“卖了多少件”是事实。维度建模天生就是按照这种业务思维来组织数据的,业务方理解容易,开发人员建模也不绕弯。
维度建模里最核心的概念是“粒度”。建任何事实表之前,必须先把粒度定死:这张表的一行代表什么?是一笔订单?一个订单里的一个子商品?一次支付流水?还是一次点击日志?粒度定不下来,后面所有指标都可能是错的。我见过一个真实案例,产品提需求说要分析“不同品类下的用户复购率”,开发直接拿订单表聚合,结果一张订单里有多个品类时,用户被重复计数,复购率虚高一倍。这就是粒度没定义清楚导致的事故。
定好粒度之后,事实表自然分为事务事实表、周期快照事实表和累积快照事实表。事务事实表记录每一个业务事件,适合分析过程;周期快照事实表按天、周记录业务状态,比如库存余额、账户余额,适合分析状态变化;累积快照事实表则记录一个业务流程从开始到结束的多个节点时间,适合分析耗时,比如“从下单到发货平均多久”。三种表各有适用场景,但二和三是给一类场景的补充,最核心的主力永远是事务事实表。
很多人还会忽略“退化维度”这个概念。订单明细表里通常会有订单号、用户 ID、店铺 ID 这些字段,它们同时承担了事实和维度的角色,当然可以创建成独立的维度表再去关联,但实际上把这些字段直接冗余在事实表里反而是更高效的做法。因为这样能减少 join,查询更快,而且订单号这种字段你本来也不可能单独建一张上千行一条记录的维度表。把这个设计用好,宽表的雏形就出来了。
2.3 星型模型、雪花模型,以及宽表的流行
维度建模的表结构呈现出来,有两种常见形态:星型模型和雪花模型。星型模型是事实表在中间,周边一圈维度表直接关联,结构简单,查询效率高;雪花模型则是维度表继续规范化,比如地区维度再拆出省份表、城市表,结构更像雪花分层展开。雪花模型节省存储冗余,但查询要多一层 join,在分布式引擎下还有 shuffle 开销,所以大多数数仓实践都偏向星型模型。
不过这几年国内数仓里最流行的其实是“宽表”方案,这也是 StarRocks、Doris、ClickHouse 这些引擎火起来之后大家越来越爱用的原因。宽表的做法是把常用维度字段直接冗余进事实表或者汇总表,一张表里既有度量值又有维度的描述属性,查询基本就是扫一张表,不需要 join。宽表不是简单否定维度建模,而是在维度建模的基础上,把高频使用的维度“反规范化”进事实表,用空间换性能。但宽表也不是越宽越好,字段过多会导致表膨胀、同步链路变慢,而且修改成本高,所以实际设计时通常只冗余查询频率最高的 10 到 20 个维度字段,低频的仍然走维度表关联。
3. 数仓分层架构:每一层到底在解决什么问题
3.1 常规数仓分层的职责分工
聊完建模方法论,自然就要聊分层。数仓分层不是拍脑袋定出来的规范,而是为了把“从原始数据到业务指标”这一长串加工过程拆成分工明确的流水线。业界最通用的分层是四层:ODS、DWD、DWS、ADS,不同公司可能有五层、六层,但思路是共通的。
| 分层 | 中文名 | 主要职责 | 典型操作 |
|---|---|---|---|
| ODS | 贴源层 | 原样接入源系统数据,保留历史留底 | 增量同步、全量同步、拉链表快照 |
| DWD | 明细层 | 清洗、去重、标准化、维度退化,形成明细事实 | 数据清洗、粒度定义、SCD 处理 |
| DWS | 汇总层 | 按主题做宽表和预聚合,供报表高速查询 | 主题宽表、汇总表、指标加工 |
| ADS | 应用层 | 面向具体应用场景提供个性化数据 | 报表数据集、接口、个性化指标 |
ODS 层的第一原则是“原样接入”。不要在这层做太多清洗逻辑,因为它是数仓的保险柜,源系统一旦重刷历史数据,ODS 还能给出原始凭证。表结构最好跟源系统保持一致,该有的字段一个不少,分区按业务日期划分,这样后面 DWD 层无论怎么加工,回到 ODS 永远能找到最初的事实。拉链表是 ODS 层一个非常实用的技术,它能在不复制全量数据的前提下保留每一行记录的生命周期,适用于大量只更新部分状态的维度数据。
DWD 层是整个数仓建模最见功力的地方,也是大多数数据开发日常工作的主战场。这里要把 ODS 层那些“带泥的菜”洗干净,去重、修正错误格式、统一单位、过滤异常值,同时做维度退化,把高频维度字段冗余进事实表。更重要的是,DWD 层要开始落实一致性维度,同一个用户在订单表里叫“user_id”,在注册日志里叫“uid”,在这里必须统一成一个字段。这个工作不做,后面所有指标都可能打架。
DWS 层是主题宽表层,它围绕业务主题(用户、商品、机构、渠道)做汇总加工,把 DWD 层明细数据按照常用维度组合预聚合起来。这层是报表和即席查询的主要数据来源,所以性能要求最高。ADS 层则更个性化,不同应用、不同 BU 可以按自己的需要把 DWS 层数据再加工,甚至可以输出到不同的存储系统里,比如实时报表用 Redis,大屏用 ClickHouse。到 ADS 层,统一规范要适当放松,因为它的核心目标是快速支撑场景需求。
3.2 为什么一定要分层:成本、治理、稳定性
每次聊到分层都会有人问:我数据量也不大,四五张表,建数仓就为了给老板一张报表,分层不是多此一举吗?这种质疑有道理,但绝大多数场景下,分层带来的好处是长期显性的。
首先是成本和性能的平衡。没有 DWS 层,十张报表都要从 DWD 明细跑聚合,每张报表重复计算一遍,集群资源被白白消耗。有了 DWS 层,预先按主题算好汇总,报表查询变成“查表”而不是“跑数”,响应快、资源省。其次是治理需求,数仓面向全公司服务,不同数据的权限不一样,用户维度、订单维度可能敏感,日志维度相对宽松,分层可以让敏感数据留在内部层,差异化授权。最后是稳定性,上游系统表结构变更、数据异常,如果所有报表都直接贴在 ODS 上,一次变动引发全链路报表挂掉;分层相当于加了缓冲区,DWD 的清洗逻辑可以挡住一部分脏数据,DWS 的重跑只影响局部。
当然,分层也要克制,不是层数越多越好。我见过某团队搞了整整七层,每层只做了一个小转换,最下游的报表数据链路延迟高、上游重刷频繁,数据出问题找半天定位不到层。正确的做法是,分层以解决问题为目标,通常四层足够,特殊情况再补中间层。
3.3 模拟项目复盘:零售订单主题如何从 ODS 一路走到 ADS
拿一个零售公司的模拟项目 X 举个例子。这个项目的源系统是一套订单系统、一套支付系统、一套商品中心,外加前端埋点日志。业务方最关心的指标是 GMV,但问题很快来了:同一个 GMV,订单系统取“下单金额”,支付系统取“成功支付金额”,供应链那帮人取“核销金额”,三套系统三种口径,连定义都没对齐。
我们在 ODS 层先把三套系统的原始表原样同步过来,订单表就是订单表,支付流水就是支付流水,不做任何加工,方便后续追溯。接着在 DWD 层做三件事:第一,统一字段标准,把三套系统的用户标识全部映射成同一个 user_id;第二,清洗异常数据,比如订单金额为负的测试单、重复提交的支付流水,按规则过滤或打标;第三,定义订单明细事实表,粒度定为“订单+子商品”,一个订单买三件商品就是三行。
关键的一步是跟业务方对齐口径:GMV 到底取哪个事件的金额?我们最终确认以“支付成功且未退款”的支付流水作为 GMV 口径,因为“下单金额”有大量未支付的虚单,“核销金额”又太滞后,都不适合作为增长决策的核心指标。口径定了之后,DWS 层围绕“用户-商品-日”维度做汇总,产出两张主题宽表:用户日汇总表、商品日汇总表。ADS 层再按时间切分生成 GMV 日报、品类销售榜、复购分析数据集。
这个案例最核心的经验是:数据开发不能闷头写 SQL,第一步必须跟业务方把口径对齐,口径对齐了,模型才有意义。技术层面反而简单,无非是选择事务事实表还是周期快照表的问题。订单业务天然适合事务事实表,库存余额则适合周期快照表,这个判断在建模初期就要定下来。
4. 数仓开发和运维中的高频问题,一个老开发的血泪排查记录
4.1 口径打架:同一个指标三个数,到底听谁的
口径不统一是数仓项目里最扎心的痛点。同一个“活跃用户数”,有人取的是当天登录过 APP 的用户,有人取的是当天打开过任意页面的用户,有人把 PC 端和移动端重复计算,结果同一个指标三个数,业务方开会拍桌子。
解决这个问题只有一个笨办法:建立指标字典,把所有核心指标的定义、计算逻辑、来源表、取数时间、维度字段全部标准化。我有一次跟团队处理一个异常,发现用户表里的“注册时间”字段格式有的是字符串、有的是日期类型,解析出来差了 8 个小时。指标字典里如果没有明确这个字段的格式标准,这类问题会反复出现。此外,指标命名也要统一,别出现一个叫“累计消费金额”、另一个叫“总消费额”的字段指向同一个逻辑。
4.2 数据倾斜实战:一张事实表卡了一天,怎么办
数仓跑批任务最怕的就是数据倾斜。核销表中某个大客户的订单量占了全表的 30%,Jion 时这个 key 所在的 Reducer 要处理的数据量远大于其他 Reducer,任务卡在那里,其他节点在空转。这类问题在按用户维度聚合时尤其常见,头部 users 的贡献总是远大于长尾用户。
常规解决思路有三条:加盐、先聚合再关联、用 MapJoin。加盐的思路是对热点 key 做一些打散处理,比如把用户 ID 加上随机后缀拆成多个子 key,再二次聚合;先聚合再关联适用于关联后还要聚合的场景,先把小表或者子查询的结果按主键聚合好,再跟大表关联;MapJoin 则适合大表 join 小表的场景,把小表分发到每个 Map 端,避免 Reduce 阶段的 shuffle。实际处理时,需要根据数据分布选择最优方案,我一般先跑一个简单的 group by 看数据分布,再决定加盐粒度。
4.3 任务依赖与空跑的坑:上游一重刷,下游全链路崩
数仓的跑批任务有严格的依赖关系,但依赖关系如果只靠调度时间而不是数据就绪状态来控制,问题会非常大。典型场景是:上游任务因为某个原因跑完但产出的是空表或部分分区数据,下游任务检测到文件存在就开始消费,结果报表输出一个明显错误的数据,还很难定位,因为日志显示任务正常跑完了。
建议做三件事:第一,调度系统要配置文件产出检测和记录数校验,大表产出为 0 时必须触发告警;第二,上游重刷数据时,下游任务最好通过血缘关系联动重跑,避免新旧数据混在一起;第三,关键指标要配置数据波动监控,比如“今日订单量环比昨日下降超过 50%”就自动报警,即使任务静默异常也能及时发现。另外,调度链路里每个任务要记录数据产出时间和行数,方便回溯问题。
4.4 元数据与血缘:数仓能不能长期维护的隐形决定因素
数仓的项目周期通常以年计,开发中最大的风险不是刚开始建不动,而是半年后没人搞得懂当时的表是干嘛的。元数据管理很多时候被认为“不急”,等到团队换人,业务调整,需要改动某张表时,才发现完全不知道它依赖哪些上游、被哪些下游消费。没有血缘关系,动一张表就像拆炸弹。
所以从建数仓第一天开始,就要要求每张表有规范的技术注释、业务注释、字段级注释,而且要定期维护表血缘和指标血缘。现在很多数据平台都自带自动血缘解析功能,但如果没有强制注释规范,血缘也画不出一张明白图。这里分享一个我长期使用的实战规范:表名规则是[层级][主题][业务过程]_[粒度/周期],比如 dwd_order_pay_flow_di 表示 DWD 层订单支付流水日增量表;字段名统一小写下划线,分区字段固定为 part_dt,注释必须写明业务口径和更新频率。命名规范本身不产生数据价值,但它能在团队协作、排障和交接时节省大量隐性成本。
5. 写在最后:几点个人体会和下一站预告
说实话,写了这么多年数仓,最大的体会不是技术本身有多难,而是那些看起来“简单”的基础问题最难解决:口径对不对、粒度定没定、依赖清不清晰。数仓做得好不好,不是看技术栈多时髦,而是看业务方能不能长期稳定地拿到口径一致的数据。很多团队把精力花在追求新引擎、新框架上,却连一张订单明细事实表的粒度定义都没下功夫,这是本末倒置。
另外一个体会是,分层和建模方法都不要教条。层数不是越多越好,维度建模也不是银弹,关键是模型是否适配团队能力、业务成熟度和查询模式。刚起步的团队,完全可以从 DWD 层明细表和 DWS 层几张宽表开始,一步步迭代,让业务方看到数据价值,再说扩展的事。
这个系列接下来,我打算把某个零售项目里沉淀下来的用户主题建模单独展开写一篇,重点讲用户维度的拉链表设计、用户生命周期口径、标签宽表怎么做不会乱;再往后可以聊实时数仓建设,Flink 接入之后如何跟离线条数仓共享模型、避免“实时一套数、离线一套数”。
最后再分享一个小技巧:每次开发新任务,顺手把“这个表到底给谁用、解决什么问题、口径来源是哪个业务系统”这三句话写在表注释里。这个动作不到一分钟,但半年后能救你一命。数据仓库是一个长期维护的工程,前期把每一块地基夯扎实,后面每一层盖楼都会快很多。