












继上一篇基础 FAQ 之后,继续围绕本项目梳理 5 个更深入的问题:增量加载、源表变更、数据测试等核心机制。项目结构依然是 staging → core(Data Vault)→ mart 三层架构。
business_db(SQL Server 业务库,含 customers / orders / products 三张表)incremental 增量模型,用哈希键(hashkey/hashdiff)做 Data Vaultmaterialized='incremental' 是什么意思?和 table 有什么区别?答:incremental(增量)是指每次 dbt run 只把"新数据"追加到表里,而不是整表重建。
| 类型 | 每次 dbt run 做什么 | 适用场景 | 数据量 |
|---|---|---|---|
view |
只更新视图定义,不存数据 | 轻量转换、查询少 | 小 |
table |
DROP 后重建整张表 | 维度表、全量刷新 | 中 |
incremental |
只 INSERT 新数据,不动已有数据 | 事实表、历史累积数据 | 大 |
hub_order 为例{{ config(
materialized = 'incremental',
unique_key = 'order_hk'
) }}
-- ... 中间逻辑省略 ...
SELECT
order_hk,
order_id,
load_date,
source_system
FROM new_records
{% if is_incremental() %}
WHERE order_hk NOT IN (SELECT order_hk FROM {{ this }})
{% endif %}
第一次跑(全量):表还不存在,dbt 直接 CREATE TABLE AS SELECT ...,把所有数据都装进去。
第二次及以后跑(增量):表已经存在了,dbt 只把 WHERE 条件筛选出的新记录 INSERT 进去。
unique_key = 'order_hk' 告诉 dbt:用这个字段判断记录是否重复。如果新数据和老数据的 order_hk 一样,dbt 会执行 UPDATE(而不是 INSERT)。但在我们的 hub 表里,因为有 WHERE order_hk NOT IN (...) 的过滤,所以实际只有新增,不会有更新。
注意:如果
unique_key没配好,可能导致主键冲突或者数据重复。
is_incremental() 这个判断是干啥的?为什么第一次跑和后续跑逻辑不一样?答:is_incremental() 是 dbt 提供的一个 Jinja 函数,用来判断"这次运行是不是增量模式"。
| 场景 | is_incremental() 返回 |
|---|---|
| 表不存在(第一次跑) | false |
用 dbt run --full-refresh 强制全量 |
false |
| 表已存在,正常增量跑 | true |
看 sat_customer.sql 里的这段逻辑:
{% if is_incremental() %}
-- 增量模式:对比 hashdiff,只追加变化的记录
changed_records AS (
SELECT ...
FROM hashed h
LEFT JOIN latest_existing le
ON h.customer_hk = le.customer_hk AND le.rn = 1
WHERE le.customer_hk IS NULL -- 全新记录
OR le.hashdiff != h.hashdiff -- hashdiff 变化(说明属性变了)
)
{% else %}
-- 全量模式:所有记录都是"新"的,直接装
changed_records AS (
SELECT * FROM hashed
)
{% endif %}
第一次跑的时候,目标表还不存在,{{ this }}(指当前模型自己的表)根本没法查。如果强行跑增量逻辑里的 FROM {{ this }},SQL 会报错"对象不存在"。
所以要分两种情况:
WHERE load_date > (SELECT MAX(load_date) FROM {{ this }})你们的 satellite 用的是第二种(hashdiff 对比),hub 和 link 用的是第一种思路的变体(主键不存在就插入)。
答:默认感知不到。dbt 的增量模型只负责"加新数据",不负责"发现被删的数据"。
想象一下这个场景:
第 1 次 dbt run:源表有 100 条订单 → hub_order 装入 100 条
第 2 次 dbt run:源表被删了 5 条,还剩 95 条
↑ dbt 增量只看"新来的",不看"少了的"
hub_order 里还是 100 条 ❌
增量 SQL 的逻辑是:
WHERE order_hk NOT IN (SELECT order_hk FROM hub_order)
它只找源表里有、目标表里没有的,不会找目标表里有、源表里没有的。
源表直接删掉行,dbt 增量模型完全不知道。解决办法:
dbt run --full-refresh 定期重建 core 层:比如每周一次全量,纠正删除。is_deleted 或 deleted_at 字段)源表不真删,只是把某条记录标记为已删除:
UPDATE customers SET is_active = 0, deleted_at = GETDATE() WHERE customer_id = 123
这种方式 dbt 可以感知,因为:
is_active / deleted_at 来判断当前状态Data Vault 范式本身就倾向于只增不改不删,所以软删除和 Data Vault 的理念非常契合。
你们 stg_customers 里有 is_active 字段,sat_customer 也带了这个字段并参与 hashdiff 计算。所以如果业务库用软删除(改 is_active=0),整条链路是能正确追踪到的。但如果业务库直接 DELETE,core 层和 mart 层都会保留"幽灵数据"。
答:看变的是哪一层,影响范围不一样。简单来说:上游加字段,下游逐层传递;上游删字段,下游要清理。
vip_level)需要改的文件从上到下依次是:
| 层级 | 文件 | 改什么 |
|---|---|---|
| staging | stg_customers.sql |
SELECT 里加上 vip_level 字段 |
| staging | schema.yml |
sources 里的 columns 补上新字段描述(可选但推荐) |
| core | sat_customer.sql |
SELECT 里加 vip_level,并把它加入 generate_hashdiff() 的参数列表 |
| mart | dim_customer.sql |
从 sat 里把 vip_level 取出来 |
| mart | schema.yml |
dim_customer 的 columns 里加上文档(可选) |
重点提醒:satellite 表的 hashdiff 计算必须包含新字段,否则这个字段变化不会触发新的快照版本。
-- 改之前
{{ generate_hashdiff(['customer_name', 'email', ...]) }} AS hashdiff,
-- 改之后(加上 vip_level)
{{ generate_hashdiff(['customer_name', 'email', ..., 'vip_level']) }} AS hashdiff,
region)| 层级 | 文件 | 改什么 |
|---|---|---|
| staging | stg_customers.sql |
SELECT 里去掉 region |
| core | sat_customer.sql |
SELECT 去掉,并从 hashdiff 参数中移除 |
| mart | dim_customer.sql |
去掉对 region 的引用 |
| mart | 下游报表 / 宽表 | 检查有没有地方用到这个字段 |
customer_id 从 INT 改成 BIGINT)SELECT * 过来或显式列出,类型跟着源表走)CAST(customer_id AS BIGINT) AS customer_id,把类型固定住table 类型的模型加字段无所谓,反正每次重建。但 incremental 表加字段要小心:
老数据:没有 vip_level 字段(或为 NULL)
新加了字段后增量跑 → SQL Server 会报错:列数不匹配
解决方案:
dbt run --full-refresh --select sat_customer 全量重建一次经验法则:只要增量表的结构变了(加列、改类型),就跑一次
--full-refresh。
# 看 stg_customers 下游有哪些模型(改了 stg 之后,这些都可能要改)
dbt ls --select stg_customers+
答:tests 是 dbt 的数据质量测试机制,跑 dbt test 时会执行,用来验证数据是否符合预期。
在 schema.yml 里给表和字段写断言,dbt 会生成对应的 SQL 查询去验证。如果返回结果不符合预期,测试就失败。
以你们项目 staging/schema.yml 里的为例:
sources:
- name: business_db
tables:
- name: customers
columns:
- name: customer_id
tests:
- unique # customer_id 必须唯一
- not_null # customer_id 不能为空
| 测试 | 作用 | 生成的 SQL 大致逻辑 |
|---|---|---|
unique |
字段值必须唯一 | SELECT customer_id, COUNT(*) FROM ... GROUP BY customer_id HAVING COUNT(*) > 1 |
not_null |
字段值不能为空 | SELECT * FROM ... WHERE customer_id IS NULL |
accepted_values |
字段值必须在指定列表中 | SELECT * FROM ... WHERE status NOT IN ('new','paid','shipped') |
relationships |
外键必须在主表中存在(参照完整性) | SELECT customer_id FROM orders WHERE customer_id NOT IN (SELECT customer_id FROM customers) |
你们项目里 orders 的 customer_id 就配了关系测试:
- name: customer_id
tests:
- not_null
- relationships:
to: source('business_db', 'customers')
field: customer_id
意思是:orders 表里的每个 customer_id,都必须在 customers 表里存在。如果出现一个订单指向不存在的客户,测试就会失败。
# 跑所有测试
dbt test
# 只跑某个模型的测试
dbt test --select stg_customers
# 跑 build = run + test 一气呵成
dbt build --select stg_customers+
你们项目在三个地方都有测试:
| 位置 | 测试对象 | 目的 |
|---|---|---|
sources: 下面 |
源表 | 校验上游源数据质量(数据进来就查) |
models: 下面的 staging |
stg 表 | 校验贴源后的数据 |
models: 下面的 mart |
dim/fct 表 | 校验最终交付给分析师的数据 |
经验法则:越靠近源头的测试越重要。问题越早发现,排查成本越低。
dbt test 只检查不动表,不会修改任何数据这 5 个问题,其实都围绕着一个核心主题:dbt 项目不是搭完就完了,它要跟着业务一起演进。
把这些机制都理解透,才能从"会写 dbt 模型"进化到"能运维一套 dbt 数仓"。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。