Course Builder个人学习工作台 · 本地模式
dbt 入门:把 SQL 变成可信的数据流水线

业务测试:把报表口径写成检查

目标:编写返回违规行的 SQL 测试,检查负金额和跨模型金额一致性。

上一课的通用测试能验证结构,却不知道“completed 才计入消费”。如果有人把 returned 也计入报表,订单编号仍然唯一,客户关系仍然有效,所有通用测试可能继续通过。本课给业务规则添加自己的反例查询。

singular data test 是 tests/ 下的一份 SQL 文件。它和模型都写 select,但含义相反:模型返回我们要交付的数据,测试返回不该出现的数据。默认测试结果为零行才通过。不要写 select count(*) 直接当测试,即使数值为零,它仍返回一行。

第一条规则:订单金额不能为负

创建 tests/assert_nonnegative_order_amount.sql:

select order_id, amount_cents
from {{ ref('stg_orders') }}
where amount_cents < 0

这是本店样本的口径:退货用状态表示,原订单金额保持非负。其他系统可能用负数表达退款,那就需要另一条业务规则,不能生搬硬套。

第二条检查把报表金额与原始业务口径对账。创建 tests/assert_revenue_reconciles.sql:

with expected as (
    select coalesce(sum(amount_cents), 0) as cents
    from {{ ref('stg_orders') }}
    where status = 'completed'
),
actual as (
    select coalesce(sum(revenue_cents), 0) as cents
    from {{ ref('customer_summary') }}
)
select expected.cents as expected_cents, actual.cents as actual_cents
from expected
cross join actual
where expected.cents <> actual.cents

两个 CTE 各产生一个数字,cross join 把这两个一行结果并排放到一行。<> 表示不相等,只有不一致才返回。这里用整数分比较,避免将浮点近似误差误判成业务错误。coalesce 也让没有成交订单时两边都按零处理。

先判断正常输入会返回几条违规行,再运行:

uv run dbt build --profiles-dir . --exclude resource_type:seed
uv run dbt test --profiles-dir . --select assert_nonnegative_order_amount assert_revenue_reconciles

预期两个自定义测试均 PASS。加上上一课的 13 项,此时共有 15 项测试。对账只能检查这一条总额规则;它不能证明每位客户的分配都正确,也不能证明数据是今天刚同步的。

故意改坏报表,再查看反例

  1. 在 customer_summary.sql 中,把 where status = 'completed' 改为 where status in ('completed', 'returned')。
  2. 运行下面两条命令。第二条是本次实验预期的失败。
uv run dbt run --profiles-dir . --select customer_summary
uv run dbt test --profiles-dir . --select assert_revenue_reconciles
  1. 单独预览报表总额:
uv run dbt show --profiles-dir . --inline "select sum(revenue_cents) as cents from {{ ref('customer_summary') }}"

应得到 6000 分,正确口径仍是 5200 分。失败不是坏事,它证明这条检查能够识别刚才引入的回归。

  1. 恢复 completed 筛选,重新执行本课开头的 build 和 test,两项均应通过。

独立练习:编写 tests/assert_all_customers_present.sql,找出 staging 中存在、但客户汇总中缺失的客户。先写反例查询,再展开答案。完成后可运行验证;为保持后续测试数量一致,验证后删除这个练习文件。

完整参考实现
select c.customer_id
from {{ ref('stg_customers') }} as c
left join {{ ref('customer_summary') }} as s
    on c.customer_id = s.customer_id
where s.customer_id is null
uv run dbt test --profiles-dir . --select assert_all_customers_present

正常版本应 PASS。如果临时改回上一课讨论的 inner join,反例会包含客户 3、4。测试后务必恢复 left join 并重新 build;最后删除练习文件,只保留本课主线的两份测试。

若测试永远失败,检查是否返回所有“正确行”,或写了始终返回一行的聚合;若更改模型后测试没变,先 run/build,让数据库使用新模型。SQL 测试末尾不要加分号,dbt 可能把查询包在其他语句中执行。

本课保留两份主线 SQL 测试,报表仍只统计 completed。与第 7 课结合后,项目既检查输入结构,也检查关键业务口径。

参考资料