Models, refs, tests, materializations — the SQL-pipeline framework that ate the warehouse.
Data Engineering Module 3 SQL Foundations.sql files in a folder with no dependency graph — "did anyone rerun the daily aggregate after I updated the source table?"dbt (data build tool) is a SQL-pipeline framework. You write SQL in .sql files; dbt turns them into a DAG, runs them in dependency order against your warehouse, runs your tests, and tracks lineage.
One CLI tool. Zero new query languages. Your existing SQL knowledge, with build-system discipline layered on top.
dbt build on a schedule..sql files in models/. Each one is a single SELECT. dbt wraps it in CREATE TABLE / CREATE VIEW / INSERT INTO based on its materialization.
ref()The magic function that turns {{ ref('stg_events') }} into the right table name AND builds the dependency graph automatically.
Schema tests (unique, not_null, accepted_values, relationships) declared in YAML, plus singular tests (any SQL that should return zero rows).
-- models/staging/stg_events.sql
SELECT
type,
actor,
repo,
created_at,
date AS event_date
FROM {{ source('raw', 'events_raw') }}
One SELECT, one file, one model. source() references your external raw table; dbt knows where it lives from sources.yml. The {{ }} is Jinja — dbt renders it before sending SQL to the warehouse.
ref() graph-- models/marts/mart_daily_event_counts.sql
SELECT
event_date,
type,
COUNT(*) AS n_events,
COUNT(DISTINCT actor) AS n_actors
FROM {{ ref('stg_events') }}
GROUP BY event_date, type
ORDER BY event_date, type
ref('stg_events') says "depends on the model named stg_events." dbt resolves the name to the right schema/table AND records the edge in the lineage graph. No hard-coded table names anywhere.
dbt build — the one command$ dbt build
12:34:01 Running with dbt=1.8.x
12:34:01 Found 4 models, 9 tests, 1 source
12:34:01
12:34:02 1 of 13 START sql view model main.stg_events .......... [RUN]
12:34:02 1 of 13 OK created view stg_events ..................... [OK in 0.4s]
12:34:02 2 of 13 START test not_null_stg_events_type ........... [RUN]
12:34:03 2 of 13 PASS .......................................... [PASS in 0.1s]
...
12:34:08 Done. PASS=13 WARN=0 ERROR=0 SKIP=0 TOTAL=13
dbt build is the headline command: build every model in dependency order, run every test after the model it tests, fail fast on the first error. One command, one log, one exit code.
# models/staging/schema.yml
version: 2
models:
- name: stg_events
columns:
- name: type
tests:
- not_null
- accepted_values:
values: ['PushEvent', 'CreateEvent', 'IssueCommentEvent',
'PullRequestEvent']
quote: true
- name: actor
tests:
- not_null
Four built-in tests cover 80% of data-quality needs: unique, not_null, accepted_values, relationships. Tests live next to the model they test.
| Materialization | Built as | Use when |
|---|---|---|
view | CREATE VIEW | Cheap to recompute, no storage cost. Default. |
table | CREATE TABLE | Expensive query, queried often, storage is cheap. |
incremental | INSERT INTO new rows | Huge table, full rebuild too slow, only new rows arrive. |
ephemeral | Inlined as CTE | Reusable SQL that shouldn't be its own object. |
incremental in 6 lines{{ config(materialized='incremental', unique_key='event_date') }}
SELECT event_date, type, COUNT(*) AS n_events
FROM {{ source('raw', 'events_raw') }}
{% if is_incremental() %}
WHERE event_date > (SELECT MAX(event_date) FROM {{ this }})
{% endif %}
GROUP BY event_date, type
{{ this }} is the model's own table. On first run, is_incremental() is false and dbt builds the full table; on subsequent runs, only new rows are inserted. unique_key controls how dbt merges/upserts.
$ dbt docs generate
$ dbt docs serve
dbt docs serve opens a browser with the full lineage graph, every model's SQL, every test, every column description. No extra config — dbt assembles it from your ref() calls and YAML.
The lineage graph shows: raw.events_raw → stg_events → stg_actors, and stg_events → mart_daily_event_counts → dashboards.
Raw tables loaded by L01–L06: Python ETL + Airflow + Parquet output, loaded into DuckDB / Snowflake.
Clean, tested mart tables consumed by BI (Looker, Mode, Hex, Metabase) and downstream systems.
Reads sources, writes models, runs tests. Same dbt_project.yml runs against DuckDB locally and Snowflake in production — only profiles.yml changes.
ref() call becomes an edge in the graph; dbt docs serve visualises it.profiles.yml change.