物化与编译:SQL 最后变成了什么
目标:把客户汇总改成表,并通过修改输入区分表和视图的更新行为。
到目前为止,默认模型都是视图。视图保留查询定义,读取时使用当前上游数据;表保存构建时算出的结果。两者都能被 select,但计算发生的时机不同。物化(materialization)就是 dbt 把模型落到数据库中的策略。
本项目的 staging 规则很轻,保留为 view;客户汇总作为交付结果,改成 table。这个选择是教学设计,不是“所有 staging 都必须是视图”的规则。只有几行数据时,两者没有值得讨论的性能差距;我们关心刷新语义。
| 类型 | 数据库中的形态 | 需要考虑什么 |
|---|---|---|
| view | 保存查询定义 | 每次读取可能重新计算上游 |
| table | 保存构建结果 | 输入变化后,需要重新构建才更新 |
| incremental | 表,每次按指定范围更新 | 要设计变更识别与补算,第 12 课实践 |
| ephemeral | 作为下游 SQL 的一部分展开 | 没有独立表可直接查询,本课不使用 |
在项目里配置不同目录
将 dbt_project.yml 完整替换为:
name: shop_dbt
version: '1.0.0'
config-version: 2
profile: shop_dbt
model-paths: [models]
seed-paths: [seeds]
test-paths: [tests]
macro-paths: [macros]
models:
shop_dbt:
staging:
+materialized: view
marts:
+materialized: table
models 下面先是项目名,再是目录名。带 + 的键是配置项,避免与路径名称混淆。hello 不在 staging 或 marts 目录,继续使用默认 view。
预测构建后 customer_summary 是否仍能用相同的 ref 查询,再运行:
uv run dbt build --profiles-dir . --exclude resource_type:seed
uv run dbt show --profiles-dir . --inline "select table_name, table_type from information_schema.tables where table_schema = 'main' and table_name in ('stg_orders', 'customer_summary') order by table_name"
预期输出:
table_name | table_type
customer_summary | BASE TABLE
stg_orders | VIEW
information_schema.tables 是数据库提供的对象目录。ref 面向资源名,所以不需要因为物化改变就修改引用它的代码。
看见 dbt 编译后的 SQL
uv run dbt compile --profiles-dir . --select customer_summary
cat target/compiled/shop_dbt/models/marts/customer_summary.sql
打开结果文件,应看到 ref 被替换成包含数据库和 schema 的对象引用,例如 "shop"."main"."stg_orders"。compile 让模板成为具体 SQL,不等于执行这份模型、写入最新报表;某些宏仍可能在编译阶段访问数据库,不能把 compile 理解为保证完全离线。
dbt Core 的 target/compiled/ 便于检查查询,模型运行后 target/run/ 还可帮助观察包装后的建表等语句。不要手动修改这些生成文件:下次运行会覆盖它们,维护对象应是 models 里的源文件。
一次受控的刷新实验
- 把 CSV 中订单 101 的 amount_cents 从 1200 改成 1300。
- 仅执行 seed,不执行 build,然后预览:
uv run dbt seed --profiles-dir .
uv run dbt show --profiles-dir . --inline "select amount_cents from {{ ref('stg_orders') }} where order_id = 101"
uv run dbt show --profiles-dir . --inline "select revenue_cents from {{ ref('customer_summary') }} where customer_id = 1"
staging 视图应读到 1300,汇总表仍是 1200。源数据改变不会自动刷新表。此刻运行对账测试会失败,原因是结果尚未刷新,并非 SQL 写错。
- 执行
uv run dbt build --profiles-dir . --exclude resource_type:seed,汇总表才会变成 1300,测试也应通过。 - 恢复 CSV 中的 1200,依次 seed、build,将主线总额恢复为 5200 分。
独立练习:如果有人只执行 compile 就宣称报表已经刷新,你会用什么查询验证?为什么上游测试失败也不等于整个项目回滚?
参考答案
继续使用上面查询 customer_summary 中客户 1 金额的命令,并与输入的 101 对照。compile 生成 SQL;run/build 才构建模型。
dbt build 按依赖执行多个任务,已经成功执行的上游可能发生变化。下游 table 被跳过时可能保留旧结果,而下游 view 可能直接读到已经变化的上游。因此测试失败后要停止交付并调查,不能假设这是一次整个项目的原子回滚。
若物化没有变化,检查 YAML 中项目名是否为 shop_dbt、目录是否是 marts;若编译文件不存在,确认 compile 成功并检查路径中的项目名。数值与上课不符时,先确认 CSV 是否已恢复,而不是继续修改 SQL。
本课保留新的项目配置,customer_summary 为 table,staging 为 view。第 10 课只重构表达式,不改变这套物化选择。