跳到主要内容

数据仓库建模原则

原则是用来坚守的,原则是会被打破的,每次妥协前知晓代价,代价可承担就好。坚守不住的时候想想为什么数仓是一把手项目。

做数仓的人都有过这种经历:

接手一张"祖传"的表,字段200+,没人说得清粒度是什么;跑出来的数和业务方对不上,排查三天发现是两个口径各算各的;好不容易上线的模型,三个月后因为新需求要推倒重来。

这些问题的根源不是SQL写得不好,是建模的时候没有原则,或者有原则但没守住。

以下是我这些年踩坑之后沉淀下来的建模原则。不是教科书上的理论,是拿血泪教训换来的。


关于粒度:一切问题的起点

1. 一张表只表达一种粒度

一张事实表中的每一行必须代表同一层级的业务事件。不同粒度(如订单级和订单明细级)绝不混在同一张表中。混粒度会导致度量值无法直接聚合,上层查询必须带隐含过滤条件才能得到正确结果。

首先思考事实表的主键是什么,如果主键是联合主键,且组成联合主键的非ID字段比较多,则需慎之又慎

2. 先定义粒度,再决定字段

建表的第一步不是列字段清单,而是用一句话回答:"这张表里一行代表什么?"

这个问题答不清楚,后面所有字段选择都是在赌运气。

粒度一旦声明,哪些维度该放进来、哪些度量有意义就自然确定了。

回忆维度建模过程:

  1. 选择业务过程
  2. 声明粒度
  3. 确定维度
  4. 确定事实

关于一致性:口径打架的根源

1.让数据只在一个地方被定义

"活跃用户"这个口径,在你的数仓里有几个版本?如果答案大于1,恭喜,你已经埋下了一颗定时炸弹。

同一个概念只应有一个权威计算位置。下游所有消费方引用结果,而不是各自重新实现。每多一个副本,就多一个将来对不上的隐患。

2.维度一致才能跨域分析

用户维度、时间维度、地区维度——这些跨业务共用的维度必须统一维护。两个事实表如果用的不是同一份用户维度,你在SQL里JOIN它们就是在做无意义的笛卡尔积。


关于分层:不是形式主义

1.上层不穿透下层

ADS直接读ODS,DWS跳过DWD去读源表——每次看到这种SQL我就知道,这个团队迟早要出事。

穿透访问意味着绕过了清洗和标准化逻辑。源头改了一个字段类型,你的ADS毫无感知地产出了一堆脏数据。

2.公共逻辑下沉,个性逻辑上浮

同一段逻辑出现在三个以上的地方?沉淀为公共层。但也别过度下沉——公共层每多一张表,改动成本就多一分。这是个平衡的艺术。


关于冗余:不是越宽越好

1.冗余是有代价的设计决策,不是偷懒

每一次冗余都要回答三个问题:

  1. 这个字段由谁维护更新?
  2. 源头变化时如何同步?
  3. 不一致时以谁为准?

答不上来,就别冗余。

2.宽表有边界,不能无限追加字段

判断标准很简单:这个字段的更新频率和生命周期,跟表里其他字段一致吗?

用户基本信息一年不变一次,用户最近7天活跃天数每天都在变——它们不该在同一张表里。

宽表是结果,不是目标,不是工具


关于时间:对不上的第一大原因

1.时间字段必须消除歧义

event_time、create_time、etl_time——这三个东西含义完全不同。但我见过无数张表,就一个 dt 字段,没人说得清它到底代表什么。

每个时间字段必须声明:它是事件发生时间,还是入库时间,还是处理时间?顺便把时区也定了。


关于变化:别假装世界是静止的

1.维度变化必须有策略

用户等级会变,商品分类会调,组织架构会重组。"暂时不考虑变化"等于默认选了Type 1(直接覆盖),而且在出问题之前没人会发现这个决定。

建表时就得为每个字段声明:变了怎么办?覆盖?拉链?还是加新列?


关于质量:不是附加品

1.业务主键必须显式声明并保证唯一

"大家都知道这张表的主键是order_id"——这句话翻译一下就是"这张表的主键没有任何校验,重复了也没人知道"。

没有显式主键的表无法安全JOIN,无法判断重复,无法做幂等写入。

2.数据必须可勾稽验证

明细层SUM出来的金额,必须等于汇总层的数字。拆出去的多张表,金额之和必须等于源头总数。

无法勾稽的模型,出了问题就是一笔糊涂账。

3.NULL是有语义的

NULL是"不知道",0是"确认为零",空字符串是"确认为空"。三者不能混用。随意用默认值填充NULL,是在掩盖问题。


关于运维:模型是活的

1.任何任务重跑必须结果一致

今天跑和明天跑,任务跑几遍,同样的输入必须产出同样的结果(数据幂等)。这意味着:

  • INSERT OVERWRITE,不是INSERT INTO
  • 业务时间做分区键,不是跑批时间
  • 不依赖"当前时间"这种隐含变量
  • row_number等排序开窗数据的分组字段必须能保证数据唯一

2.设计时就要考虑纠错成本

数据一定会出错,这不是如果,是什么时候的问题。

某天的数据写坏了,能不能只重跑那一天?维度表历史记录要修正,影响范围有多大?让纠错代价可控的模型,才是能在生产环境活下来的模型。

3.数据有成本,模型需要生命周期

每张表都在烧存储、烧计算、烧维护人力。建表时就该定义:保多久?刷多频繁?没人用了多久之后可以下线?

不管生命周期的仓库,只会长大,不会变好。

4. 分区是数据的物理边界,必须与业务时间对齐

分区字段代表数据的物理隔离边界,决定了查询扫描范围和生命周期管理的最小单位。分区策略必须与业务数据的产生节奏一致——按天产生的数据按天分区,按小时到达的数据按小时分区。错位的分区会导致要么多扫数据,要么跨分区才能拿到完整快照。


关于协作:模型是给人用的

1.命名是契约,不是标签

dws_trade_order_1d 这个名字承诺了:汇总层、交易域、订单粒度、天级聚合。

任何人看到表名就能判断这张表能不能满足自己的需求。如果看了名字还得打开文档甚至读SQL才能确认——命名就失败了。

2.指标口径必须与模型绑定

每个指标必须能落地为:表 + 字段 + 过滤条件 + 聚合方式。

如果一个指标定义写不成确定的SQL,它还停留在需求阶段。口径文档脱离模型独立维护,三个月后一定和实际产出对不上。

3.枚举值必须有码表

"1=有效 2=无效"只写在代码注释里?等着吧,迟早有人把2当成"待审核"用。

4.模型的稳定性优先于完美性

核心层的模型一旦上线并被下游依赖,其结构变更的成本远高于最初设计时多想一步。宁可在设计阶段多花时间讨论粒度和维度边界,也不要追求快速上线后再重构。


关于依赖:保持有向无环

1.模型之间的依赖必须是DAG

循环依赖 = 调度无法确定顺序 = 一个节点挂了全链路不可控。

如果发现两张表互相依赖,说明职责划分出了问题。

2.跨源整合必须定义归一规则

用户信息来自APP注册和客服系统,两个源的手机号不一致,以谁为准?

不定义归一规则就整合,得到的不是完整视图,是一坨混合数据。


最后

这些原则不复杂,甚至有些显而易见。但建模的难不在于知道这些原则,而在于在项目压力下、在需求催促中、在"先上线再优化"的诱惑面前,还能守住它们。

每一条被违反的原则,都会变成未来某天的一次紧急排查、一次数据事故、一次推倒重来。

建模的时候多花一天想清楚,等于给未来的自己省三个月。