dbt (Data Build Tool): Complete Reference Guide
A complete, beginner-to-advanced reference for dbt concepts. Click any section to expand it.
Table of Contents
- What is dbt?
- How dbt fits in the Modern Data Stack
- Project Structure
- Core Concepts
- The Layered Data Model
- Materializations
- Jinja & Macros
- Packages
- Variables
- Environments & Profiles
- Advanced Topics
- Key Commands
- Quick Reference Cheatsheet
What is dbt?
dbt (data build tool) is a transformation framework that lets data analysts and engineers write SQL SELECT statements to define transformations, and dbt handles the CREATE TABLE / VIEW logic, dependency resolution, testing, and documentation automatically.
dbt is the T in ELT: it runs entirely inside your data warehouse. No data ever leaves.
Key principles:
- Write
SELECT: neverCREATE,DROP, orINSERT - Every
.sqlfile = one table or view in your warehouse - Dependencies are declared with
ref(): dbt resolves run order automatically - Everything is version-controlled, tested, and documented
How dbt fits in the Modern Data Stack
┌─────────────┐ Load ┌──────────────────┐ Transform ┌──────────────┐ Query ┌──────────────┐
│ Sources │ ────────────► │ Data Warehouse │ ────────────► │ dbt │ ────────► │ BI / ML │
│ DBs, APIs │ │ Snowflake·BigQuery│ │ SQL + tests │ │ Looker·Metab.│
│ CSVs, etc. │ │ Redshift·DuckDB │ │ docs + DAG │ │ Tableau etc. │
└─────────────┘ └──────────────────┘ └──────────────┘ └──────────────┘
▲ ▲
│ │
EL tools dbt lives here
(Fivetran, Airbyte) runs SQL inside your warehouse
Project Structure
my_dbt_project/
├── models/ ← your SQL lives here
│ ├── staging/ ← light cleaning of raw tables
│ │ ├── stg_orders.sql
│ │ ├── stg_customers.sql
│ │ ├── _sources.yml ← declare raw source tables
│ │ └── _stg_models.yml ← tests + docs for staging models
│ ├── intermediate/ ← optional join/logic layer (not exposed to BI)
│ │ └── int_orders_with_items.sql
│ └── marts/ ← business-level models (BI reads these)
│ ├── orders.sql
│ ├── customers.sql
│ └── _marts.yml
├── macros/ ← reusable Jinja functions
│ └── clean_string.sql
├── seeds/ ← CSV files loaded as warehouse tables
│ └── country_codes.csv
├── tests/ ← custom singular test SQL files
│ └── assert_orders_positive_amount.sql
├── snapshots/ ← slowly changing dimension configs
│ └── orders_snapshot.sql
├── analyses/ ← ad-hoc SQL (compiled but not run)
├── dbt_project.yml ← project config (name, paths, materializations)
└── profiles.yml ← warehouse connection (usually ~/.dbt/profiles.yml)
Core Concepts
Models
What is a model?
A model is a single .sql file containing a SELECT statement. dbt executes it and creates a table or view in your warehouse. The filename becomes the table name.
-- models/staging/stg_orders.sql
select
id as order_id,
customer_id,
cast(created_at as timestamp) as ordered_at,
amount_cents / 100.0 as amount,
lower(trim(status)) as status
from {{ source('raw', 'orders') }}
where id is not null
dbt turns this into:
create or replace view dev.stg_orders as (
select
id as order_id,
...
from raw.orders
where id is not null
);
The ref() function: the most important concept in dbt
ref() is how you reference another model. It does two things:
- Resolves the correct table path for your environment (
dev.stg_ordersin dev,prod.stg_ordersin prod) - Registers a dependency so dbt builds models in the correct order
-- models/marts/orders.sql
select
o.order_id,
o.ordered_at,
o.amount,
c.full_name as customer_name,
c.email as customer_email
from {{ ref('stg_orders') }} o -- dependency on stg_orders
left join {{ ref('stg_customers') }} c -- dependency on stg_customers
on o.customer_id = c.customer_id
dbt sees the ref() calls and knows: run stg_orders and stg_customers before orders. Always. Automatically.
Inline config block
Add a config() block at the top of any model to override project-level settings:
{{
config(
materialized = 'table',
tags = ['marts', 'finance'],
schema = 'reporting'
)
}}
select * from {{ ref('stg_orders') }}
Config options include: materialized, tags, schema, database, alias, unique_key, partition_by, cluster_by, indexes.
A complete staging model example
-- models/staging/stg_orders.sql
{{
config(
materialized = 'view',
tags = ['staging', 'orders']
)
}}
with source as (
select * from {{ source('raw', 'orders') }}
),
renamed as (
select
id as order_id,
customer_id,
cast(created_at as timestamp) as ordered_at,
cast(updated_at as timestamp) as updated_at,
coalesce(amount_cents, 0) / 100.0 as amount,
lower(trim(status)) as status,
_fivetran_deleted as is_deleted
from source
where id is not null
and _fivetran_deleted = false
)
select * from renamed
A complete mart model example
-- models/marts/orders.sql
{{
config(
materialized = 'table',
tags = ['marts', 'core']
)
}}
with orders as (
select * from {{ ref('stg_orders') }}
),
customers as (
select * from {{ ref('stg_customers') }}
),
final as (
select
o.order_id,
o.ordered_at,
o.amount,
o.status,
o.customer_id,
c.full_name as customer_name,
c.email as customer_email,
c.country_code,
date_trunc('month', o.ordered_at) as order_month,
case
when o.amount >= 100 then 'high_value'
when o.amount >= 50 then 'mid_value'
else 'low_value'
end as order_tier
from orders o
left join customers c on o.customer_id = c.customer_id
)
select * from final
Sources
What are sources?
Sources are raw tables that dbt doesn't own: they're loaded by your EL tool (Fivetran, Airbyte, etc.). You declare them in a YAML file so dbt knows about them and can run freshness checks.
# models/staging/_sources.yml
version: 2
sources:
- name: raw # logical name used in {{ source() }}
database: my_database # optional: defaults to profile database
schema: raw # actual schema in your warehouse
tables:
- name: orders
description: "Raw orders loaded by Fivetran from the production DB"
loaded_at_field: _fivetran_synced
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}
- name: customers
description: "Raw customer records from the production DB"
loaded_at_field: _fivetran_synced
freshness:
warn_after: {count: 24, period: hour}
error_after: {count: 48, period: hour}
Reference a source in a model:
select * from {{ source('raw', 'orders') }}
-- ▲ ▲
-- source name table name (from YAML)
Check freshness:
dbt source freshness
Tests
Generic tests (built-in)
Declared in YAML alongside your model under data_tests: (dbt 1.8+; older projects use tests:, which is still accepted). dbt ships with four built-in generic tests:
| Test | What it checks |
|---|---|
unique |
Every value in the column is distinct |
not_null |
No NULL values exist |
accepted_values |
All values are in a predefined list |
relationships |
Every value exists in another model's column (FK integrity) |
# models/marts/_marts.yml
version: 2
models:
- name: orders
columns:
- name: order_id
data_tests:
- unique
- not_null
- name: customer_id
data_tests:
- not_null
- relationships:
to: ref('stg_customers')
field: customer_id
- name: status
data_tests:
- accepted_values:
values: ['pending', 'shipped', 'delivered', 'cancelled']
- name: amount
data_tests:
- not_null
Singular tests (custom SQL)
A singular test is a .sql file in the tests/ folder. It should return zero rows to pass: any row returned is a failure.
-- tests/assert_orders_positive_amount.sql
-- Fails if any order has a negative or zero amount
select order_id
from {{ ref('orders') }}
where amount <= 0
-- tests/assert_no_orphaned_orders.sql
-- Fails if any order references a non-existent customer
select o.order_id
from {{ ref('orders') }} o
left join {{ ref('customers') }} c on o.customer_id = c.customer_id
where c.customer_id is null
Running tests
dbt test # run all tests
dbt test -s orders # test one model
dbt test -s tag:marts # test all models tagged 'marts'
dbt test --store-failures # save failing rows to a table for inspection
dbt build -s +orders # run + test orders and all upstream models
Documentation
schema.yml: tests + docs in one place
# models/marts/_marts.yml
version: 2
models:
- name: orders
description: >
One row per order. Joins stg_orders with stg_customers.
Source of truth for all order-level reporting.
columns:
- name: order_id
description: "Primary key. Unique identifier for each order."
- name: ordered_at
description: "Timestamp when the order was placed (UTC)."
- name: amount
description: "Order total in USD."
- name: order_tier
description: "high_value (≥$100), mid_value ($50-$99), low_value (<$50)."
Generating and serving docs
dbt docs generate # compile docs into target/catalog.json
dbt docs serve # open a local documentation site at localhost:8080
The docs site includes:
- Searchable model + column descriptions
- Auto-generated DAG / lineage graph
- Source freshness status
- Test results
doc() blocks: reusable descriptions
For long descriptions, write them in a .md file and reference them:
<!-- models/marts/docs.md -->
{% docs order_tier %}
Bucketed order value:
- **high_value**: order total ≥ $100
- **mid_value**: order total $50-$99
- **low_value**: order total < $50
{% enddocs %}
# Reference in schema.yml
- name: order_tier
description: "{{ doc('order_tier') }}"
Macros
What are macros?
Macros are Jinja-templated functions stored in macros/. They let you write reusable SQL logic: think of them as functions in a programming language.
-- macros/cents_to_dollars.sql
{% macro cents_to_dollars(column_name) %}
coalesce({{ column_name }}, 0) / 100.0
{% endmacro %}
Use it in any model:
select
id as order_id,
{{ cents_to_dollars('amount_cents') }} as amount
from {{ source('raw', 'orders') }}
Macro with arguments and defaults
-- macros/date_spine.sql
{% macro clean_string(column_name, replacement='unknown') %}
nullif(lower(trim({{ column_name }})), '')
{% endmacro %}
-- macros/generate_surrogate_key.sql
{% macro generate_surrogate_key(field_list) %}
md5(
concat_ws('-',
{% for field in field_list %}
cast({{ field }} as varchar)
{% if not loop.last %}, {% endif %}
{% endfor %}
)
)
{% endmacro %}
Usage:
select
{{ generate_surrogate_key(['order_id', 'line_item_id']) }} as surrogate_key
from {{ ref('stg_order_items') }}
Seeds
What are seeds?
Seeds are CSV files in the seeds/ directory that dbt loads as tables in your warehouse. Useful for static lookup data: country codes, mapping tables, cost rates, etc.
-- seeds/country_codes.csv
country_code,country_name,region
US,United States,North America
GB,United Kingdom,Europe
DE,Germany,Europe
JP,Japan,Asia Pacific
Load them:
dbt seed # load all seeds
dbt seed -s country_codes # load one seed
Reference in a model:
select
o.*,
cc.country_name,
cc.region
from {{ ref('orders') }} o
left join {{ ref('country_codes') }} cc on o.country_code = cc.country_code
Configure in dbt_project.yml:
seeds:
my_project:
country_codes:
+column_types:
country_code: varchar(2)
Snapshots
What are snapshots?
Snapshots track changes to rows over time: useful for slowly changing dimensions (SCDs). dbt adds dbt_valid_from and dbt_valid_to columns to record when each version of a row was active.
-- snapshots/orders_snapshot.sql
{% snapshot orders_snapshot %}
{{
config(
target_schema = 'snapshots',
unique_key = 'order_id',
strategy = 'timestamp',
updated_at = 'updated_at'
)
}}
select * from {{ source('raw', 'orders') }}
{% endsnapshot %}
Run snapshots:
dbt snapshot
Result table includes:
| order_id | status | dbt_valid_from | dbt_valid_to |
|---|---|---|---|
| 1 | pending | 2024-01-01 | 2024-01-03 |
| 1 | shipped | 2024-01-03 | null |
The DAG
How the DAG works
dbt automatically builds a Directed Acyclic Graph from all your ref() and source() calls. It guarantees models run in the correct order: you never have to manage this manually.
raw.orders ──────► stg_orders ──────┐
├──► orders (mart)
raw.customers ───► stg_customers ───┘
View the DAG:
dbt docs generate && dbt docs serve
# navigate to the lineage graph in the UI
Select models using graph operators:
dbt run -s stg_orders # just this model
dbt run -s +orders # orders + everything upstream
dbt run -s orders+ # orders + everything downstream
dbt run -s +orders+ # orders + all upstream + all downstream
dbt run -s stg_orders+1 # stg_orders + one level downstream only
The Layered Data Model
The industry-standard pattern for organising a dbt project:
Raw (sources)
│
▼
Staging ── 1:1 with source tables. Rename, cast, clean. No joins. No business logic.
│ Materialized as: VIEW
▼
Intermediate ── Join staging models. Begin applying business logic.
│ Not exposed to BI tools.
│ Materialized as: VIEW (or ephemeral)
▼
Marts ── Business entities: Orders, Customers, Revenue.
BI tools, ML models, and analysts read these.
Materialized as: TABLE
Naming conventions:
stg_[source]__[table].sql: staging (e.g.stg_shopify__orders.sql)int_[entity]_[verb].sql: intermediate (e.g.int_orders_joined.sql)[entity].sqlorfct_[entity].sql/dim_[entity].sql: marts
Materializations
View Materialization
How view materialization works
dbt executes CREATE OR REPLACE VIEW. No data is stored: the SQL runs live every time something queries the view.
{{ config(materialized='view') }}
with source as (
select * from {{ source('raw', 'orders') }}
),
renamed as (
select
id as order_id,
customer_id,
cast(created_at as timestamp) as ordered_at,
amount_cents / 100.0 as amount
from source
)
select * from renamed
Use when: staging layer, small tables, models not queried directly by BI tools.
Avoid when: the table is large (slow queries), or you have deep chains of views.
Table Materialization
How table materialization works
dbt runs CREATE TABLE AS SELECT. Data is physically stored. On every run, dbt drops and recreates the full table.
{{ config(materialized='table') }}
select
o.order_id,
o.ordered_at,
o.amount,
c.full_name as customer_name
from {{ ref('stg_orders') }} o
left join {{ ref('stg_customers') }} c on o.customer_id = c.customer_id
Use when: marts, models queried by BI tools, large datasets where query speed matters.
Avoid when: the table is extremely large and a full rebuild is too slow (use incremental instead).
Incremental Materialization
How incremental materialization works
On the first run, dbt builds the full table. On subsequent runs, it only processes new or changed rows and appends/upserts them.
{{
config(
materialized = 'incremental',
unique_key = 'order_id'
)
}}
select
order_id,
customer_id,
ordered_at,
amount,
status
from {{ ref('stg_orders') }}
{% if is_incremental() %}
-- only process rows newer than the most recent row already in the table
where ordered_at > (select max(ordered_at) from {{ this }})
{% endif %}
Incremental strategies:
| Strategy | Behaviour | Supported by |
|---|---|---|
append |
Insert only new rows (no dedup) | All warehouses |
merge |
Upsert rows by unique_key |
Snowflake, BigQuery, Databricks |
delete+insert |
Delete matching rows, re-insert | Snowflake, Redshift |
insert_overwrite |
Overwrite entire partitions | BigQuery, Spark |
{{ config(
materialized = 'incremental',
unique_key = 'order_id',
incremental_strategy = 'merge'
) }}
Force a full refresh:
dbt run -s orders --full-refresh
Use when: large event tables, logs, any table where rebuilding everything each run is too slow/expensive.
Ephemeral Materialization
How ephemeral materialization works
An ephemeral model is never written to the warehouse. Instead, dbt injects it as a CTE into any model that references it.
{{ config(materialized='ephemeral') }}
select
order_id,
amount,
case
when amount >= 100 then 'high_value'
when amount >= 50 then 'mid_value'
else 'low_value'
end as order_tier
from {{ ref('stg_orders') }}
When a downstream model calls ref('orders_with_tier'), dbt inlines the SQL as a CTE:
-- compiled output of downstream model
with orders_with_tier as (
-- ephemeral model SQL injected here
select order_id, amount, case ... end as order_tier
from dev.stg_orders
),
final as (
select * from orders_with_tier where order_tier = 'high_value'
)
select * from final
Use when: small helper transformations you don't want cluttering your warehouse schema.
Avoid when: multiple models reference it, the SQL gets duplicated into each.
Setting materializations globally in dbt_project.yml
# dbt_project.yml
name: my_project
version: '1.0.0'
config-version: 2
models:
my_project:
staging:
+materialized: view # all models in staging/ → view
intermediate:
+materialized: view # all models in intermediate/ → view
marts:
+materialized: table # all models in marts/ → table
finance:
+materialized: table
+tags: ['finance']
The + prefix applies the config to the folder and all subfolders. An inline config() block in a model file always overrides the dbt_project.yml default.
Jinja & Macros in Depth
Jinja basics: if, for, variables
dbt uses Jinja2 templating. Anything inside {{ }} is rendered; {% %} is control flow.
-- if statement
{% if target.name == 'prod' %}
where is_deleted = false
{% else %}
where true -- include everything in dev
{% endif %}
-- for loop
select
{% for payment_method in ['credit_card', 'bank_transfer', 'voucher'] %}
sum(case when payment_method = '{{ payment_method }}'
then amount else 0 end)
as {{ payment_method }}_amount
{% if not loop.last %},{% endif %}
{% endfor %}
from {{ ref('payments') }}
-- set a variable
{% set my_date = '2024-01-01' %}
where ordered_at >= '{{ my_date }}'
Writing and calling macros
-- macros/safe_divide.sql
{% macro safe_divide(numerator, denominator) %}
case
when {{ denominator }} = 0 or {{ denominator }} is null then null
else {{ numerator }} / {{ denominator }}
end
{% endmacro %}
-- usage in a model
select
order_id,
{{ safe_divide('revenue', 'items_count') }} as revenue_per_item
from {{ ref('orders') }}
run_query: execute SQL inside a macro
{% macro get_column_values(table, column) %}
{% set query %}
select distinct {{ column }}
from {{ table }}
order by 1
{% endset %}
{% set results = run_query(query) %}
{% if execute %}
{{ return(results.columns[0].values()) }}
{% endif %}
{% endmacro %}
Packages
Installing and using packages
dbt packages are shared libraries of macros and models. Add them to packages.yml:
# packages.yml
packages:
- package: dbt-labs/dbt_utils
version: 1.1.1
- package: calogica/dbt_expectations
version: 0.10.0
- package: dbt-labs/dbt_project_evaluator
version: 0.8.0
Install:
dbt deps
dbt_utils: most-used macros:
-- surrogate key from multiple columns
{{ dbt_utils.generate_surrogate_key(['order_id', 'line_item_id']) }}
-- pivot rows to columns
{{ dbt_utils.pivot('payment_method', dbt_utils.get_column_values(ref('payments'), 'payment_method')) }}
-- union multiple tables
{{ dbt_utils.union_relations(relations=[ref('orders_2022'), ref('orders_2023')]) }}
-- date spine (generate a row for every date in a range)
{{ dbt_utils.date_spine(
datepart = 'day',
start_date = "'2024-01-01'",
end_date = "'2024-12-31'"
) }}
dbt_expectations: Great Expectations-style tests:
- name: amount
data_tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 100000
- dbt_expectations.expect_column_to_exist
- dbt_expectations.expect_column_values_to_not_be_null
Variables
Defining and using variables
Variables let you pass dynamic values into your models.
Define in dbt_project.yml:
vars:
start_date: '2024-01-01'
payment_methods: ['credit_card', 'bank_transfer']
is_test_run: false
Use in a model:
where ordered_at >= '{{ var("start_date") }}'
{% if var("is_test_run") %}
limit 100
{% endif %}
Override at runtime:
dbt run --vars '{"start_date": "2023-01-01", "is_test_run": true}'
Variable with default fallback:
{{ var('start_date', '2020-01-01') }}
-- uses 2020-01-01 if start_date is not set anywhere
Environments & Profiles
profiles.yml: warehouse connections
Located at ~/.dbt/profiles.yml (outside the project, so credentials aren't checked into git):
# ~/.dbt/profiles.yml
my_project:
target: dev
outputs:
dev:
type: snowflake
account: xy12345.us-east-1
user: "{{ env_var('DBT_USER') }}"
password: "{{ env_var('DBT_PASSWORD') }}"
role: transformer
database: analytics
warehouse: dev_wh
schema: dbt_yourname # personal dev schema
threads: 4
prod:
type: snowflake
account: xy12345.us-east-1
user: "{{ env_var('DBT_PROD_USER') }}"
password: "{{ env_var('DBT_PROD_PASSWORD') }}"
role: transformer_prod
database: analytics
warehouse: prod_wh
schema: prod
threads: 8
Run against a specific target:
dbt run --target prod
dbt run --target dev # default
target object: environment-aware SQL
The target object exposes the current environment in Jinja:
-- limit rows in dev to keep things fast
select * from {{ ref('events') }}
{% if target.name != 'prod' %}
limit 1000
{% endif %}
-- use a cheaper warehouse in dev
{{ config(
snowflake_warehouse = 'prod_wh' if target.name == 'prod' else 'dev_wh'
) }}
Advanced Topics
Snapshots (Slowly Changing Dimensions)
Timestamp strategy
Use when your source table has a reliable updated_at column:
{% snapshot customers_snapshot %}
{{
config(
target_schema = 'snapshots',
unique_key = 'customer_id',
strategy = 'timestamp',
updated_at = 'updated_at'
)
}}
select * from {{ source('raw', 'customers') }}
{% endsnapshot %}
Check strategy
Use when there's no updated_at: dbt hashes specified columns to detect changes:
{% snapshot orders_snapshot %}
{{
config(
target_schema = 'snapshots',
unique_key = 'order_id',
strategy = 'check',
check_cols = ['status', 'amount']
)
}}
select * from {{ source('raw', 'orders') }}
{% endsnapshot %}
Exposures
Documenting downstream consumers
Exposures document what uses your dbt models: dashboards, ML models, reverse ETL, etc. They appear in the lineage graph so you can see the blast radius of a model change.
# models/marts/exposures.yml
version: 2
exposures:
- name: orders_dashboard
type: dashboard
maturity: high
url: https://looker.mycompany.com/dashboards/42
description: "Executive orders dashboard: refreshed daily"
depends_on:
- ref('orders')
- ref('customers')
owner:
name: Data Team
email: data@mycompany.com
- name: churn_prediction_model
type: ml
maturity: medium
description: "Propensity-to-churn model trained weekly"
depends_on:
- ref('customers')
owner:
name: ML Team
email: ml@mycompany.com
Semantic Layer & Metrics
Defining metrics with MetricFlow
dbt's semantic layer (powered by MetricFlow) lets you define metrics once and query them consistently across any BI tool.
# models/marts/metrics.yml
semantic_models:
- name: orders
defaults:
agg_time_dimension: ordered_at
model: ref('orders')
entities:
- name: order
type: primary
expr: order_id
- name: customer
type: foreign
expr: customer_id
dimensions:
- name: ordered_at
type: time
type_params:
time_granularity: day
- name: order_tier
type: categorical
measures:
- name: order_count
agg: count
expr: order_id
- name: total_revenue
agg: sum
expr: amount
metrics:
- name: total_revenue
type: simple
label: "Total Revenue"
type_params:
measure: total_revenue
- name: average_order_value
type: ratio
label: "Average Order Value"
type_params:
numerator: total_revenue
denominator: order_count
Custom Schemas
Controlling where models land in the warehouse
By default, all models land in the schema defined in profiles.yml. Override with config:
{{ config(schema='finance') }}
-- lands in: analytics.dbt_yourname_finance (dev) or analytics.finance (prod)
Customise the schema-naming logic by overriding generate_schema_name in macros/:
-- macros/generate_schema_name.sql
{% macro generate_schema_name(custom_schema_name, node) -%}
{%- set default_schema = target.schema -%}
{%- if custom_schema_name is none -%}
{{ default_schema }}
{%- else -%}
{%- if target.name == 'prod' -%}
{{ custom_schema_name | trim }} -- prod: just the custom name
{%- else -%}
{{ default_schema }}_{{ custom_schema_name | trim }} -- dev: prefix with dev schema
{%- endif -%}
{%- endif -%}
{%- endmacro %}
CI/CD with dbt
GitHub Actions workflow
# .github/workflows/dbt_ci.yml
name: dbt CI
on:
pull_request:
branches: [main]
jobs:
dbt-ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dbt
run: pip install dbt-snowflake
- name: dbt deps
run: dbt deps
- name: dbt build (slim CI: only changed models + downstream)
run: |
dbt build \
--select state:modified+ \
--defer \
--state ./prod-artifacts
env:
DBT_USER: ${{ secrets.DBT_USER }}
DBT_PASSWORD: ${{ secrets.DBT_PASSWORD }}
Slim CI with state:modified+ only builds models that changed in the PR and their downstream dependencies: much faster than rebuilding everything.
Key Commands
# ── Project setup ──────────────────────────────────────────────
dbt init my_project # scaffold a new project
dbt debug # test warehouse connection
dbt deps # install packages from packages.yml
# ── Running models ─────────────────────────────────────────────
dbt run # build all models
dbt run -s stg_orders # build one model
dbt run -s +orders # build orders + all upstream
dbt run -s orders+ # build orders + all downstream
dbt run -s staging.* # build all models in the staging/ folder
dbt run -s tag:finance # build all models tagged 'finance'
dbt run --target prod # run against production
dbt run --full-refresh # rebuild incremental models from scratch
# ── Testing ────────────────────────────────────────────────────
dbt test # run all tests
dbt test -s orders # test one model
dbt test --store-failures # save failing rows to a table
dbt source freshness # check source table freshness
# ── Building (run + test + seed + snapshot) ────────────────────
dbt build # run everything
dbt build -s +orders # build and test orders + all upstream
# ── Docs ───────────────────────────────────────────────────────
dbt docs generate # compile documentation
dbt docs serve # serve docs locally at localhost:8080
# ── Utilities ──────────────────────────────────────────────────
dbt compile -s orders # compile SQL without running (writes to target/)
dbt seed # load CSV seeds into warehouse
dbt snapshot # run snapshots (SCD tracking)
dbt clean # delete target/ and dbt_packages/
dbt ls -s tag:staging # list models matching a selector
Quick Reference Cheatsheet
| Concept | Purpose | File location |
|---|---|---|
model |
A SQL SELECT → table/view in warehouse | models/**/*.sql |
source() |
Reference a raw table you don't own | models/**/_sources.yml |
ref() |
Reference another dbt model | Inside any .sql model |
config() |
Set materialization, tags, schema per model | Top of .sql file |
schema.yml |
Declare tests + descriptions | models/**/_*.yml |
macro |
Reusable Jinja function | macros/*.sql |
seed |
Load a CSV as a warehouse table | seeds/*.csv |
snapshot |
Track row history (SCD type 2) | snapshots/*.sql |
exposure |
Document downstream consumers | models/**/*.yml |
packages.yml |
Install third-party macro libraries | Project root |
dbt_project.yml |
Project-level config + folder materializations | Project root |
profiles.yml |
Warehouse connection credentials | ~/.dbt/ |
| Materialization | Stores data? | Rebuilt how? | Best for |
|---|---|---|---|
view |
No | SQL re-runs on every query | Staging, light transforms |
table |
Yes | Full drop + recreate each run | Marts, BI-facing models |
incremental |
Yes | Append/upsert new rows only | Large event tables, logs |
ephemeral |
No | Injected as CTE into downstream | Small helper logic |
Resources
- dbt Documentation
- dbt Discourse (community forum)
- dbt Slack community
- dbt Package Hub
- dbt_utils package
- dbt_expectations package
- MetricFlow (semantic layer)