ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从零开始掌握dbt-core:构建现代数据栈的可靠转换层

2026/8/6 1:21:53 拓冰建站 浏览量
从零开始掌握dbt-core:构建现代数据栈的可靠转换层 1. 为什么是 dbt一个数据工程师的视角转变如果你和我一样在数据仓库里摸爬滚打了好几年那你一定经历过这样的场景凌晨两点被电话叫醒原因是某个核心报表的数据对不上。你睡眼惺忪地打开终端连上数据库开始在一堆名为transform_final_v2.sql、transform_final_v3_fixed.sql的文件里试图理清业务逻辑和数据流向。更头疼的是你根本不敢轻易修改这些 SQL因为你不知道下游有多少张表、多少个看板依赖着它。这种“黑盒”式的、文档缺失的、测试靠人肉的 SQL 代码库是很多数据团队的常态也是数据质量事故的温床。dbtData Build Tool的出现正是为了解决这个核心痛点。它不是一个全新的数据库也不是一个可视化 ETL 工具。你可以把它理解为一个专为数据分析师和数据工程师设计的“SQL 编译器”和“项目管理框架”。它的核心思想是将软件工程的最佳实践引入到数据转换Tranformation这个环节。简单说dbt 让你能用写软件的方式去写 SQL从而获得版本控制、模块化、测试、文档化和依赖管理的能力。我最初接触 dbt-coredbt 的开源核心版本时感觉像是给杂乱无章的 SQL 脚本世界带来了一整套严谨的“交通规则”和“施工标准”。为什么现在要学 dbt因为现代数据栈Modern Data Stack的理念已经深入人心其核心之一就是“ELT”而非“ETL”。数据先被快速、原始地加载Extract Load到云数据仓库如 Snowflake, BigQuery, Redshift中然后在仓库内部进行转换Transform。dbt 正是这个“T”环节的绝对标准。掌握了 dbt你就能以更高效、更可靠的方式在数据仓库中构建清晰、可信的数据模型告别“脚本炼狱”。这篇教程我将带你从零开始手把手搭建一个 dbt-core 的本地开发环境并完成你的第一个数据模型项目让你亲身体会这种工作流带来的变革。2. 环境准备不仅仅是安装一个包在真正运行pip install dbt-core之前我们需要先理清 dbt 的运行环境。dbt-core 是一个 Python 包但它更像一个“指挥官”它需要连接到一个实际的数据平台适配器去执行 SQL。因此环境准备分为两部分Python 环境和数据平台连接。2.1 Python 环境与虚拟隔离强烈建议使用虚拟环境来管理 dbt 的依赖。这能避免与你系统上其他 Python 项目的包版本冲突。我习惯用venv简单直接。# 1. 创建项目目录并进入 mkdir my_first_dbt_project cd my_first_dbt_project # 2. 创建 Python 虚拟环境 python -m venv dbt_venv # 3. 激活虚拟环境 # 在 macOS/Linux 上 source dbt_venv/bin/activate # 在 Windows 上 # dbt_venv\Scripts\activate # 激活后命令行提示符前通常会显示 (dbt_venv)接下来安装 dbt-core。但请注意dbt-core本身不包含任何适配器。你必须根据你要连接的数据仓库安装对应的适配器包。例如dbt-snowflake用于 Snowflakedbt-bigquery用于 Google BigQuerydbt-redshift用于 Amazon Redshiftdbt-postgres用于 PostgreSQL为了教程的通用性我们选择dbt-postgres因为它可以本地部署无需云账户。如果你本地没有 PostgreSQL强烈建议使用 Docker 快速启动一个。# 安装 dbt-core 及 PostgreSQL 适配器 pip install dbt-core dbt-postgres安装完成后运行dbt --version检查是否成功。你会看到 dbt-core 的版本号以及已安装的适配器列表。2.2 目标数据平台配置profiles.yml 详解这是 dbt 配置中最关键的一步也是最容易出错的地方。dbt 通过一个名为profiles.yml的配置文件来管理所有数据仓库的连接信息。这个文件默认位于~/.dbt/目录下Windows 在C:\Users\用户名\.dbt\。让我们手动创建这个文件和目录mkdir -p ~/.dbt touch ~/.dbt/profiles.yml用文本编辑器打开profiles.yml。它的结构是这样的my_first_dbt_project: # 这是 profile 名称通常与项目名一致 target: dev # 默认使用的环境target outputs: dev: # 环境名称如 dev开发、prod生产 type: postgres # 适配器类型 host: localhost # 数据库主机 user: your_username # 数据库用户名 pass: your_password # 数据库密码 port: 5432 # 数据库端口 dbname: your_database # 数据库名称 schema: dbt_tutorial # 模式Schemadbt 将在此模式下创建表/视图 threads: 4 # 同时运行模型的最大线程数关键点与避坑指南type字段必须与你安装的适配器包名后半部分严格对应如postgres,snowflake。写错会导致连接失败。schema字段这非常重要。在 dbt 中schema类似于一个项目的工作空间或命名空间。所有由 dbt 生成的表、视图都会创建在这个 schema 下。建议为每个项目或环境使用独立的 schema如dbt_project_name_dev。密码安全直接将密码明文写在配置文件中有安全风险。对于生产环境dbt 支持使用环境变量。例如可以将pass: “{{ env_var(‘DBT_PASSWORD’) }}”然后在运行前设置环境变量。多环境配置你可以在outputs下配置多个环境如dev,prod通过dbt run --target prod来切换。这为开发、测试、生产环境隔离提供了便利。配置好profiles.yml后可以使用dbt debug命令来测试配置是否正确。这个命令会检查配置文件、网络连接和依赖项并给出详细的诊断信息。首次运行时务必仔细阅读其输出。3. 初始化你的第一个 dbt 项目脚手架与核心概念环境配置妥当后我们就可以创建 dbt 项目了。dbt 提供了一个初始化命令它会生成一个标准化的项目结构。# 确保在项目目录 my_first_dbt_project 下且虚拟环境已激活 dbt init my_first_dbt_project运行命令后它会询问你使用哪个 profile选择我们刚在profiles.yml里配置的my_first_dbt_project以及使用哪个 target选择dev。之后你会得到一个如下结构的目录my_first_dbt_project/ ├── analyses/ # 存放探索性、一次性的 SQL 查询 ├── macros/ # 存放可复用的 Jinja 代码块函数 ├── models/ # **核心目录**存放所有的数据模型 │ └── example/ # 示例模型 │ ├── my_first_dbt_model.sql │ ├── my_second_dbt_model.sql │ └── schema.yml # 模型配置、测试和文档 ├── seeds/ # 存放需要手动加载的静态数据CSV ├── snapshots/ # 存放实现缓慢变化维度SCD逻辑的 SQL ├── tests/ # 存放自定义的通用测试非必须 ├── .gitignore # Git 忽略文件 ├── dbt_project.yml # **项目根配置文件** └── README.md现在我们来解剖两个最核心的文件dbt_project.yml和模型文件。3.1 dbt_project.yml项目的控制中心这个文件定义了项目的全局配置。# dbt_project.yml name: my_first_dbt_project version: ‘1.0.0’ config-version: 2 profile: ‘my_first_dbt_project’ # 指定使用的 profile 名称 model-paths: [“models”] # 模型文件路径 analysis-paths: [“analyses”] test-paths: [“tests”] seed-paths: [“seeds”] macro-paths: [“macros”] snapshot-paths: [“snapshots”] target-path: “target” # dbt 运行日志、编译后 SQL 的输出目录 clean-targets: # 清理目录 - “target” - “dbt_packages” models: my_first_dbt_project: # 项目名下的模型配置 example: # 对应 models/example/ 子目录 materialized: view # 配置此目录下所有模型的物化方式为‘视图’关键配置解析profile必须与profiles.yml中的 profile 名称一致。model-paths等定义了 dbt 去哪里寻找不同类型的文件。你可以自定义这些路径。models: ... materialized这是 dbt 的一个核心概念——物化策略。它决定了 dbt 如何将你的 SQL 模型持久化到数据库中。主要有四种view视图默认选项。运行dbt run时会创建一个 SQL 视图。优点是轻量、实时缺点是每次查询都会重新计算底层 SQL对复杂模型可能性能不佳。table表运行dbt run时会创建一张物理表。优点是查询性能好缺点是占用存储且需要定期更新全量或增量。incremental增量表高级模式。首次运行创建全量表后续运行只插入或更新新增或变化的数据。适用于大数据量表是平衡性能和成本的关键。ephemeral临时模型不会被持久化到数据库而是像 CTE公共表表达式一样被内联到引用它的模型中。用于组织复杂逻辑不产生实际表/视图。在项目配置中你可以在不同层级全局、目录、单个模型覆盖这个配置。3.2 解剖一个基础模型SQL Jinja让我们打开models/example/my_first_dbt_model.sql{{ config(materialized‘table’) }} with source_data as ( select 1 as id, ‘Alice’ as name union all select 2 as id, ‘Bob’ as name ) select * from source_data这不仅仅是 SQL{{ config(...) }}这是Jinja语法。dbt 使用 Jinja2 模板引擎将 SQL 文件“编译”成可执行的 SQL。这里的config是一个特殊的 Jinja 函数用于设置该模型的配置。它覆盖了项目文件中materialized: view的配置指定此模型物化为表。模型就是 SELECT 语句一个 dbt 模型文件的核心就是一个SELECT语句。它定义了数据的转换逻辑。这个SELECT语句的查询结果就是最终要物化创建为视图或表的数据集。那么这个模型最终在数据库中会变成什么当你运行dbt run时dbt 会做以下几件事读取my_first_dbt_model.sql。用 Jinja 引擎处理其中的{{ config() }}等语句。生成一段纯粹的、针对目标数据库的 SQL。对于此模型生成的 SQL 大致是create table if not exists dbt_tutorial.my_first_dbt_model as ( with source_data as (... select ...) select * from source_data )在你在profiles.yml中配置的数据库your_database和模式dbt_tutorial下创建一张名为my_first_dbt_model的表。4. 运行、测试与文档化构建可信的数据流水线有了模型我们就可以让 dbt 动起来了。4.1 首次运行与依赖管理在项目根目录下执行dbt run你会看到 dbt 开始执行检查profiles.yml连接。解析项目中的所有模型文件。根据模型间的引用关系通过{{ ref() }}函数下文详述构建一个有向无环图DAG。按照 DAG 的依赖顺序依次编译 SQL 并在数据库中执行创建或更新表/视图。运行成功后连接到你的 PostgreSQL 数据库查看dbt_tutorialschema你应该能看到一张名为my_first_dbt_model的表里面有两行数据。现在让我们创建一个有依赖关系的模型。编辑models/example/my_second_dbt_model.sql{{ config(materialized‘view’) }} -- 使用 ref() 函数引用另一个模型 select id, name, ‘Hello, ‘ || name as greeting from {{ ref(‘my_first_dbt_model’) }}核心魔法{{ ref() }}函数这是 dbt 中最重要的函数之一。它用于声明模型之间的依赖关系。ref(‘my_first_dbt_model’)会被 dbt 编译成数据库中的完全限定名如dbt_tutorial.my_first_dbt_model。好处1可移植性你的代码里不写死数据库名和模式名。即使换一个环境如从dev换到prodschema 变了dbt 会自动帮你替换代码无需修改。好处2依赖管理dbt 能通过ref()函数自动分析出执行顺序。在上例中dbt 知道必须先创建my_first_dbt_model才能创建my_second_dbt_model。好处3完整性检查如果你错误地引用了一个不存在的模型dbt run或dbt compile阶段就会报错而不是等到数据库执行时才出错。再次运行dbt run。这次 dbt 会识别到my_second_dbt_model依赖于my_first_dbt_model因此按正确顺序执行。你会在数据库中看到一个基于第一个模型创建的视图。4.2 为数据添加测试质量保障的基石数据不准一切白搭。dbt 内置了一个强大且易用的测试框架。测试写在schema.yml文件中或其他.yml文件但schema.yml是约定俗成的名称。打开models/example/schema.ymlversion: 2 models: - name: my_first_dbt_model description: “A starter dbt model” columns: - name: id description: “The primary key for this table” tests: - unique - not_null - name: name description: “The name of the user” tests: - not_null这个 YAML 文件做了两件事添加文档description字段用于描述模型和列这些描述后续可以生成数据文档。定义测试在columns下为id列添加了unique唯一性和not_null非空测试为name列添加了not_null测试。运行测试dbt testdbt 会为每个测试生成并执行一条 SQL 查询。例如unique测试的查询本质是select count(distinct id) as total_unique, count(id) as total from model group by id having total_unique ! total。如果查询返回任何行则测试失败。测试的类型通用测试Generic Tests如上所述的unique,not_null还有accepted_values允许值列表、relationships外键关系基于ref()等。它们是声明式的配置简单。自定义测试Singular Tests在tests/目录下写一个.sql文件其查询如果返回任何行则表示测试失败。这给了你最大的灵活性可以测试任何业务逻辑例如“今日订单金额必须为正数”。实操心得测试策略不要试图为每一列都加上所有测试这会导致运行缓慢。我的经验是关键标识列如主键必须uniquenot_null。核心业务列如金额、数量、日期必须not_null。状态枚举列使用accepted_values确保数据清洁。外键列使用relationships测试确保引用完整性。 将测试视为数据合同的“断言”优先保障核心业务逻辑的正确性。4.3 生成与查看数据文档让数据资产一目了然清晰的文档是数据可用的前提。dbt 可以基于你的项目代码和schema.yml中的描述自动生成一个完整的静态网站作为数据文档。# 首先运行命令生成文档所需的物料 dbt docs generate # 然后启动一个本地服务器查看文档 dbt docs serve执行dbt docs serve后浏览器会自动打开http://localhost:8080。你会看到一个包含以下内容的网站项目概览显示所有模型、测试、源等的依赖关系图DAG。这个可视化图谱极其强大你可以清晰地看到数据是如何从源头经过层层模型转换的。模型详情点击任何一个模型可以看到它的代码、物化类型、列信息、描述以及它所有的上游依赖和下游依赖。测试详情查看每个测试覆盖了哪些模型和列。血缘分析追踪任一列数据的来源和去向。这个自动生成的文档站是 dbt 项目价值的集中体现。它让数据管道从“黑盒”变成了“白盒”新同事 onboarding 时不再需要口口相传或翻阅陈年 Wiki直接看文档站就能理解数据是如何产生的。这也是 dbt 倡导的“Docs as Code”理念的实践。5. 进阶模式与项目实战要点掌握了基础运行和测试后我们可以探讨一些更接近真实项目的模式。5.1 使用 Sources 定义数据源之前的模型直接使用了{{ ref() }}引用其他 dbt 模型。但对于最原始的数据比如从业务数据库同步过来的表我们应该使用{{ source() }}函数来定义。首先在项目根目录或models/下创建一个sources.yml文件version: 2 sources: - name: raw_data # 数据源名称 database: raw_db # 数据库名可被profile覆盖 schema: public # 模式名 tables: - name: users # 原始表名 description: “Raw users table from production database” - name: orders description: “Raw orders table”然后在模型中可以这样引用{{ config(materialized‘table’) }} select user_id, count(*) as order_count from {{ source(‘raw_data’, ‘orders’) }} -- 引用源source(‘源名称’ ‘表名称’) group by 1为什么用 Source清晰的血缘在文档站中source会被特殊标记让你一眼区分“外部原始数据”和“内部衍生模型”。集中管理如果原始表名或模式发生变化只需在sources.yml中修改一处。源数据测试你可以在sources.yml中为原始表的列添加测试确保进入 dbt 管道的数据质量基线。5.2 增量模型处理大数据量的利器当事实表数据量巨大时每次全量重建table成本高昂。这时需要使用incremental物化策略。{{ config( materialized‘incremental’, unique_key‘order_id’ ) }} select order_id, customer_id, order_amount, order_date from {{ source(‘raw_data’, ‘orders’) }} {% if is_incremental() %} -- 仅当增量运行时添加此 WHERE 条件 where order_date (select max(order_date) from {{ this }}) {% endif %}关键配置与逻辑materialized: ‘incremental’声明为增量模型。unique_key: ‘order_id’指定唯一键dbt 用它来合并数据如果某order_id已存在则更新该行否则插入。{{ is_incremental() }}这是一个 Jinja 宏在首次全量运行时为false在后续增量运行时为true。{{ this }}代表当前模型在数据库中的最终表名。首次运行dbt run会执行完整的SELECT语句创建全量表。后续运行dbt run会执行SELECT ... WHERE order_date (max_date)只获取新增数据然后根据unique_key与目标表进行合并MERGE 或 DELETEINSERT。避坑指南增量模型的挑战时间戳可靠性增量逻辑严重依赖order_date这类字段的准确性和单调递增性。如果数据回填或时间戳有误会导致数据遗漏或重复。唯一键选择unique_key必须能唯一标识一行。复合主键可以写成unique_key [‘id’, ‘date’]。性能考量WHERE子句中的子查询(select max(...) from {{ this }})在大表上可能较慢。有时需要创建单独的增量标识表来优化。Schema 变更如果向增量模型添加新列需要确保 SQL 能处理历史数据中该列为 NULL 的情况并且要考虑数据库 MERGE 语句对列变更的支持。5.3 宏实现 SQL 逻辑的复用当你发现多段 SQL 代码在做同样的事情时就该考虑使用宏了。宏类似于编程中的函数存放在macros/目录下。例如创建一个macros/standardize_phone.sql{% macro standardize_phone(phone_column) %} -- 移除所有非数字字符 regexp_replace({{ phone_column }}, ‘[^0-9]’, ‘’, ‘g’) {% endmacro %}在模型中可以这样使用select user_id, {{ standardize_phone(‘raw_phone_number’) }} as phone_number from {{ source(‘raw_data’, ‘users’) }}dbt 还内置了许多实用的宏比如{{ dbt_utils.surrogate_key([‘field_a’, ‘field_b’]) }}需要安装dbt-utils包可以生成代理键。宏极大地提升了代码的复用性和可维护性。6. 开发工作流与生产部署思考6.1 本地开发循环一个高效的 dbt 本地开发流程通常是新建或编辑模型在models/下创建.sql和.yml文件。快速编译检查运行dbt compile。这不会执行 SQL但会检查 Jinja 语法、ref/source引用是否正确并将模板编译为纯 SQL 输出到target/目录。你可以查看生成的 SQL 是否符合预期。运行特定模型使用dbt run --select model_name只运行某一个模型快速验证逻辑。结合--full-refresh可以强制全量重建。运行及测试运行dbt run构建模型然后dbt test执行测试。生成文档dbt docs generate更新文档并通过dbt docs serve本地查看血缘和描述。6.2 版本控制与协作dbt 项目天生适合 Git所有模型、宏、测试、文档定义都是纯文本文件SQL 和 YAML。dbt_project.yml和profiles.yml排除密码定义了整个项目。通过 Git 分支进行功能开发通过 Pull Request 进行代码审查。在 PR 中可以清晰地看到 SQL 逻辑的变更。一个常见的协作规范是禁止直接在main分支上修改模型。任何更改都通过特性分支提交并通过 CI/CD 流程进行自动化测试和部署。6.3 生产部署模式对于生产环境有几种常见的部署模式CI/CD 流水线最推荐的方式。使用 Jenkins, GitLab CI, GitHub Actions 等工具。当代码合并到主分支时自动触发流水线执行dbt run和dbt test。可以将生产环境的数据库连接信息配置在 CI/CD 系统的环境变量中。调度器集成使用 Apache Airflow, Dagster, Prefect 等调度工具。将dbt run作为一个任务节点嵌入到更复杂的数据管道中。这种方式便于管理跨系统如数据摄取、dbt 转换、BI 刷新的依赖和调度。dbt Clouddbt Labs 提供的托管服务。它提供了 Web IDE、调度、日志查看、文档托管和基于 PR 的部署流程开箱即用适合不想自建基础设施的团队。无论哪种方式核心原则是将配置尤其是密码与代码分离使用环境变量管理不同环境的连接信息。从零开始接触 dbt-core最大的感受是它用一种优雅的方式将数据转换的“手工作坊”升级为了“标准化工厂”。它强迫你思考依赖、思考测试、思考文档而这些正是构建可靠、可维护数据资产的基石。刚开始可能会觉得多了一些“约束”但当你需要回溯数据血缘、快速定位问题、或者让新同事理解复杂的业务逻辑时你会庆幸当初选择了这条更规范的路。