未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适合已有 dbt Model、MERGE/UPSERT 和分区基础的读者学习增量策略并开展受控练习,不是可跨数据库直接部署的生产模板;本次仅作全文结构审阅,未执行代码或外部事实核验。示例中的严格最大水位过滤仍需读者处理空目标表、同时间戳及迟到数据,三天回看不能保证覆盖所有历史变更;merge 需满足匹配键非空、输入键粒度唯一等条件,不能代替数据质量测试。BigQuery 分区覆盖须提供被替换分区应保留的完整数据,无未来日期数据时示例涉及今天及前三天共四个日期,窗口外修正需另行处理。数据库与适配器的策略支持、事务行为、SQL 方言、刷新可用性和实际性能须按项目环境确认;决策树中的千万行门槛宜视为教学简化,schema 自动同步也不应理解为自动回填历史数据。
dbt 增量模型 - 大表的高效更新策略
abstract 本文核心
假设某订单表有 5 亿行,在特定环境下每次 dbt run 全量重建需要 4 小时,且这一耗时超过业务可接受范围。这里的行数与耗时是示例假设,不代表通用性能结果。
增量模型(incremental materialization)是解决这个问题的核心手段。
核心思路:只处理新增和变更的数据,跳过已经处理过的历史数据。
为什么需要增量模型
全量重建(materialized='table')的问题:
假设场景下每次全量 dbt run 的成本(以下数字并非实测结果):
- 读取整个订单表(5亿行)
- 执行所有 JOIN 和计算
- 删除旧表,创建新表
时间:4-6 小时
成本:大量的 BigQuery slot / Snowflake Credit
实际上,每次新增的数据:
- 今天新产生的订单:10万行(占总量的 0.02%)
- 历史数据从未变化
增量模型的逻辑:只处理上次运行之后新增的数据。
基础增量模型
最简单的增量模型
-- models/marts/core/fct_orders.sql
{{ config(
materialized='incremental', -- 声明为增量模型
unique_key='order_id' -- 匹配键;是否用于更新取决于增量策略,不能单独保证 upsert
) }}
WITH source_orders AS (
SELECT * FROM {{ ref('stg_orders') }}
),
final AS (
SELECT
order_id,
customer_id,
created_at,
updated_at,
total_price_cents,
financial_status
FROM source_orders
)
SELECT * FROM final
{% if is_incremental() %}
-- 关键:只在增量运行时过滤,只处理新数据
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
-- ↑
-- {{ this }} 代表当前模型对应的表
-- 这行 SQL 查询现有表中最新的时间戳
{% endif %}
is_incremental() 宏的工作原理
第一次运行(表不存在):
is_incremental() = FALSE
→ WHERE 子句不生效
→ 处理所有历史数据,全量建表
后续增量运行(目标表已存在、模型采用 incremental 物化,且未执行全量刷新):
is_incremental() = TRUE
→ WHERE 子句生效
→ 只处理 updated_at 大于当前表最大值的数据
→ 按配置或适配器默认的增量策略写入现有表;是否按 unique_key 更新已有记录取决于所用策略
tip {{ this }} 的含义
{{ this }} 是 dbt 的特殊变量,代表当前模型对应的数据库表。
在增量运行时,{{ this }} 就是已有的数据,新数据会与它合并。
增量策略(incremental_strategy)
dbt 支持多种合并策略,不同数据库支持的策略不同:
策略一:append(仅追加)
{{ config(
materialized='incremental',
incremental_strategy='append' -- 只追加,不更新
) }}
SELECT
event_id,
user_id,
event_type,
event_timestamp
FROM {{ ref('stg_events') }}
{% if is_incremental() %}
WHERE event_timestamp > (SELECT MAX(event_timestamp) FROM {{ this }})
{% endif %}
- 行为:直接 INSERT 新数据,不检查重复
- 适用场景:只有新增没有更新的数据(如用户行为日志、事件流)
- 注意:如果源数据有重复或上游可能补数,会导致表中有重复行
策略二:merge(合并,推荐)
{{ config(
materialized='incremental',
incremental_strategy='merge',
unique_key='order_id' -- 按订单键匹配更新;匹配键须非空且输入在键粒度上唯一,并非声明数据库主键
) }}
SELECT
order_id,
customer_id,
status,
total_price_cents,
updated_at
FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}
- 行为:
MERGE INTO ... WHEN MATCHED THEN UPDATE, WHEN NOT MATCHED THEN INSERT - 适用场景:数据可能更新(订单状态变化、用户信息修改)
- 支持:Snowflake、BigQuery、Spark、Databricks
- PostgreSQL 注意:能否使用
merge取决于 PostgreSQL 版本及 dbt-postgres 适配器支持;delete+insert可用于实现按键替换,不能概括为 PostgreSQL 唯一的增量更新方式。
策略三:delete+insert
{{ config(
materialized='incremental',
incremental_strategy='delete+insert',
unique_key='order_id'
) }}
SELECT ...
- 行为:先删除匹配 unique_key 的旧数据,再插入新数据
- 适用场景:当前数据库或 dbt 适配器不支持所需 merge 策略,或适合按键删除后插入的场景;不能将 PostgreSQL、Redshift 一概归为不支持 MERGE。
- 原子性:取决于适配器实现及事务支持;若删除与插入处于同一事务中,不能直接断言会暴露中间数据空缺。
策略四:insert_overwrite(按分区覆盖)
{{ config(
materialized='incremental',
incremental_strategy='insert_overwrite',
partition_by={
"field": "order_date",
"data_type": "date",
"granularity": "day" -- BigQuery 特有
}
) }}
SELECT
order_id,
order_date,
customer_id,
total_price_cents
FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
WHERE order_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY) -- 从今天前第 3 天起覆盖;若无未来日期数据,包含今天及前三天共 4 个日期
{% endif %}
- 行为:按分区整体覆盖,不按行合并
- 适用场景:BigQuery 分区表、Spark 分区写入
- 优点:适合按日期分区并能提供完整分区数据的大表;是否比 merge 更高效取决于扫描范围、分区规模和执行实现。
增量过滤策略
"只处理增量数据"的核心是找到合适的过滤条件:
方案一:基于更新时间(最常用)
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}
- 前提:源表必须有
updated_at字段且可靠更新 - 风险:如果有时钟偏差或延迟数据,可能漏掉
方案二:带缓冲的时间过滤(更安全)
{% if is_incremental() %}
-- 往回多看 3 天,处理延迟到达的数据
-- 配合 merge,并确保匹配键非空、输入在键粒度上唯一,才能避免回看数据被重复插入
WHERE updated_at > (SELECT MAX(updated_at) - INTERVAL '3 days' FROM {{ this }})
{% endif %}
tip 为什么要加缓冲?
上游数据有时会延迟到达(比如昨天的数据今天才同步过来)。 多往回看几天可覆盖窗口内的延迟数据;配合 merge,并确保匹配键非空且输入在键粒度上唯一,可避免重复写入。窗口之外的迟到数据仍需补数或其他处理机制。 性能损失可以接受(多处理几天的数据),数据正确性更重要。
方案三:基于自增 ID
{% if is_incremental() %}
WHERE order_id > (SELECT MAX(order_id) FROM {{ this }})
{% endif %}
- 前提:自增 ID 严格单调递增,且数据不会更新
- 适用:只有 INSERT 没有 UPDATE 的表(如日志事件)
方案四:多条件组合
{% if is_incremental() %}
-- 新增的订单 OR 状态有变化的订单
WHERE order_id > (SELECT MAX(order_id) FROM {{ this }})
OR (
updated_at > (SELECT MAX(updated_at) FROM {{ this }})
AND order_id <= (SELECT MAX(order_id) FROM {{ this }})
)
{% endif %}
全量刷新:--full-refresh
有时候需要抛弃增量记录,重新全量构建:
# 对所有增量模型执行全量刷新
dbt run --full-refresh
# 只对指定模型全量刷新
dbt run --select fct_orders --full-refresh
需要全量刷新的场景:
- 上游数据修复了历史数据(如补录了半年前的订单)
- 增量逻辑有 bug,导致历史数据不准确
- 模型的字段定义变更(新增列等)
- 初次部署到新环境
warning 全量刷新的代价
全量刷新会重新构建目标表,但替换方式取决于适配器实现,这意味着:
- 可能影响表的可用性,应评估对 BI 报表的影响,不能假定所有实现都会产生不可用窗口
- 需要重新处理全量数据,实际时间和成本取决于模型与执行环境
- 在生产环境需要选择业务低峰期执行
增量模型的主键设计
unique_key 的选择直接影响数据正确性:
单字段主键
{{ config(
materialized='incremental',
unique_key='order_id' -- 简单情况,订单ID唯一
) }}
复合主键
{{ config(
materialized='incremental',
unique_key=['order_id', 'item_id'] -- 订单明细表:订单+商品才唯一
) }}
warning unique_key 必须真正唯一
如果 unique_key 指定的字段在源数据中有重复,merge 的结果会不确定。 务必在 staging 层测试 unique 约束,确保数据清洗干净后再传递给增量层。
分区与增量结合(BigQuery 示例)
在 BigQuery 等支持分区的数据仓库中,结合分区可能减少扫描和重写范围;实际收益取决于过滤条件、分区规模和执行实现:
-- models/marts/fct_orders.sql(BigQuery 分区增量模型)
{{ config(
materialized='incremental',
incremental_strategy='insert_overwrite',
partition_by={
"field": "order_date",
"data_type": "date",
"granularity": "day"
},
cluster_by=["customer_id", "financial_status"] -- 聚簇键提升过滤效率
) }}
SELECT
order_id,
customer_id,
DATE(created_at) AS order_date,
financial_status,
total_price_cents
FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
-- 从今天前第 3 天起处理分区;若无未来日期数据,覆盖今天及前三天共 4 个日期
WHERE DATE(created_at) >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)
{% endif %}
常见坑与解决方案
warning 坑一:更新时间字段不可靠
症状:增量运行后发现历史数据更新没有被捕获
原因:源系统更新数据时没有更新 updated_at 字段
解决:
- 联系上游修复 updated_at 的维护逻辑
- 使用 CDC(变更数据捕获)工具(如 Debezium)来捕获变更
- 接受缺陷,做全量刷新
warning 坑二:schema 变更导致增量失败
症状:新增了一个字段,增量运行报错列不存在 原因:增量模式下,dbt 会 INSERT INTO 已有表,新列在老表里不存在 解决:
# 执行全量刷新,重建表结构
dbt run --select fct_orders --full-refresh
或者配置 on_schema_change 参数:
{{ config(
materialized='incremental',
on_schema_change='sync_all_columns' -- 自动同步 schema 变更
) }}
warning 坑三:增量模型间的依赖
症状:A 增量模型依赖 B 增量模型,B 跑完后 A 的增量过滤条件不准确
原因:A 过滤 WHERE updated_at > MAX(updated_at IN A),但 B 可能包含了 A 没见过的数据
解决:检查上下游变更传播、水位字段和回看窗口是否匹配。增量模型可以引用另一个增量模型;直接引用 staging 层并不能自动消除迟到数据或水位遗漏。
warning 坑四:忘记 --full-refresh 导致数据积累错误
症状:表里数据越来越多,但有明显重复
原因:unique_key 设置有误,或者增量逻辑本身有 bug
解决:及时执行 dbt run --full-refresh 清理,修复逻辑后重跑
增量模型设计原则
| 原则 | 说明 |
|---|---|
| 幂等性 | 相同输入重复运行不应改变结果;merge 需满足匹配键、输入唯一性和转换确定性等条件,append 需另行避免重复追加 |
| 安全缓冲 | 根据实际延迟范围设置回看窗口;窗口外迟到数据仍需补数或其他处理机制 |
| 配合 unique_key | 按键更新或替换时配置合适的匹配键;append 和按分区覆盖的 insert_overwrite 不依赖行级 unique_key |
| 优先处理大表 | 小表用全量更简单,不要过度设计 |
| 监控首次运行 | 首次全量运行时间可能很长,要提前计划 |
总结
增量模型决策树:
表的数据量大吗(>1000万行)?
└─ 是 → 全量重建耗时可接受吗?
│ └─ 不可接受 → 使用增量模型
│ ├─ 只有新增,没有更新 → append 策略
│ ├─ 有更新,需要 upsert → merge 策略
│ ├─ 按分区管理 → insert_overwrite 策略
│ └─ 不支持 merge → delete+insert 策略
└─ 否 → 使用全量表(materialized='table'),逻辑更简单
下一步
增量模型可以减少重复处理,但实际运行效率取决于过滤与写入策略;如何进一步检查数据正确性?
06-测试与数据质量 - 用代码的方式保证数据质量,把 QA 测试内嵌到 dbt 工作流
最后更新:2026-03-18
#dbt #增量模型 #incremental #性能优化 #数据处理
info 前置知识
- 掌握 dbt Model 的基础用法(
ref()、config()),参考 dbt Model 基础 - 理解数据库中 MERGE/UPSERT 的语义(增量更新的核心操作)
- 了解数据的分区概念(按日期分区加速过滤)
掌握检查
- [ ] 能写出一个带
{{ is_incremental() }}过滤条件的增量模型,并解释首次运行(全量)和后续运行(增量)时的行为差异 - [ ] 能说出
append、merge、insert_overwrite、delete+insert四种增量策略各自的适用场景,以及需要什么前提条件(如 merge 需要数仓支持 MERGE 语句) - [ ] 理解"增量过滤多往回看几天"的必要性:为什么不能只看
WHERE updated_at > MAX(updated_at),而要多扣几天(防止数据延迟到达) - [ ] 知道
dbt run --full-refresh的使用时机:schema 变更、增量逻辑修复、数据重置时需要全量重建 - [ ] 能解释增量模型依赖另一个增量模型时,如何检查变更传播、水位字段和回看窗口是否匹配
实践练习
基础题:把你项目中的一个全量表(materialized='table')改造为增量模型:添加 is_incremental() 过滤条件,设置 unique_key,配置 merge 策略。运行 dbt run 首次全量,然后再运行一次验证增量逻辑正常。
进阶题:有一张订单表,订单会在创建后 7 天内更新状态(从 pending 变成 completed 或 cancelled)。分析 WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }}) 在更新时间可靠且及时到达、数据延迟到达、多个记录具有相同时间戳、目标表为空等情况下的行为,并提出相应处理方式。注意:创建后 7 天内更新并不意味着数据最多延迟 7 天,回看窗口应依据实际延迟范围确定。