关于 dbt-data-transformation
这是一份面向分析工程师的 dbt 指南,介绍如何构建数据仓库转换流水线,涵盖模块化 SQL 模型、数据质量测试、文档、增量处理和 Jinja 宏。
主要功能
- 01SQL 模型与依赖管理
- 02视图、表和增量构建
- 03模式与自定义数据测试
- 04模型文档与血缘关系
- 05Jinja 宏与可复用逻辑
适用场景
- 01构建数据仓库 ELT 转换
- 02测试数据质量与关系
- 03增量处理大型数据集
- 04编写模型文档并追踪依赖
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ manutej/ luxor-claude-marketplace --skill dbt-data-transformation安装后在 Claude Code 中输入/dbt-data-transformation确认已加载。
附带文件EXAMPLES.mdREADME.md
详细介绍
dbt 数据转换
一项全面的技能,帮助你精通用于分析工程的 dbt(data build tool)。此技能涵盖模型开发、测试策略、文档实践、增量构建、Jinja 模板、宏开发、包管理以及生产部署工作流。
何时使用此技能
在以下情况下使用此技能:
- 为分析和商业智能构建数据转换流水线
- 使用模块化、可测试的 SQL 创建数据仓库
- 实施 ELT(提取、加载、转换)工作流
- 开发用于分析的维度模型(事实表、维度表)
- 管理复杂的 SQL 依赖关系和数据血缘
- 在多个项目中创建可复用的数据转换逻辑
- 测试数据质量并实施数据契约
- 编写数据模型和业务逻辑的文档
- 为大型数据集构建增量模型
- 使用 Airflow、Dagster 或 dbt Cloud 等工具编排 dbt
- 将旧版 ETL 流程迁移到现代 ELT 架构
- 为分析团队实施 DataOps 实践
核心概念
什么是 dbt?
dbt(data build tool)让分析工程师能够更有效地转换数据仓库中的数据。它是一种开发框架,将软件工程最佳实践引入数据转换:
- 版本控制:将 SQL 转换作为代码存储在 Git 中
- 测试:内置数据质量测试框架
- 文档:自动生成、可搜索的数据字典
- 模块化:通过 refs 和宏复用 SQL
- 血缘:自动解析和可视化依赖关系
- 部署:用于数据转换的 CI/CD
dbt 工作流
1. Develop: Write SQL SELECT statements as models
2. Test: Define data quality tests
3. Document: Add descriptions and metadata
4. Build: dbt run compiles and executes models
5. Test: dbt test validates data quality
6. Deploy: CI/CD pipelines deploy to production
dbt 的关键实体
- 模型:定义数据转换的 SQL SELECT 语句
- 源:数据仓库中的原始数据表
- 种子:加载到数据仓库中的 CSV 文件
- 测试:数据质量断言
- 宏:可复用的 Jinja-SQL 函数
- 快照:捕获 2 型缓慢变化维度
- 暴露项:dbt 模型的下游用途(仪表板、ML 模型)
- 指标:业务指标定义
模型开发
基本模型结构
dbt 模型是保存为 .sql 文件的 SELECT 语句:
-- models/staging/stg_orders.sql
with source as (
select * from {{ source('jaffle_shop', 'orders') }}
),
renamed as (
select
id as order_id,
user_id as customer_id,
order_date,
status,
_etl_loaded_at
from source
)
select * from renamed
要点:
- 模型只能包含 SELECT 语句(不含 DDL)
- 使用 CTE(通用表表达式)提高可读性
- 使用
{{ source() }}引用源 - dbt 根据物化方式处理 CREATE/INSERT 逻辑
ref() 函数
使用 {{ ref() }} 引用其他模型:
-- models/marts/fct_orders.sql
with orders as (
select * from {{ ref('stg_orders') }}
),
customers as (
select * from {{ ref('stg_customers') }}
),
joined as (
select
orders.order_id,
orders.order_date,
customers.customer_name,
orders.status
from orders
left join customers
on orders.customer_id = customers.customer_id
)
select * from joined
ref() 的优势:
- 自动构建依赖关系图
- 解析为正确的架构/数据库
- 可在开发环境中测试,而不影响生产环境
- 为血缘可视化提供支持
source() 函数
定义并引用原始数据源:
# models/staging/sources.yml
version: 2
sources:
- name: jaffle_shop
description: Raw data from the Jaffle Shop application
database: raw
schema: jaffle_shop
tables:
- name: orders
description: One record per order
columns:
- name: id
description: Primary key for orders
tests:
- unique
- not_null
- name: user_id
description: Foreign key to customers
- name: order_date
description: Date order was placed
- name: status
description: Order status (completed, pending, cancelled)
-- Reference the source
select * from {{ source('jaffle_shop', 'orders') }}
源的功能:
- 编写原始数据表的文档
- 测试源数据质量
- 使用
freshness配置跟踪新鲜度 - 将源定义与转换分开
模型组织
推荐的项目结构:
models/
├── staging/ # One-to-one with source tables
│ ├── jaffle_shop/
│ │ ├── _jaffle_shop__sources.yml
│ │ ├── _jaffle_shop__models.yml
│ │ ├── stg_jaffle_shop__orders.sql
│ │ └── stg_jaffle_shop__customers.sql
│ └── stripe/
│ ├── _stripe__sources.yml
│ ├── _stripe__models.yml
│ └── stg_stripe__payments.sql
├── intermediate/ # Purpose-built transformations
│ └── int_orders_joined.sql
└── marts/ # Business-defined entities
├── core/
│ ├── _core__models.yml
│ ├── dim_customers.sql
│ └── fct_orders.sql
└── marketing/
└── fct_customer_sessions.sql
命名约定:
stg_:暂存模型(与源一一对应)int_:中间模型(不向最终用户公开)fct_:事实表dim_:维度表
物化方式
物化方式决定 dbt 如何在数据仓库中构建模型:
1. 视图(默认)
{{ config(materialized='view') }}
select * from {{ ref('base_model') }}
特点:
- 轻量,不存储数据
- 每次访问视图时都会执行查询
- 最适用于:小型数据集、不常查询的模型
- 构建速度快,查询速度较慢
2. 表
{{ config(materialized='table') }}
select * from {{ ref('base_model') }}
特点:
- 每次运行都会完整重建表
- 数据以物理形式存储
- 最适用于:中小型数据集、频繁查询的模型
- 构建速度较慢,查询速度较快
3. 增量
{{ config(
materialized='incremental',
unique_key='order_id',
on_schema_change='fail'
) }}
select * from {{ source('jaffle_shop', 'orders') }}
{% if is_incremental() %}
-- Only process new/updated records
where order_date > (select max(order_date) from {{ this }})
{% endif %}
特性:
- 后续运行时仅处理新数据
- 首次运行会构建完整表
- 最适用于:大型数据集、事件/时间序列数据
- 增量构建速度快,并保留历史数据
增量策略:
-- Append (default): Add new rows only
{{ config(
materialized='incremental',
incremental_strategy='append'
) }}
-- Merge: Upsert based on unique_key
{{ config(
materialized='incremental',
unique_key='order_id',
incremental_strategy='merge'
) }}
-- Delete+Insert: Delete matching records, insert new
{{ config(
materialized='incremental',
unique_key='order_id',
incremental_strategy='delete+insert'
) }}
4. 临时
{{ config(materialized='ephemeral') }}
select * from {{ ref('base_model') }}
特性:
- 不在数据仓库中构建
- 在依赖模型中以内联 CTE 的形式插入
- 最适用于:轻量级转换、避免视图过多
- 不存储数据,会编译到下游模型中
物化方式对比
| 物化方式 | 构建速度 | 查询速度 | 存储 | 用例 |
|---|---|---|---|---|
| 视图 | 快 | 慢 | 无 | 小型数据集、查询频率低 |
| 表 | 慢 | 快 | 高 | 中型数据集、查询频率高 |
| 增量 | 快* | 快 | 高 | 大型数据集、时间序列数据 |
| 临时 | 不适用 | 不定 | 无 | 中间逻辑、CTE |
*首次完整构建后
测试
Schema 测试
在 YAML 中定义的内置通用测试:
# models/staging/stg_orders.yml
version: 2
models:
- name: stg_orders
description: Staged order data
columns:
- name: order_id
description: Primary key
tests:
- unique
- not_null
- name: customer_id
description: Foreign key to customers
tests:
- not_null
- relationships:
to: ref('stg_customers')
field: customer_id
- name: status
description: Order status
tests:
- accepted_values:
values: ['placed', 'shipped', 'completed', 'returned', 'cancelled']
- name: order_total
description: Total order amount
tests:
- not_null
- dbt_utils.expression_is_true:
expression: ">= 0"
内置测试:
unique:值不得重复not_null:值不得为 nullaccepted_values:值必须在指定列表中relationships:验证外键
自定义数据测试
在 tests/ 目录中创建自定义测试:
-- tests/assert_positive_order_totals.sql
select
order_id,
order_total
from {{ ref('fct_orders') }}
where order_total < 0
工作原理:
- 如果查询返回任何行,测试就会失败
- 查询应返回未通过测试的记录
- 可使用任意 SQL 逻辑
高级测试模式
-- Test for data freshness
-- tests/assert_orders_are_fresh.sql
with latest_order as (
select max(order_date) as max_date
from {{ ref('fct_orders') }}
)
select max_date
from latest_order
where max_date < current_date - interval '1 day'
-- Test for referential integrity across time
-- tests/assert_no_orphaned_orders.sql
select
o.order_id,
o.customer_id
from {{ ref('fct_orders') }} o
left join {{ ref('dim_customers') }} c
on o.customer_id = c.customer_id
where c.customer_id is null
使用 dbt_utils 进行测试
# Requires dbt-utils package
models:
- name: stg_orders
columns:
- name: order_id
tests:
# Test for uniqueness across multiple columns
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
- order_id
- order_date
# Test for sequential values
- dbt_utils.sequential_values:
interval: 1
# Test that values match regex
- dbt_utils.not_null_proportion:
at_least: 0.95
测试严重性级别
models:
- name: stg_orders
columns:
- name: order_id
tests:
- unique:
severity: error # Fail build (default)
- not_null:
severity: warn # Warning only
文档
模型文档
# models/marts/core/_core__models.yml
version: 2
models:
- name: fct_orders
description: |
Order fact table containing one row per order with associated
customer and payment information. This is the primary table for
order analytics and reporting.
**Grain:** One row per order
**Refresh:** Incremental, updates daily at 2 AM UTC
**Notes:**
- Includes cancelled orders (filter with status column)
- Payment info joined from Stripe data
- Customer info joined from application database
columns:
- name: order_id
description: Primary key for orders table
tests:
- unique
- not_null
- name: customer_id
description: |
Foreign key to dim_customers. Links to customer who placed the order.
**Note:** May be null for guest checkout orders.
- name: order_date
description: Date order was placed (UTC timezone)
- name: status
description: |
Current order status. Possible values:
- `placed`: Order received, not yet processed
- `shipped`: Order shipped to customer
- `completed`: Order delivered and confirmed
- `returned`: Order returned by customer
- `cancelled`: Order cancelled before shipment
- name: order_total
description: Total order amount in USD including tax and shipping
文档块
创建可重复使用的文档:
{% docs order_status %}
Order status indicates the current state of an order in our fulfillment pipeline.
| Status | Description | Next Steps |
|--------|-------------|------------|
| placed | Order received | Inventory check |
| shipped | En route to customer | Track shipment |
| completed | Delivered successfully | Request feedback |
| returned | Customer return initiated | Process refund |
| cancelled | Order cancelled | Update inventory |
{% enddocs %}
{% docs customer_id %}
Unique identifier for customers. This ID is:
- Generated by the application on account creation
- Persistent across orders
- Used to track customer lifetime value
- **Note:** NULL for guest checkouts
{% enddocs %}
引用文档块:
models:
- name: fct_orders
columns:
- name: status
description: "{{ doc('order_status') }}"
- name: customer_id
description: "{{ doc('customer_id') }}"
生成文档
# Generate documentation site
dbt docs generate
# Serve documentation locally
dbt docs serve --port 8001
# View in browser at http://localhost:8001
文档功能:
- 交互式血缘图(DAG 可视化)
- 可搜索的模型目录
- 列级文档
- 源数据新鲜度跟踪
- 测试覆盖情况可见
- 编译后的 SQL 预览
文档最佳实践
- 各个层级都编写文档:项目、模型、列、数据源
- 解释业务逻辑:说明转换存在的原因
- 明确界定粒度:一行代表……
- 注明刷新计划:数据更新频率
- 记录假设:边缘情况、已知问题
- 链接外部资源:Confluence、wiki、仪表板
增量模型
基本增量模式
{{ config(
materialized='incremental',
unique_key='event_id'
) }}
with source as (
select
event_id,
user_id,
event_timestamp,
event_type,
event_properties
from {{ source('analytics', 'events') }}
{% if is_incremental() %}
-- Only process events newer than existing data
where event_timestamp > (select max(event_timestamp) from {{ this }})
{% endif %}
)
select * from source
关键组件:
is_incremental():首次运行后为 True{{ this }}:引用当前模型的表unique_key:用于去重的列
使用 Merge 策略的增量模型
{{ config(
materialized='incremental',
unique_key='order_id',
incremental_strategy='merge',
merge_update_columns=['status', 'updated_at'],
merge_exclude_columns=['created_at']
) }}
with orders as (
select
order_id,
customer_id,
order_date,
status,
order_total,
current_timestamp() as updated_at,
case
when status = 'placed' then current_timestamp()
else null
end as created_at
from {{ source('ecommerce', 'orders') }}
{% if is_incremental() %}
-- Look back 3 days to catch late-arriving updates
where order_date >= (select max(order_date) - interval '3 days' from {{ this }})
{% endif %}
)
select * from orders
Merge 策略特性:
- 根据
unique_key更新现有记录 - 插入新记录
- 可选:指定要更新/排除的列
- 最适用于:缓慢变化的数据、历史记录更新
使用 Delete+Insert 的增量模型
{{ config(
materialized='incremental',
unique_key=['date', 'customer_id'],
incremental_strategy='delete+insert'
) }}
with daily_metrics as (
select
date_trunc('day', order_timestamp) as date,
customer_id,
count(*) as order_count,
sum(order_total) as total_revenue
from {{ ref('fct_orders') }}
{% if is_incremental() %}
where date_trunc('day', order_timestamp) >= (
select max(date) - interval '7 days' from {{ this }}
)
{% endif %}
group by 1, 2
)
select * from daily_metrics
Delete+Insert 策略:
- 删除所有与
unique_key匹配的行 - 插入新行
- 最适用于:聚合数据、完整分区替换
- 对批量更新而言,比 Merge 更高效
处理延迟到达的数据
{{ config(
materialized='incremental',
unique_key='order_id'
) }}
select
order_id,
customer_id,
order_date,
status,
_loaded_at
from {{ source('ecommerce', 'orders') }}
{% if is_incremental() %}
-- Use _loaded_at instead of order_date to catch updates
where _loaded_at > (select max(_loaded_at) from {{ this }})
-- OR use a lookback window
-- where order_date > (select max(order_date) - interval '3 days' from {{ this }})
{% endif %}
使用分区的增量模型
{{ config(
materialized='incremental',
unique_key='event_id',
partition_by={
'field': 'event_date',
'data_type': 'date',
'granularity': 'day'
},
cluster_by=['user_id', 'event_type']
) }}
select
event_id,
user_id,
event_type,
event_timestamp,
date(event_timestamp) as event_date
from {{ source('analytics', 'raw_events') }}
{% if is_incremental() %}
where date(event_timestamp) > (select max(event_date) from {{ this }})
{% endif %}
分区优势:
- 提升查询性能
- 优化成本(扫描更少数据)
- 高效进行增量处理
- 更适用于时间序列数据
完整刷新功能
# Force full rebuild of incremental models
dbt run --full-refresh
# Full refresh specific model
dbt run --select my_incremental_model --full-refresh
宏与 Jinja
基本宏结构
-- macros/cents_to_dollars.sql
{% macro cents_to_dollars(column_name, precision=2) %}
round({{ column_name }} / 100.0, {{ precision }})
{% endmacro %}
用法:
select
order_id,
{{ cents_to_dollars('amount_cents') }} as amount_dollars
from {{ ref('stg_orders') }}
可重复使用的数据质量宏
-- macros/test_not_negative.sql
{% macro test_not_negative(model, column_name) %}
select
{{ column_name }}
from {{ model }}
where {{ column_name }} < 0
{% endmacro %}
日期序列宏
-- macros/date_spine.sql
{% macro date_spine(start_date, end_date) %}
with date_spine as (
{{ dbt_utils.date_spine(
datepart="day",
start_date="cast('" ~ start_date ~ "' as date)",
end_date="cast('" ~ end_date ~ "' as date)"
) }}
)
select
date_day
from date_spine
{% endmacro %}
动态 SQL 生成
-- macros/pivot_metric.sql
{% macro pivot_metric(metric_column, group_by_column, values) %}
select
{{ group_by_column }},
{% for value in values %}
sum(case when status = '{{ value }}' then {{ metric_column }} else 0 end)
as {{ value }}_{{ metric_column }}
{% if not loop.last %},{% endif %}
{% endfor %}
from {{ ref('fct_orders') }}
group by 1
{% endmacro %}
用法:
{{ pivot_metric(
metric_column='order_total',
group_by_column='customer_id',
values=['completed', 'pending', 'cancelled']
) }}
授予权限的宏
-- macros/grant_select.sql
{% macro grant_select(schema, role) %}
{% set sql %}
grant select on all tables in schema {{ schema }} to {{ role }};
{% endset %}
{% do run_query(sql) %}
{% do log("Granted select on " ~ schema ~ " to " ~ role, info=True) %}
{% endmacro %}
在钩子中的用法:
# dbt_project.yml
on-run-end:
- "{{ grant_select(target.schema, 'analyst_role') }}"
特定环境逻辑
-- macros/generate_schema_name.sql
{% macro generate_schema_name(custom_schema_name, node) -%}
{%- set default_schema = target.schema -%}
{%- if target.name == 'prod' -%}
{%- if custom_schema_name is not none -%}
{{ custom_schema_name | trim }}
{%- else -%}
{{ default_schema }}
{%- endif -%}
{%- else -%}
{{ default_schema }}_{{ custom_schema_name | trim }}
{%- endif -%}
{%- endmacro %}
审计列宏
-- macros/add_audit_columns.sql
{% macro add_audit_columns() %}
current_timestamp() as dbt_updated_at,
current_timestamp() as dbt_created_at,
'{{ var("dbt_user") }}' as dbt_updated_by
{% endmacro %}
用法:
select
order_id,
customer_id,
order_total,
{{ add_audit_columns() }}
from {{ ref('stg_orders') }}
Jinja 控制结构
-- Conditionals
{% if target.name == 'prod' %}
from {{ source('production', 'orders') }}
{% else %}
from {{ source('development', 'orders') }}
{% endif %}
-- Loops
{% for status in ['placed', 'shipped', 'completed'] %}
sum(case when status = '{{ status }}' then 1 else 0 end) as {{ status }}_count
{% if not loop.last %},{% endif %}
{% endfor %}
-- Set variables
{% set payment_methods = ['credit_card', 'paypal', 'bank_transfer'] %}
{% for method in payment_methods %}
count(distinct case when payment_method = '{{ method }}'
then customer_id end) as {{ method }}_customers
{% if not loop.last %},{% endif %}
{% endfor %}
包管理
安装包
# packages.yml
packages:
# dbt-utils: Essential utility macros
- package: dbt-labs/dbt_utils
version: 1.1.1
# Audit helper: Compare datasets
- package: dbt-labs/audit_helper
version: 0.9.0
# Codegen: Code generation utilities
- package: dbt-labs/codegen
version: 0.11.0
# Custom package from Git
- git: "https://github.com/your-org/dbt-custom-package.git"
revision: main
# Local package
- local: ../dbt-shared-macros
安装包:
dbt deps
使用 dbt_utils
-- Surrogate key generation
select
{{ dbt_utils.generate_surrogate_key(['order_id', 'line_item_id']) }} as order_line_id,
order_id,
line_item_id
from {{ ref('stg_order_lines') }}
-- Union multiple tables
{{ dbt_utils.union_relations(
relations=[
ref('orders_2022'),
ref('orders_2023'),
ref('orders_2024')
]
) }}
-- Get column values as list
{% set statuses = dbt_utils.get_column_values(
table=ref('stg_orders'),
column='status'
) %}
-- Pivot table
{{ dbt_utils.pivot(
column='metric_name',
values=dbt_utils.get_column_values(table=ref('metrics'), column='metric_name'),
agg='sum',
then_value='metric_value',
else_value=0,
prefix='',
suffix='_total'
) }}
创建自定义包
包的项目结构:
dbt-custom-package/
├── dbt_project.yml
├── macros/
│ ├── custom_test.sql
│ └── custom_macro.sql
├── models/
│ └── example_model.sql
└── README.md
# dbt_project.yml for custom package
name: 'custom_package'
version: '1.0.0'
config-version: 2
require-dbt-version: [">=1.0.0", "<2.0.0"]
软件包版本控制
# Semantic versioning
packages:
- package: dbt-labs/dbt_utils
version: [">=1.0.0", "<2.0.0"] # Any 1.x version
# Exact version
- package: dbt-labs/dbt_utils
version: 1.1.1
# Git branch/tag
- git: "https://github.com/org/package.git"
revision: v1.2.3
# Latest from branch
- git: "https://github.com/org/package.git"
revision: main
生产工作流
CI/CD 流水线(GitHub Actions)
# .github/workflows/dbt_ci.yml
name: dbt CI
on:
pull_request:
branches: [main]
jobs:
dbt_run:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dbt
run: |
pip install dbt-core dbt-snowflake
- name: Install dbt packages
run: dbt deps
- name: Run dbt debug
run: dbt debug
env:
DBT_SNOWFLAKE_ACCOUNT: ${{ secrets.DBT_SNOWFLAKE_ACCOUNT }}
DBT_SNOWFLAKE_USER: ${{ secrets.DBT_SNOWFLAKE_USER }}
DBT_SNOWFLAKE_PASSWORD: ${{ secrets.DBT_SNOWFLAKE_PASSWORD }}
- name: Run dbt models (modified only)
run: dbt run --select state:modified+ --state ./prod_manifest
env:
DBT_SNOWFLAKE_ACCOUNT: ${{ secrets.DBT_SNOWFLAKE_ACCOUNT }}
DBT_SNOWFLAKE_USER: ${{ secrets.DBT_SNOWFLAKE_USER }}
DBT_SNOWFLAKE_PASSWORD: ${{ secrets.DBT_SNOWFLAKE_PASSWORD }}
- name: Run dbt tests
run: dbt test --select state:modified+
env:
DBT_SNOWFLAKE_ACCOUNT: ${{ secrets.DBT_SNOWFLAKE_ACCOUNT }}
DBT_SNOWFLAKE_USER: ${{ secrets.DBT_SNOWFLAKE_USER }}
DBT_SNOWFLAKE_PASSWORD: ${{ secrets.DBT_SNOWFLAKE_PASSWORD }}
精简 CI(仅测试已更改的模型)
# Store production manifest
dbt compile --target prod
cp target/manifest.json ./prod_manifest/
# In CI: Test only changed models and downstream dependencies
dbt test --select state:modified+ --state ./prod_manifest
生产环境部署
# .github/workflows/dbt_prod.yml
name: dbt Production Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dbt
run: pip install dbt-core dbt-snowflake
- name: Install packages
run: dbt deps
- name: Run dbt seed
run: dbt seed --target prod
- name: Run dbt run
run: dbt run --target prod
- name: Run dbt test
run: dbt test --target prod
- name: Generate docs
run: dbt docs generate --target prod
- name: Upload docs to S3
run: |
aws s3 sync target/ s3://dbt-docs-bucket/
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
使用 Airflow 编排
# dags/dbt_dag.py
from airflow import DAG
from airflow.operators.bash import BashOperator
from datetime import datetime, timedelta
default_args = {
'owner': 'analytics',
'depends_on_past': False,
'email_on_failure': True,
'email_on_retry': False,
'retries': 1,
'retry_delay': timedelta(minutes=5),
}
with DAG(
'dbt_production',
default_args=default_args,
description='Run dbt models in production',
schedule_interval='0 2 * * *', # 2 AM daily
start_date=datetime(2024, 1, 1),
catchup=False,
tags=['dbt', 'analytics'],
) as dag:
dbt_deps = BashOperator(
task_id='dbt_deps',
bash_command='cd /opt/dbt && dbt deps',
)
dbt_seed = BashOperator(
task_id='dbt_seed',
bash_command='cd /opt/dbt && dbt seed --target prod',
)
dbt_run_staging = BashOperator(
task_id='dbt_run_staging',
bash_command='cd /opt/dbt && dbt run --select staging.* --target prod',
)
dbt_run_marts = BashOperator(
task_id='dbt_run_marts',
bash_command='cd /opt/dbt && dbt run --select marts.* --target prod',
)
dbt_test = BashOperator(
task_id='dbt_test',
bash_command='cd /opt/dbt && dbt test --target prod',
)
dbt_docs = BashOperator(
task_id='dbt_docs',
bash_command='cd /opt/dbt && dbt docs generate --target prod',
)
# Define task dependencies
dbt_deps >> dbt_seed >> dbt_run_staging >> dbt_run_marts >> dbt_test >> dbt_docs
dbt Cloud 集成
# dbt_cloud.yml
# Environment configuration
environments:
- name: Production
dbt_version: 1.7.latest
type: deployment
- name: Development
dbt_version: 1.7.latest
type: development
# Job configuration
jobs:
- name: Production Run
environment: Production
triggers:
schedule:
cron: "0 2 * * *" # 2 AM daily
commands:
- dbt deps
- dbt seed
- dbt run
- dbt test
- name: CI Check
environment: Development
triggers:
github_webhook: true
commands:
- dbt deps
- dbt run --select state:modified+
- dbt test --select state:modified+
监控与告警
-- macros/post_hook_monitoring.sql
{% macro monitor_row_count(threshold=0) %}
{% if execute %}
{% set row_count_query %}
select count(*) as row_count from {{ this }}
{% endset %}
{% set results = run_query(row_count_query) %}
{% set row_count = results.columns[0].values()[0] %}
{% if row_count < threshold %}
{{ exceptions.raise_compiler_error("Row count " ~ row_count ~ " below threshold " ~ threshold) }}
{% endif %}
{{ log("Model " ~ this ~ " has " ~ row_count ~ " rows", info=True) }}
{% endif %}
{% endmacro %}
用法:
{{ config(
post_hook="{{ monitor_row_count(threshold=1000) }}"
) }}
select * from {{ ref('stg_orders') }}
最佳实践
命名约定
模型:
stg_[source]__[entity].sql # Staging: stg_stripe__payments.sql
int_[entity]_[verb].sql # Intermediate: int_orders_joined.sql
fct_[entity].sql # Fact: fct_orders.sql
dim_[entity].sql # Dimension: dim_customers.sql
测试:
assert_[description].sql # assert_positive_order_totals.sql
宏:
[verb]_[noun].sql # generate_surrogate_key.sql
SQL 风格指南
-- ✓ Good: Clear CTEs, proper formatting
with orders as (
select
order_id,
customer_id,
order_date,
status
from {{ ref('stg_orders') }}
where status != 'cancelled'
),
customers as (
select
customer_id,
customer_name,
customer_email
from {{ ref('dim_customers') }}
),
final as (
select
orders.order_id,
orders.order_date,
customers.customer_name,
orders.status
from orders
left join customers
on orders.customer_id = customers.customer_id
)
select * from final
-- ✗ Bad: Nested subqueries, poor formatting
select o.order_id, o.order_date, c.customer_name, o.status from (
select order_id, customer_id, order_date, status from {{ ref('stg_orders') }}
where status != 'cancelled') o left join (select customer_id, customer_name from
{{ ref('dim_customers') }}) c on o.customer_id = c.customer_id
性能优化
1. 对大型表使用增量模型
-- Process only new data
{{ config(materialized='incremental') }}
select * from {{ source('events', 'page_views') }}
{% if is_incremental() %}
where event_timestamp > (select max(event_timestamp) from {{ this }})
{% endif %}
2. 利用聚簇和分区
{{ config(
materialized='table',
partition_by={'field': 'order_date', 'data_type': 'date'},
cluster_by=['customer_id', 'status']
) }}
3. 减少扫描的数据量
-- ✓ Good: Filter early
with source as (
select *
from {{ source('app', 'events') }}
where event_date >= '2024-01-01' -- Filter in source CTE
)
-- ✗ Bad: Filter late
with source as (
select * from {{ source('app', 'events') }}
)
select * from source
where event_date >= '2024-01-01' -- Filtering after full scan
4. 对简单转换使用 ephemeral
-- Avoid creating unnecessary views
{{ config(materialized='ephemeral') }}
select
order_id,
lower(trim(status)) as status_clean
from {{ ref('stg_orders') }}
项目结构最佳实践
1. 分层组织转换
Staging → Intermediate → Marts
↓ ↓ ↓
1:1 Purpose-built Business
Sources Logic Entities
2. 将复杂逻辑模块化
-- Instead of one massive model, break it down:
-- intermediate/int_order_items_aggregated.sql
-- intermediate/int_customer_lifetime_value.sql
-- intermediate/int_payment_summaries.sql
-- marts/fct_orders.sql (combines intermediate models)
3. 采用一致的文件组织方式
models/
├── staging/
│ └── [source]/
│ ├── _[source]__sources.yml
│ ├── _[source]__models.yml
│ └── stg_[source]__[table].sql
├── intermediate/
│ └── int_[purpose].sql
└── marts/
└── [business_area]/
├── _[area]__models.yml
└── [model_type]_[entity].sql
测试策略
1. 在多个层级进行测试
# Source tests: Data quality at ingestion
sources:
- name: raw_data
tables:
- name: orders
columns:
- name: id
tests: [unique, not_null]
# Model tests: Transformation logic
models:
- name: fct_orders
tests:
- dbt_utils.expression_is_true:
expression: "order_total >= 0"
columns:
- name: order_id
tests: [unique, not_null]
# Custom tests: Business logic
# tests/assert_revenue_reconciliation.sql
2. 使用适当的测试严重级别
# Critical tests: error (fail build)
# Nice-to-have tests: warn (log but don't fail)
tests:
- unique:
severity: error
- dbt_utils.not_null_proportion:
at_least: 0.95
severity: warn
3. 测试覆盖率目标
- 100% 的主键:唯一性 + 非空
- 100% 的外键:关系测试
- 所有业务逻辑:自定义数据测试
- 关键计算:表达式测试
文档规范
1. 为每个模型编写文档
models:
- name: fct_orders
description: |
**Purpose:** [Why this model exists]
**Grain:** [One row represents...]
**Refresh:** [When and how often]
**Consumers:** [Who uses this]
2. 为复杂逻辑编写文档
-- Use comments for complex business rules
select
order_id,
-- Revenue recognition: Only count completed orders
-- cancelled within 30 days (per finance policy 2024-03)
case
when status = 'completed'
and datediff('day', order_date, current_date) > 30
then order_total
else 0
end as recognized_revenue
from {{ ref('stg_orders') }}
3. 保持文档更新
- 逻辑更改时更新文档
- 在代码审查期间检查文档
- 定期生成文档:
dbt docs generate
20 个详细示例
示例 1:基础暂存模型
-- models/staging/jaffle_shop/stg_jaffle_shop__customers.sql
with source as (
select * from {{ source('jaffle_shop', 'customers') }}
),
renamed as (
select
id as customer_id,
first_name,
last_name,
first_name || ' ' || last_name as customer_name,
email,
_loaded_at
from source
)
select * from renamed
示例 2:包含多个联接的事实表
-- models/marts/core/fct_orders.sql
{{ config(
materialized='table',
tags=['core', 'daily']
) }}
with orders as (
select * from {{ ref('stg_jaffle_shop__orders') }}
),
customers as (
select * from {{ ref('dim_customers') }}
),
payments as (
select
order_id,
sum(amount) as total_payment_amount
from {{ ref('stg_stripe__payments') }}
where status = 'success'
group by 1
),
final as (
select
orders.order_id,
orders.customer_id,
customers.customer_name,
orders.order_date,
orders.status,
coalesce(payments.total_payment_amount, 0) as order_total,
{{ add_audit_columns() }}
from orders
left join customers
on orders.customer_id = customers.customer_id
left join payments
on orders.order_id = payments.order_id
)
select * from final
示例 3:增量事件表
-- models/marts/analytics/fct_page_views.sql
{{ config(
materialized='incremental',
unique_key='page_view_id',
partition_by={
'field': 'event_date',
'data_type': 'date',
'granularity': 'day'
},
cluster_by=['user_id', 'page_path']
) }}
with events as (
select
event_id as page_view_id,
user_id,
session_id,
event_timestamp,
date(event_timestamp) as event_date,
event_properties:page_path::string as page_path,
event_properties:referrer::string as referrer,
_loaded_at
from {{ source('analytics', 'raw_events') }}
where event_type = 'page_view'
{% if is_incremental() %}
-- Use _loaded_at to catch late-arriving data
and _loaded_at > (select max(_loaded_at) from {{ this }})
{% endif %}
),
enriched as (
select
page_view_id,
user_id,
session_id,
event_timestamp,
event_date,
page_path,
referrer,
-- Parse URL components
split_part(page_path, '?', 1) as page_path_clean,
case
when referrer like '%google%' then 'Google'
when referrer like '%facebook%' then 'Facebook'
when referrer is null then 'Direct'
else 'Other'
end as referrer_source,
_loaded_at
from events
)
select * from enriched
示例 4:使用 SCD 类型 2 的客户维度
-- models/marts/core/dim_customers.sql
{{ config(
materialized='table',
unique_key='customer_key'
) }}
with customers as (
select * from {{ ref('stg_jaffle_shop__customers') }}
),
customer_orders as (
select
customer_id,
min(order_date) as first_order_date,
max(order_date) as most_recent_order_date,
count(order_id) as total_orders
from {{ ref('fct_orders') }}
group by 1
),
final as (
select
{{ dbt_utils.generate_surrogate_key(['customers.customer_id', 'customers._loaded_at']) }}
as customer_key,
customers.customer_id,
customers.customer_name,
customers.email,
customer_orders.first_order_date,
customer_orders.most_recent_order_date,
customer_orders.total_orders,
case
when customer_orders.total_orders >= 10 then 'VIP'
when customer_orders.total_orders >= 5 then 'Regular'
when customer_orders.total_orders >= 1 then 'New'
else 'Prospect'
end as customer_segment,
customers._loaded_at as effective_from,
null as effective_to,
true as is_current
from customers
left join customer_orders
on customers.customer_id = customer_orders.customer_id
)
select * from final
示例 5:聚合指标表
-- models/marts/analytics/daily_order_metrics.sql
{{ config(
materialized='incremental',
unique_key=['metric_date', 'status'],
incremental_strategy='delete+insert'
) }}
with orders as (
select * from {{ ref('fct_orders') }}
{% if is_incremental() %}
where order_date >= (select max(metric_date) - interval '7 days' from {{ this }})
{% endif %}
),
daily_metrics as (
select
date_trunc('day', order_date) as metric_date,
status,
count(distinct order_id) as order_count,
count(distinct customer_id) as unique_customers,
sum(order_total) as total_revenue,
avg(order_total) as avg_order_value,
min(order_total) as min_order_value,
max(order_total) as max_order_value,
percentile_cont(0.5) within group (order by order_total) as median_order_value
from orders
group by 1, 2
)
select * from daily_metrics
示例 6:使用宏透视指标
-- models/marts/analytics/customer_order_status_summary.sql
with orders as (
select
customer_id,
status,
order_total
from {{ ref('fct_orders') }}
)
select
customer_id,
{% for status in ['placed', 'shipped', 'completed', 'returned', 'cancelled'] %}
sum(case when status = '{{ status }}' then 1 else 0 end)
as {{ status }}_count,
sum(case when status = '{{ status }}' then order_total else 0 end)
as {{ status }}_revenue
{% if not loop.last %},{% endif %}
{% endfor %}
from orders
group by 1
示例 7:用于 SCD 类型 2 的快照
-- snapshots/customers_snapshot.sql
{% snapshot customers_snapshot %}
{{
config(
target_schema='snapshots',
target_database='analytics',
unique_key='customer_id',
strategy='timestamp',
updated_at='updated_at',
invalidate_hard_deletes=True
)
}}
select
customer_id,
customer_name,
email,
customer_segment,
updated_at
from {{ source('jaffle_shop', 'customers') }}
{% endsnapshot %}
示例 8:漏斗分析模型
-- models/marts/analytics/conversion_funnel.sql
with page_views as (
select
user_id,
session_id,
min(event_timestamp) as session_start
from {{ ref('fct_page_views') }}
where event_date >= current_date - interval '30 days'
group by 1, 2
),
product_views as (
select distinct
user_id,
session_id
from {{ ref('fct_page_views') }}
where page_path like '/product/%'
and event_date >= current_date - interval '30 days'
),
add_to_cart as (
select distinct
user_id,
session_id
from {{ ref('fct_events') }}
where event_type = 'add_to_cart'
and event_date >= current_date - interval '30 days'
),
checkout_started as (
select distinct
user_id,
session_id
from {{ ref('fct_events') }}
where event_type = 'checkout_started'
and event_date >= current_date - interval '30 days'
),
orders as (
select distinct
customer_id as user_id,
session_id
from {{ ref('fct_orders') }}
where order_date >= current_date - interval '30 days'
and status = 'completed'
),
funnel as (
select
count(distinct page_views.session_id) as sessions,
count(distinct product_views.session_id) as product_views,
count(distinct add_to_cart.session_id) as add_to_cart,
count(distinct checkout_started.session_id) as checkout_started,
count(distinct orders.session_id) as completed_orders
from page_views
left join product_views using (session_id)
left join add_to_cart using (session_id)
left join checkout_started using (session_id)
left join orders using (session_id)
),
funnel_metrics as (
select
sessions,
product_views,
round(100.0 * product_views / nullif(sessions, 0), 2) as pct_product_views,
add_to_cart,
round(100.0 * add_to_cart / nullif(product_views, 0), 2) as pct_add_to_cart,
checkout_started,
round(100.0 * checkout_started / nullif(add_to_cart, 0), 2) as pct_checkout_started,
completed_orders,
round(100.0 * completed_orders / nullif(checkout_started, 0), 2) as pct_completed_orders,
round(100.0 * completed_orders / nullif(sessions, 0), 2) as overall_conversion_rate
from funnel
)
select * from funnel_metrics
示例 9:队列留存分析
-- models/marts/analytics/cohort_retention.sql
with customer_orders as (
select
customer_id,
date_trunc('month', order_date) as order_month
from {{ ref('fct_orders') }}
where status = 'completed'
),
first_order as (
select
customer_id,
min(order_month) as cohort_month
from customer_orders
group by 1
),
cohort_data as (
select
f.cohort_month,
c.order_month,
datediff('month', f.cohort_month, c.order_month) as months_since_first_order,
count(distinct c.customer_id) as customer_count
from first_order f
join customer_orders c
on f.customer_id = c.customer_id
group by 1, 2, 3
),
cohort_size as (
select
cohort_month,
customer_count as cohort_size
from cohort_data
where months_since_first_order = 0
),
retention as (
select
cohort_data.cohort_month,
cohort_data.months_since_first_order,
cohort_data.customer_count,
cohort_size.cohort_size,
round(100.0 * cohort_data.customer_count / cohort_size.cohort_size, 2) as retention_pct
from cohort_data
join cohort_size
on cohort_data.cohort_month = cohort_size.cohort_month
)
select * from retention
order by cohort_month, months_since_first_order
示例 10:收入归因模型
-- models/marts/analytics/revenue_attribution.sql
with touchpoints as (
select
user_id,
session_id,
event_timestamp,
case
when referrer like '%google%' then 'Google'
when referrer like '%facebook%' then 'Facebook'
when referrer like '%email%' then 'Email'
when referrer is null then 'Direct'
else 'Other'
end as channel
from {{ ref('fct_page_views') }}
),
customer_journeys as (
select
t.user_id,
o.order_id,
o.order_total,
t.channel,
t.event_timestamp,
o.order_date,
row_number() over (
partition by o.order_id
order by t.event_timestamp
) as touchpoint_number,
count(*) over (partition by o.order_id) as total_touchpoints
from touchpoints t
join {{ ref('fct_orders') }} o
on t.user_id = o.customer_id
and t.event_timestamp <= o.order_date
and t.event_timestamp >= dateadd('day', -30, o.order_date)
),
attributed_revenue as (
select
order_id,
channel,
order_total,
-- First touch attribution
case when touchpoint_number = 1
then order_total else 0 end as first_touch_revenue,
-- Last touch attribution
case when touchpoint_number = total_touchpoints
then order_total else 0 end as last_touch_revenue,
-- Linear attribution
order_total / total_touchpoints as linear_revenue,
-- Time decay (more recent touchpoints get more credit)
order_total * (power(2, touchpoint_number - 1) /
(power(2, total_touchpoints) - 1)) as time_decay_revenue
from customer_journeys
)
select
channel,
count(distinct order_id) as orders,
sum(first_touch_revenue) as first_touch_revenue,
sum(last_touch_revenue) as last_touch_revenue,
sum(linear_revenue) as linear_revenue,
sum(time_decay_revenue) as time_decay_revenue
from attributed_revenue
group by 1
示例 11:数据质量测试套件
-- tests/assert_fct_orders_quality.sql
-- Test multiple data quality rules in one test
with order_quality_checks as (
select
order_id,
customer_id,
order_date,
order_total,
status,
-- Check 1: Order total should be positive
case when order_total < 0
then 'Negative order total' end as check_1,
-- Check 2: Order date should not be in future
case when order_date > current_date
then 'Future order date' end as check_2,
-- Check 3: Customer ID should exist
case when customer_id is null
then 'Missing customer ID' end as check_3,
-- Check 4: Status should be valid
case when status not in ('placed', 'shipped', 'completed', 'returned', 'cancelled')
then 'Invalid status' end as check_4
from {{ ref('fct_orders') }}
),
failed_checks as (
select
order_id,
check_1,
check_2,
check_3,
check_4
from order_quality_checks
where check_1 is not null
or check_2 is not null
or check_3 is not null
or check_4 is not null
)
select * from failed_checks
示例 12:缓慢变化维度合并
-- models/marts/core/dim_products_scd.sql
{{ config(
materialized='incremental',
unique_key='product_key',
merge_update_columns=['product_name', 'category', 'price', 'effective_to', 'is_current']
) }}
with source_data as (
select
product_id,
product_name,
category,
price,
updated_at
from {{ source('ecommerce', 'products') }}
{% if is_incremental() %}
where updated_at > (select max(updated_at) from {{ this }} where is_current = true)
{% endif %}
),
{% if is_incremental() %}
existing_records as (
select *
from {{ this }}
where is_current = true
),
changed_records as (
select
s.product_id,
s.product_name,
s.category,
s.price,
s.updated_at
from source_data s
join existing_records e
on s.product_id = e.product_id
and (
s.product_name != e.product_name
or s.category != e.category
or s.price != e.price
)
),
expire_old_records as (
select
e.product_key,
e.product_id,
e.product_name,
e.category,
e.price,
e.effective_from,
c.updated_at as effective_to,
false as is_current,
e.updated_at
from existing_records e
join changed_records c
on e.product_id = c.product_id
),
new_versions as (
select
{{ dbt_utils.generate_surrogate_key(['c.product_id', 'c.updated_at']) }} as product_key,
c.product_id,
c.product_name,
c.category,
c.price,
c.updated_at as effective_from,
null::timestamp as effective_to,
true as is_current,
c.updated_at
from changed_records c
),
combined as (
select * from expire_old_records
union all
select * from new_versions
)
select * from combined
{% else %}
-- First load: all records are current
select
{{ dbt_utils.generate_surrogate_key(['product_id', 'updated_at']) }} as product_key,
product_id,
product_name,
category,
price,
updated_at as effective_from,
null::timestamp as effective_to,
true as is_current,
updated_at
from source_data
{% endif %}
示例 13:用于排名的窗口函数
-- models/marts/analytics/customer_rfm_score.sql
with customer_metrics as (
select
customer_id,
max(order_date) as last_order_date,
count(order_id) as total_orders,
sum(order_total) as total_revenue
from {{ ref('fct_orders') }}
where status = 'completed'
group by 1
),
rfm_calculations as (
select
customer_id,
-- Recency: Days since last order
datediff('day', last_order_date, current_date) as recency_days,
-- Frequency: Total orders
total_orders as frequency,
-- Monetary: Total revenue
total_revenue as monetary,
-- Recency score (1-5, lower days = higher score)
ntile(5) over (order by datediff('day', last_order_date, current_date) desc) as recency_score,
-- Frequency score (1-5, more orders = higher score)
ntile(5) over (order by total_orders) as frequency_score,
-- Monetary score (1-5, more revenue = higher score)
ntile(5) over (order by total_revenue) as monetary_score
from customer_metrics
),
rfm_segments as (
select
customer_id,
recency_days,
frequency,
monetary,
recency_score,
frequency_score,
monetary_score,
recency_score * 100 + frequency_score * 10 + monetary_score as rfm_score,
case
when recency_score >= 4 and frequency_score >= 4 and monetary_score >= 4
then 'Champions'
when recency_score >= 3 and frequency_score >= 3 and monetary_score >= 3
then 'Loyal Customers'
when recency_score >= 4 and frequency_score <= 2 and monetary_score <= 2
then 'Promising'
when recency_score >= 3 and frequency_score <= 2 and monetary_score <= 2
then 'Potential Loyalists'
when recency_score <= 2 and frequency_score >= 3 and monetary_score >= 3
then 'At Risk'
when recency_score <= 2 and frequency_score <= 2 and monetary_score <= 2
then 'Hibernating'
when recency_score <= 1
then 'Lost'
else 'Need Attention'
end as customer_segment
from rfm_calculations
)
select * from rfm_segments
示例 14:合并多个数据源
-- models/staging/stg_all_events.sql
{{ config(
materialized='view'
) }}
-- Union events from multiple sources using dbt_utils
{{
dbt_utils.union_relations(
relations=[
ref('stg_web_events'),
ref('stg_mobile_events'),
ref('stg_api_events')
],
exclude=['_loaded_at'], -- Exclude source-specific columns
source_column_name='event_source' -- Add column to track source
)
}}
示例 15:代理键生成
-- models/marts/core/fct_order_lines.sql
with order_lines as (
select
order_id,
line_number,
product_id,
quantity,
unit_price,
quantity * unit_price as line_total
from {{ source('ecommerce', 'order_lines') }}
)
select
{{ dbt_utils.generate_surrogate_key(['order_id', 'line_number']) }} as order_line_key,
{{ dbt_utils.generate_surrogate_key(['order_id']) }} as order_key,
{{ dbt_utils.generate_surrogate_key(['product_id']) }} as product_key,
order_id,
line_number,
product_id,
quantity,
unit_price,
line_total
from order_lines
示例 16:用于时间序列的日期骨架
-- models/marts/analytics/daily_revenue_complete.sql
-- Generate complete date spine to ensure no missing dates
with date_spine as (
{{ dbt_utils.date_spine(
datepart="day",
start_date="cast('2020-01-01' as date)",
end_date="cast(current_date as date)"
) }}
),
daily_revenue as (
select
date_trunc('day', order_date) as order_date,
sum(order_total) as revenue
from {{ ref('fct_orders') }}
where status = 'completed'
group by 1
),
complete_series as (
select
date_spine.date_day,
coalesce(daily_revenue.revenue, 0) as revenue,
-- 7-day moving average
avg(coalesce(daily_revenue.revenue, 0)) over (
order by date_spine.date_day
rows between 6 preceding and current row
) as revenue_7d_ma,
-- Month-to-date revenue
sum(coalesce(daily_revenue.revenue, 0)) over (
partition by date_trunc('month', date_spine.date_day)
order by date_spine.date_day
) as revenue_mtd
from date_spine
left join daily_revenue
on date_spine.date_day = daily_revenue.order_date
)
select * from complete_series
示例 17:自定义架构宏覆盖
-- macros/generate_schema_name.sql
{% macro generate_schema_name(custom_schema_name, node) -%}
{%- set default_schema = target.schema -%}
{%- if target.name == 'prod' -%}
-- Production: Use custom schema names directly
{%- if custom_schema_name is not none -%}
{{ custom_schema_name | trim }}
{%- else -%}
{{ default_schema }}
{%- endif -%}
{%- elif target.name == 'dev' -%}
-- Development: Prefix with dev_username
{%- if custom_schema_name is not none -%}
dev_{{ env_var('DBT_USER', 'unknown') }}_{{ custom_schema_name | trim }}
{%- else -%}
dev_{{ env_var('DBT_USER', 'unknown') }}
{%- endif -%}
{%- else -%}
-- Default: Concatenate target schema with custom schema
{{ default_schema }}_{{ custom_schema_name | trim }}
{%- endif -%}
{%- endmacro %}
示例 18:跨数据库查询宏
-- macros/cross_db_concat.sql
-- Handle database-specific concat syntax
{% macro concat(fields) -%}
{{ return(adapter.dispatch('concat', 'dbt_utils')(fields)) }}
{%- endmacro %}
{% macro default__concat(fields) -%}
concat({{ fields|join(', ') }})
{%- endmacro %}
{% macro snowflake__concat(fields) -%}
{{ fields|join(' || ') }}
{%- endmacro %}
{% macro bigquery__concat(fields) -%}
concat({{ fields|join(', ') }})
{%- endmacro %}
{% macro redshift__concat(fields) -%}
{{ fields|join(' || ') }}
{%- endmacro %}
用法:
select
{{ concat(['first_name', "' '", 'last_name']) }} as full_name
from {{ ref('stg_customers') }}
示例 19:前置钩子和后置钩子配置
-- models/marts/core/fct_orders.sql
{{
config(
materialized='incremental',
unique_key='order_id',
pre_hook=[
"delete from {{ this }} where order_date < dateadd('year', -3, current_date)",
"{{ log('Starting incremental load for fct_orders', info=True) }}"
],
post_hook=[
"create index if not exists idx_fct_orders_customer_id on {{ this }}(customer_id)",
"create index if not exists idx_fct_orders_order_date on {{ this }}(order_date)",
"{{ grant_select(this, 'analyst_role') }}",
"{{ log('Completed incremental load for fct_orders', info=True) }}"
],
tags=['core', 'incremental']
)
}}
select * from {{ ref('stg_orders') }}
{% if is_incremental() %}
where order_date > (select max(order_date) from {{ this }})
{% endif %}
示例 20:Exposure 定义
# models/exposures.yml
version: 2
exposures:
- name: customer_dashboard
description: |
Executive dashboard showing customer metrics including:
- Customer acquisition trends
- Customer lifetime value
- Retention rates
- RFM segmentation
type: dashboard
maturity: high
url: https://looker.company.com/dashboards/customer-metrics
owner:
name: Analytics Team
email: [email protected]
depends_on:
- ref('fct_orders')
- ref('dim_customers')
- ref('customer_rfm_score')
- ref('cohort_retention')
tags: ['executive', 'customer-analytics']
- name: revenue_forecast_model
description: |
Machine learning model for revenue forecasting.
Uses historical order data to predict future revenue.
type: ml
maturity: medium
url: https://mlflow.company.com/models/revenue-forecast
owner:
name: Data Science Team
email: [email protected]
depends_on:
- ref('fct_orders')
- ref('daily_revenue_complete')
tags: ['ml', 'forecasting']
快速参考命令
必备 dbt 命令
# Install dependencies
dbt deps
# Compile project (check for errors)
dbt compile
# Run all models
dbt run
# Run specific model
dbt run --select fct_orders
# Run model and downstream dependencies
dbt run --select fct_orders+
# Run model and upstream dependencies
dbt run --select +fct_orders
# Run model and all dependencies
dbt run --select +fct_orders+
# Run all models in a directory
dbt run --select staging.*
# Run models with specific tag
dbt run --select tag:daily
# Run models, exclude specific ones
dbt run --exclude staging.*
# Run with full refresh (incremental models)
dbt run --full-refresh
# Test all models
dbt test
# Test specific model
dbt test --select fct_orders
# Generate documentation
dbt docs generate
# Serve documentation
dbt docs serve
# Debug connection
dbt debug
# Clean compiled files
dbt clean
# Seed CSV files
dbt seed
# Snapshot models
dbt snapshot
# List resources
dbt ls --select staging.*
# Show compiled SQL
dbt show --select fct_orders
# Parse project
dbt parse
模型选择语法
# By name
--select model_name
# By path
--select staging.jaffle_shop.*
# By tag
--select tag:daily
# By resource type
--select resource_type:model
# By package
--select package:dbt_utils
# By status (modified, new)
--select state:modified+ --state ./prod_manifest
# Combinations (union)
--select model_a model_b
# Intersections
--select tag:daily,staging.*
# Graph operators
--select +model_name # Upstream dependencies
--select model_name+ # Downstream dependencies
--select +model_name+ # All dependencies
--select @model_name # Model + children/parents to nth degree
资源
- dbt 官方文档:https://docs.getdbt.com/
- dbt Discourse 社区:https://discourse.getdbt.com/
- dbt GitHub 仓库:https://github.com/dbt-labs/dbt-core
- dbt 软件包中心:https://hub.getdbt.com/
- dbt 学习:https://courses.getdbt.com/
- dbt 风格指南:https://github.com/dbt-labs/corp/blob/main/dbt_style_guide.md
- 分析工程指南:https://www.getdbt.com/analytics-engineering/
- dbt Slack 社区:https://www.getdbt.com/community/join-the-community/
技能版本:1.0.0 最后更新:2025 年 10 月 技能类别:数据工程、分析工程、数据转换 兼容:dbt Core 1.0+、dbt Cloud、Snowflake、BigQuery、Redshift、Postgres、Databricks