dbt-core — 把 SQL 当工程代码写,让数据仓库里的转换跑起来
是什么
dbt(data build tool)是一个用 Python 写的命令行工具,它让数据分析师只用 SELECT 语句就能在数据仓库里做”转换”,并配套版本控制、测试、文档、依赖图。
日常类比:把数据仓库想成一个装修工地。原始数据是从供应商运来的板材、瓷砖、电线(已经被 Fivetran / Airbyte 这种”卸货卡车”卸到工地)。dbt 是工地的施工管理软件——每个 SQL 文件是一道工序图纸,ref() 是工序之间的先后关系,dbt 看完所有图纸自动排施工顺序、自动验收(测试)、自动出竣工报告(文档)。它自己不抡锤——锤子是数据仓库(Snowflake / BigQuery / Redshift / DuckDB)。
写起来是这样:
-- models/marts/orders_daily.sqlSELECT date_trunc('day', created_at) AS day, count(*) AS orders, sum(amount) AS revenueFROM {{ ref('stg_orders') }} -- 上游模型WHERE status = 'paid'GROUP BY 1写完跑 dbt run,dbt 把这条 SELECT 包成 CREATE TABLE orders_daily AS ... 发给仓库执行,并把它接到 stg_orders 后面成为 DAG 一节。
为什么重要
不理解 dbt,下面这些事就解释不通:
- 为什么 2018-2024 数据团队从”写一堆 cron+Python 脚本”突然转向”写 SQL + 提 PR + 跑 CI 测试”——dbt 把分析工作搬进了软件工程的工作流
- 为什么 Snowflake / BigQuery 占领市场后,仓内转换(in-warehouse T)能把 Spark/Hadoop 的 T 这一步挤下去——dbt 让 SQL 重新变成主力
- 为什么 2020 后 LinkedIn 上冒出”Analytics Engineer”这个新岗位——介于分析师和数据工程师之间,写 dbt 模型为生
- 为什么 OpenLineage / Datafold / Elementary 这些血缘和数据观测工具,第一个支持的格式都是 dbt 编译产物
manifest.json
核心要点
dbt 的世界由七个东西咬合:
- Model:一个
.sql文件,内容是一条 SELECT。dbt 帮你包成CREATE TABLE / VIEW,文件名就是表名。 - ref() 和 source():Jinja 函数。
{{ ref('stg_orders') }}解析时变成具体表名,dbt 同时记下”本模型依赖 stg_orders”——DAG 就是从所有 ref 关系扫出来的。 - Materialization:决定 SELECT 怎么落地。
view(每次查询时算)、table(dbt run 时算一次存下来)、incremental(只算新增行 merge 进去)、ephemeral(不落地,编译进下游 SQL 当 CTE)。 - Tests:在
schema.yml声明not_null/unique/relationships/accepted_values,或写自定义 SQL 测试。dbt test把每条测试编译成一条SELECT count(*) FROM ... WHERE 违规条件,行数 > 0 就算失败。 - Macros:用 Jinja 写的 SQL 函数,复用 SQL 片段。
dbt-utils这类 packages 就是社区 macros 集合。 - Snapshots:用 SCD-2 风格捕获缓变维度——给一张会被原地更新的源表加上”何时生效、何时失效”两列,留下历史。
- Adapter:抽象不同仓库方言的插件层。
dbt-snowflake/dbt-bigquery/dbt-postgres/dbt-duckdb——dbt-core 只管编译和 DAG,怎么连库、怎么 merge、怎么建表全交给 adapter。
整个流程的关键是:dbt 自己不算数据,它只把 .sql + Jinja 编译成纯 SQL,把这条 SQL 发给仓库执行。所以 dbt 跑得快不快,几乎只看你的仓库跑得快不快。
实践案例
案例 1:一个最小项目
models/ staging/ stg_orders.sql -- SELECT * FROM raw.orders WHERE deleted_at IS NULL stg_customers.sql marts/ customer_orders.sql -- JOIN 上面两个 stg_*schema.yml -- 声明每个 model 的列 + 测试dbt_project.yml -- 项目配置(仓库连接、materialization 默认值)跑 dbt run → 三个表按依赖顺序建好;跑 dbt test → 所有声明的测试一起执行;跑 dbt docs generate → 生成静态网站,含 DAG 图和列级文档。
案例 2:incremental 模型避免重算全表
{{ config(materialized='incremental', unique_key='event_id') }}SELECT * FROM {{ ref('raw_events') }}{% if is_incremental() %} WHERE event_at > (SELECT max(event_at) FROM {{ this }}){% endif %}第一次跑:全量建表。后续跑:只查新增行,按 event_id merge 进现有表。{{ this }} 是 dbt 注入的”我自己这张表”的引用。
案例 3:CI 里跑 dbt——分析师工作流的最大改变
PR 提交时,CI 在测试 schema 跑 dbt build --select state:modified+(只跑改过的 model + 它的下游),全部通过才能 merge。这条流程在 dbt 之前几乎不存在——分析师在仓库里改 SQL 没人 review、没人测试,凭手感。
案例 4:测试声明长这样
models: - name: stg_orders columns: - name: order_id tests: - unique - not_null - name: customer_id tests: - relationships: to: ref('stg_customers') field: customer_id - name: status tests: - accepted_values: values: [pending, paid, refunded]跑 dbt test,dbt 把每条声明展开成一条断言 SQL:SELECT count(*) FROM stg_orders WHERE order_id IS NULL,行数 > 0 就算这条测试失败。所有测试结果一起报告。
踩过的坑
- Jinja 调试痛:
{{ ref(...) }}出错时,报错往往在编译后的 SQL 里,不在源文件。看target/compiled/<project>/...找编译产物,对照行号。 - incremental 合并键写错 = 数据翻倍:
unique_key必须真唯一,否则 merge 不上、变成 append,悄悄重复。建议写完后立刻配unique测试。 - 测试默认全表扫:
unique、not_null默认SELECT count(*) FROM <整张表>,大表上很慢。用where:配置加过滤,或用dbt-utils.expression_is_true自定义。 - DAG 大了之后编译变慢:几百个 model 后,
dbt parse本身就要几十秒。社区的defer+state:modified+是标配——CI 只跑改过的部分。 - dbt-core 没有调度器:定时触发要外挂 Airflow / Dagster / dbt Cloud。新人常以为
dbt run自带 cron,结果生产上没人跑。
适用 vs 不适用场景
适用:
- 已有云数据仓库(Snowflake / BigQuery / Redshift / Databricks SQL)的团队做 T
- 数据团队想引入 git + PR + CI 工作流
- 需要给下游用户提供有文档、有测试、可追溯血缘的数据集市
不适用:
- 流式(streaming)数据——dbt 是批处理思维,分钟级以下不合适,看 Materialize / RisingWave
- 数据还没进仓库的阶段——dbt 不做 EL,只做 T;用 Fivetran / Airbyte / 自写 ingestion
- 仓库本身性能是瓶颈的场景——dbt 优化空间有限,因为它只是发 SQL,慢在仓库
历史小故事(可跳过)
- 2016 年:Tristan Handy 在波士顿开 Fishtown Analytics 咨询公司,给客户建数据仓库时反复手写一堆 ETL 脚本,受不了,写了个内部工具叫 dbt 帮自己加速。开源出来时只有 SQL + Jinja + 一个简单的 DAG。
- 2018 年:Snowflake / BigQuery 这类云仓库已经把性能解决了,分析师终于敢在仓库里直接做 T;dbt 踩着这波顺风开始扩散。
- 2020 年:Fishtown 改名 dbt Labs,融资数亿美元,发布 dbt Cloud 商业版。同年 Tristan 在博客里造词 Analytics Engineer,定义这个新岗位。
- 2022 年:dbt + Snowflake + Fivetran + Looker 被打包卖成 Modern Data Stack——这是 2022 数据圈最响的概念之一。dbt 是其中的 T。
学到什么
- 抽象的位置很关键:dbt 不重写仓库、不替代 SQL,它只在 SQL 之外加了一层”工程化壳”——版本、依赖、测试、文档。这一层就足以改变整个行业。
- 编译 + 委托执行 是个朴素但强力的范式——dbt、Terraform、Pulumi 都是这套:源文件不直接生效,先编译成目标平台的语言,再让目标平台执行。
- 工具能创造岗位:Analytics Engineer 这个 title 几乎是 dbt 一手催生的。工具改变了哪些人能干哪些事。
- 开源 + SaaS 双轨:dbt-core 免费撑生态,dbt Cloud 收 SaaS 钱(IDE / 调度 / SSO)——是 2018 年后开源商业化的标准玩法。
- 承认自己不做的事:dbt 明确拒绝做调度、做 ingestion、做 BI——这种”狭窄定义”反而让它在生态里站稳。同期一些”什么都做”的工具反而活得艰难。
延伸阅读
- 官方教程:dbt Learn — Fundamentals(免费,4 小时建一个完整项目)
- 创始人长文:Tristan Handy — The Modern Data Experience
- 源码:dbt-labs/dbt-core(Python,看
core/dbt/parser/理解 ref 解析) - 对照阅读:SQLMesh 和 dbt 的差异(增量推断、Python model)