Quick Reference · analytics engineering · the T in ELT

dbt cheat sheet

dbt turns SELECT statements into a data pipeline. You write one model per .sql file; ref() and source() wire them into a DAG; dbt compiles your Jinja+SQL to plain SQL, wraps each query in the right CREATE TABLE/VIEW/MERGE, and runs them in dependency order against your warehouse — then tests and documents the result. Learn the map once and the commands stop being a list to memorise.

setup / project models & SQL commands & selection YAML config Jinja & macros tests · docs · freshness destructive / gotcha most common

Distilled & cross-checked against: docs.getdbt.com (About · Build · Reference) · verified by running dbt-core 1.12.0 against DuckDB · y42.com · datagym.io · thedataschool.co.uk · hevodata.com

What dbt actually does — compile, wrap, run the DAG
ONE MODEL — you write a SELECT, dbt writes the DDL model.sql {{ config(materialized=…) }} select … from {{ ref('stg_orders') }} just a SELECT + Jinja compiled SQL Jinja resolved to plain warehouse SQL ref → db.schema.table wrapped in DDL create table / view / merge — per materializ. dbt writes this for you your warehouse Snowflake · BigQuery · Databricks Redshift · Postgres · DuckDB ✓ dbt test  ·  📖 dbt docs compile run execute MANY MODELS — ref() & source() build the DAG; dbt runs it in dependency order SOURCES (raw) raw.orders raw.customers seed: countries STAGING (views) stg_orders stg_customers INTERMEDIATE int_orders_joined MARTS (tables — what BI reads) fct_orders dim_customers mart_revenue dbt build runs seeds → models → snapshots → tests in this topological order · ref('x') is the edge that creates the arrow. Naming layers stg_ / int_ / fct_ / dim_ is convention, not enforced — but it keeps the DAG readable and is what every dbt project does.
anatomy of a dbt project — a model, its tests, and the build
-- models/marts/fct_orders.sql  ·  one model = one SELECT
{{ config(materialized='incremental', unique_key='order_id') }}
select o.order_id, o.customer_id, c.country, o.amount
from {{ ref('stg_orders') }} o                # ref() → builds the DAG edge
join {{ ref('stg_customers') }} c using (customer_id)
{% if is_incremental() %} where o.updated_at > (select max(updated_at) from {{ this }}) {% endif %}

# models/marts/_models.yml  ·  properties + tests live in YAML
models:
  - name: fct_orders
    columns:
      - name: order_id
        data_tests: [unique, not_null]      # generic tests

$ dbt deps                 # install packages.yml
$ dbt build -s +fct_orders  # fct_orders and everything upstream, run + tested in DAG order
01Setup & Projectonce per project
02The dbt Modela .sql file = one SELECT
03Materializationshow the SELECT persists
04Incremental Modelsonly process new rows
05Core Commandsthe daily verbs
06build vs runthe key distinction
07Node Selectiongraph operators
08Selection Methods & Stateselect smarter
09Sources & Freshnessthe raw layer
10Properties · schema.ymldescribe & configure
11Project Configdbt_project.yml
12Seeds & Snapshotsstatic data · history
13Tests · Genericdata quality in YAML
14Tests · Singular & Unitcustom & logic tests
15Jinja Essentialstemplated SQL
16Macros & Packagesreusable SQL
17Docs, Lineage & Hooksdescribe & automate
18Engines & Ecosystemthe 2026 landscape

Four ideas worth a picture

The mental models behind the commands — compilation, materializations, graph selection, and slowly-changing snapshots.

ref() → compile → wrap in DDL

Your SELECT with Jinja becomes plain SQL, then dbt wraps it in the DDL for its materialization.

YOU WRITE select * from {{ ref('stg_orders') }} {{ config(materialized='table') }} dbt compile dbt COMPILES select * from analytics.staging.stg_orders dbt run dbt RUNS create table analytics.marts.orders as ( ... )

Materializations — same SELECT, different DDL

The one config that most changes cost & freshness. Pick per model.

view create view as (…) no storage · always fresh recomputes on every read table create table as (…) full rebuild each run fast reads · can be stale incremental merge new rows only cheapest on big data is_incremental() gate most likely to bite you ephemeral no object created inlined as a CTE into its children not queryable in BI

Graph selection — the + operator

Where you put + decides which slice of the lineage runs.

stg_a stg_b int_xselected fct_p fct_q +int_x= green + int_x int_x+= int_x + amber +int_x+= all five

Snapshots — SCD-2 history

When a source row changes, dbt closes the old version and inserts a new one.

source row id=7 changes tier: freepro id tier dbt_valid_from dbt_valid_to 7 free 2026-01-01 2026-06-01 closed 7 pro 2026-06-01 NULL current row timestamp strategy: compares an updated_at column check strategy: hashes check_cols — no timestamp needed • query "as of" any date by filtering valid_from / valid_to snapshots are append-only history — you can't rebuild lost versions

Worth memorizing

ref() > hardcodingalways ref()/source() — that's what builds the DAG & lineage
build ≠ runbuild also runs seeds/snapshots/tests in DAG order; run = models only
+ direction+model = parents (upstream) · model+ = children (downstream)
incremental bitesis_incremental() is true only on the 2nd+ run of an existing table
--full-refreshdrops & rebuilds an incremental — use after logic or schema changes
view vs tableview = always fresh, slow reads · table = fast reads, stale till rebuilt
ephemeralno warehouse object — inlined as a CTE; BI & ad-hoc can't query it
config precedencein-file config() > folder +config > project defaults
generic vs singulargeneric tests live in YAML; singular = a .sql returning bad rows
snapshot strategytimestamp misses changes if updated_at isn't bumped; check hashes cols
dbt depsrun after every packages.yml change; dbt_packages/ is git-ignored
state:modified+the CI trick — build only what changed since prod, plus downstream
run order = the DAGtopological, not file order or alphabetical
Jinja runs firstit compiles before SQL executes — use run_query for warehouse values