3. dbt模型基础与测试切入点

在 dbt 项目里,测试并不是独立于建模之外的一道“附加工序”,而是从模型定义、依赖声明到物化方式选择的过程中逐步浮现出来的。要判断一个模型该测什么、在哪里测、测到什么程度,首先必须理解 dbt 的四类核心资源——模型(model)、源(source)、种子(seed)、快照(snapshot),以及连接它们的两个关键函数 ref() 与 source()。本节围绕这些基础对象,说明它们各自的质量含义,并给出在模型开发中识别测试需求与风险点的可操作方法。

四类资源:它们各自代表什么、风险在哪里

模型(model)是 dbt 中最主要的资源,通常是一个 .sql 文件,里面写一条 SELECT 语句,dbt 负责把它物化成视图、表或增量表。模型是转换逻辑的载体,因此它的质量风险集中在“逻辑是否正确、粒度是否唯一、字段是否可信”。例如一个订单明细模型,如果 order_id 与 line_no 的组合不唯一,下游任何按订单聚合的指标都会翻倍。

源(source)是对外部已有数据的声明,通常指向数据库里由其他系统写入的表,比如业务库的 raw.orders、埋点平台的 raw.events。源不是 dbt 生成的,dbt 只能读取。它的风险在于“上游不可控”:字段可能被改名、类型可能被调整、分区可能延迟、重复数据可能被补写。因此源是测试的第一道防线,重点测新鲜度、唯一性和非空。

种子(seed)是用 CSV 文件放进 seeds/ 目录、由 dbt seed 加载成表的小型静态数据,常用于国家代码映射、渠道字典、测试用固定维度。它的风险是“人工维护、容易过期或写错”,所以适合测唯一性、枚举值范围和引用完整性。

快照(snapshot)用于记录维度表随时间的变化,通过 dbt snapshot 生成带 dbt_valid_from、dbt_valid_to 的 SCD Type 2 表。它的风险是“时间区间重叠或断裂”,即同一业务主键的有效期出现交叉或空洞,导致按时间点关联时取到错误版本。

ref 与 source:依赖声明决定测试位置

ref('model_name') 用于引用同一 dbt 项目内的模型,source('source_name', 'table_name') 用于引用已在 sources.yml 中声明的外部表。这两个函数不只是“写路径”,它们同时构建了 DAG(有向无环图),让 dbt 知道执行顺序,也让测试可以沿着依赖链定位。

一个实用的判断规则是:测试应尽量靠近风险发生的位置。源表字段可能被上游改坏,就在 source 上测;模型内部逻辑可能产生重复或空值,就在模型上测;种子字典可能写错,就在 seed 上测;快照可能产生时间区间异常,就在 snapshot 上测。把测试放在错误最早可能出现的地方,失败信息才最有指向性。

dbt 资源依赖与测试切入点 测试靠近风险发生位置:source → 模型 → 快照 / 下游模型 源 source raw.orders raw.events 风险:字段改名 / 延迟 / 重复 种子 seed country_code.csv channel_dict.csv 风险:人工维护 / 过期 模型 model stg_orders.sql dim_customer.sql fct_order_items.sql ref('stg_orders') ref('dim_customer') source('raw','orders') 风险:粒度重复 / 空值 / 逻辑错误 风险:关联丢失 / 类型不一致 快照 snapshot dim_customer_snapshot dbt_valid_from / to 风险:时间区间重叠 / 断裂 下游模型 rpt_daily_sales rpt_customer_ltv 风险:指标口径偏差 source() ref() snapshot ref() 测试切入点速查 source 层 新鲜度 / 唯一 / 非空 seed 层 唯一 / 枚举 / 引用完整 model 层 粒度唯一 / 非空 / 关系 snapshot 层 时间区间连续 / 不重叠

在模型开发中识别测试需求:从“问自己”开始

写模型时,不要等模型跑完再想测试。可以在写 SELECT 的同时问四个问题,它们分别对应四类常见风险:

  1. 这个模型的粒度是什么? 如果粒度是“一行一个订单”,那么 order_id 应唯一;如果粒度是“一行一个订单行”,那么 order_id + line_no 应唯一。粒度不唯一是最常见的质量事故来源。
  2. 哪些字段是下游必须依赖的? 主键、外键、金额、时间戳通常不能为空。对它们加 not_null 测试,比事后排查便宜得多。
  3. 字段的取值范围是否受约束? 状态字段只允许 paid、refunded、cancelled,就加 accepted_values;金额不应为负,就加 dbt_utils.expression_is_true 或自定义测试。
  4. 这个模型与上游的关联是否可能丢失? 如果 fct_order_items 通过 customer_id 关联 dim_customer,就应测 relationships,确保没有孤儿记录。

以 stg_orders 为例,一个合理的测试配置可以写成:

models:
  - name: stg_orders
    columns:
      - name: order_id
        tests:
          - unique
          - not_null
      - name: status
        tests:
          - accepted_values:
              values: ['paid', 'refunded', 'cancelled']
      - name: customer_id
        tests:
          - not_null
          - relationships:
              to: ref('dim_customer')
              field: customer_id

这段配置直接对应了上面四个问题:唯一性、非空、枚举范围、引用完整性。它不追求“每个字段都测”,而是把测试集中在真正会影响下游的字段上。

质量风险点的优先级判断

测试资源有限,不可能对所有字段一视同仁。一个实用的优先级排序是:主键唯一性 > 关键字段非空 > 外键关系 > 枚举范围 > 业务规则。主键重复会让所有聚合指标失真,属于最高优先级;关键字段为空会让下游过滤或关联失效;外键断裂会产生孤儿记录;枚举越界通常影响分类统计;业务规则(如“退款金额不超过订单金额”)则视业务影响决定是否加测。

提示:在 dbt 中,source 的测试写在 sources.yml 的 tables 下,模型的测试写在 schema.yml 或模型同目录的 .yml 中。两者语法一致,但 source 测试失败通常意味着上游系统出了问题,模型测试失败则更多指向自身逻辑。
注意:不要用测试替代对数据的理解。例如 unique 测试通过,只说明当前快照下没有重复,不代表增量加载后不会出现重复。对于增量模型,应结合 unique 与 not_null 在增量分区上做验证,必要时使用 dbt_utils.unique_combination_of_columns 处理复合主键。

把测试需求写进开发流程

一个可复用的做法是:每新建一个模型,先在 .yml 中声明它的粒度和关键字段测试,再写 SQL。这样测试不是“补作业”,而是模型契约的一部分。对于 source,在接入新表时同步声明 freshness 和主键测试;对于 seed,在 CSV 提交时检查唯一键和枚举值;对于 snapshot,在首次运行后验证 dbt_valid_from 与 dbt_valid_to 不重叠、最新记录 dbt_valid_to 为空。把这些检查固化为开发习惯,测试才会真正成为数据质量工程的一部分,而不是一次性任务。