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

增量模型:处理新订单和旧订单更正

目标:构建按更新时间更新的订单表,并验证新增、覆盖、漏更正与全量恢复。

前面的 table 每次重新计算完整结果。数据量变大后,可能希望只处理变化的订单,这就是本课的增量模型。代价是你必须说明哪些记录可能变了,以及如何替换已有行。仅写 materialized='incremental',工具并不知道业务上的变更范围。

我们新增一张 order_ledger,一行代表一笔订单的最新状态,保留所有订单状态。它与 customer_summary 并列,专门用于实验,不替换已经可靠的客户汇总路径。增量不是五行数据的性能需要,而是理解状态管理的练习。

增量条件也是正确性条件

创建 models/marts/order_ledger.sql:

{{ config(
    materialized='incremental',
    unique_key='order_id',
    incremental_strategy='delete+insert'
) }}

select
    order_id,
    customer_id,
    order_date,
    status,
    amount_cents,
    updated_at
from {{ ref('stg_orders') }}
{% if is_incremental() %}
where updated_at >= (
    select coalesce(max(updated_at), timestamp '1900-01-01')
    from {{ this }}
)
{% endif %}

文件内的 config 覆盖第 9 课 marts 目录的 table 默认值。第一次没有目标表,is_incremental() 为假,整段 where 不生成;之后目标表存在、模型是 incremental 且没有指定 full-refresh 时,该条件为真。this 指当前模型的目标对象;这里用它读上次处理到的最大更新时间,称为水位。

>= 重读水位边界上的记录,避免漏掉相同时间戳的变更。unique_key 指出哪一列识别同一笔订单;本课使用 DuckDB 支持的 delete+insert,对本批次出现的订单键先删除旧版本,再插入新版本。它不会自动删除“已从上游消失且本批没出现”的键,也不负责替你去重。本课输入必须保持一笔订单一行,并有可靠的 updated_at。

创建 models/marts/order_ledger.yml,属性模型名与现有 schema.yml 中的 customer_summary 不重复:

version: 2
models:
  - name: order_ledger
    description: 每笔订单的最新状态;按 updated_at 水位增量处理,不保存历史版本。
    columns:
      - name: order_id
        data_tests: [not_null, unique]

先预测连续运行两次是否会有十行,再运行:

uv run dbt build --profiles-dir . --select +order_ledger
uv run dbt build --profiles-dir . --select +order_ledger
uv run dbt show --profiles-dir . --inline "select count(*) as n, sum(amount_cents) as cents from {{ ref('order_ledger') }}"

预期仍为 n = 5、cents = 6600,没有重复累加。本课新增两项测试,全项目共 17 项。配置 unique_key 不等于数据库唯一约束,因此仍保留测试。

实验一:新增一笔,同时更正旧订单

将 seeds/raw_orders.csv 完整替换成这份六行数据:

order_id,customer_id,order_date,status,amount_cents,updated_at
101,1,2026-01-01,completed,1300,2026-01-04 10:00:00
102,1,2026-01-02,returned,800,2026-01-02 10:00:00
103,2,2026-01-02,completed,2500,2026-01-02 11:00:00
104,2,2026-01-03,completed,1500,2026-01-03 10:00:00
105,3,2026-01-03,placed,600,2026-01-03 11:00:00
106,4,2026-01-04,completed,700,2026-01-04 09:00:00

101 的下单日期没有改变,但金额与 updated_at 更新了;106 是新增。先比较两者的更新时间与旧水位,再运行:

uv run dbt seed --profiles-dir .
uv run dbt build --profiles-dir . --exclude resource_type:seed
uv run dbt show --profiles-dir . --inline "select count(*) as n, sum(amount_cents) as cents from {{ ref('order_ledger') }}"
uv run dbt show --profiles-dir . --inline "select order_id, amount_cents from {{ ref('order_ledger') }} where order_id in (101, 106) order by order_id"

应为 6 行、总金额 7400 分;101 为 1300,106 为 700。全体订单金额包含 returned、placed,不能与报表的 completed 消费额直接比较。再运行一次相同 build,仍应是 6 行、7400 分。

实验二:一条会被水位漏掉的更正

独立练习:保持刚才的六行输入,只把 103 的金额从 2500 改成 2600,故意不改 updated_at。预测 order_ledger 和 stg_orders 会不会一致,然后 seed、build,再查询:

uv run dbt seed --profiles-dir .
uv run dbt build --profiles-dir . --exclude resource_type:seed
uv run dbt show --profiles-dir . --inline "select s.amount_cents as source_cents, l.amount_cents as ledger_cents from {{ ref('stg_orders') }} s join {{ ref('order_ledger') }} l on s.order_id = l.order_id where s.order_id = 103"
结果、原因与修复

预期 source_cents = 2600、ledger_cents = 2500。103 的时间早于目标水位 2026-01-04 10:00:00,被 where 排除了。17 项测试仍可能全绿:现有检查没有定义 ledger 与源的逐行一致性,customer_summary 又是独立全量构建的表。这是“测试通过不等于一切正确”的具体例子。

先在这个小型练习库中全量重建增量表:

uv run dbt build --profiles-dir . --select +order_ledger --full-refresh
uv run dbt show --profiles-dir . --inline "select amount_cents from {{ ref('order_ledger') }} where order_id = 103"

应变为 2600。实际系统应明确变更时间语义,必要时读取一段重叠窗口、记录装载时间或定期补算。有限窗口只能覆盖窗口内的迟到,并不能无条件修复所有历史变更;全量重建也有成本。

离开本课前恢复基线

回到第 3 课“Seeds 与 SQL”,把 raw_orders.csv 恢复成那里的完整五行版本,再执行:

uv run dbt seed --profiles-dir .
uv run dbt build --profiles-dir . --exclude resource_type:seed --full-refresh
uv run dbt show --profiles-dir . --inline "select count(*) as n, sum(amount_cents) as cents from {{ ref('order_ledger') }}"

必须回到 5 行、6600 分。只恢复 CSV 再普通增量构建不够:旧的 106 可能留在目标中,未来水位也可能阻止旧记录重读。这就是增量引入的历史状态。

若出现重复键,先查输入是否重复,不要以为 unique_key 会自动验证唯一性;若旧值一直不更新,分别查看输入更新时间、目标最大时间与实际筛选条件。保留主线的 order_ledger SQL 和 YAML,恢复原始 CSV 后再进入下一课。

参考资料