DBTFoundations

Models, Compilation, and the DAG

How analytics engineers turn raw warehouse tables into trusted models — pick a topic on the left and its full breakdown loads here: the mental model, the compiled SQL dbt actually issues, live runs you can drive yourself, and the failure modes that quietly ship wrong numbers.

18 min readTopics chapter readerLevel · Easy
01 · Compilation

What Compilation Actually Does

Your model file is a template, not SQL. Compilation renders it to real SQL; the materialization then wraps that SQL in DDL. Three files, three stages, and only the third one ever reaches the warehouse.

4 min · Topic 1 of 7

Your first model in Chapter 2 read a hard-coded table name, which worked. This chapter removes that cheat, and the reason it matters requires understanding what happens between the file you write and the statement your warehouse receives.

Three files exist for every model: the one you wrote, the compiled SQL with all templating resolved, and the run SQL with DDL wrapped around it. Step through them below — the second one is the file you will open most often for the rest of your dbt career.

The same model, at three stages
parsecompilerun
models/marts/fct_listens.sql
{{ config(materialized='table') }}

select
    l.listen_id,
    l.user_id,
    s.show_title,
    l.ms_played
from {{ ref('int_listens_enriched') }} l
join {{ ref('dim_show') }} s using (show_id)
What you wroteJinja and SQL mixed together. This file is not valid SQL — no warehouse would accept it. It is a template.
Stage 1 of 3
Core mental model

Your model file is a template, not SQL. Compilation renders it to real SQL; the materialization then wraps that SQL in DDL. Three files, three stages, and only the third one ever reaches the warehouse.

Why it matters

Almost every “why is my model doing that?” has an immediate answer in `target/compiled/`, because that file is not an approximation — it is exactly what was sent. Reading it turns debugging from inference into observation, and it is the fastest habit you can form in your first month.

Parse
dbt reads every file in the project, discovers the nodes, and records the graph edges from ref() and source() calls. No SQL is generated and nothing is sent anywhere.
Compile
All Jinja is rendered and every ref()/source() becomes a fully-qualified table name. The result is plain SQL, written to target/compiled/.
Run
The compiled SQL is wrapped in whatever DDL the materialization requires and issued to the warehouse. Written to target/run/ so you can read exactly what executed.
Relation name
The database.schema.name a ref() resolves to. All three parts come from configuration and the active target, which is why the same file produces different names in dev, CI and production.
Common mistake

Trying to run a model file directly in a warehouse console to debug it. It is not valid SQL — the Jinja will be rejected. Run `dbt compile` and paste the file from target/compiled/ instead, which is guaranteed to be the same SQL dbt would issue.

Assuming compiled SQL differs between environments in some deep way. The only difference is usually the resolved schema. If a model works in dev and fails in prod, diff the two compiled files — the answer is nearly always visible in the first two lines.

Better habit

Run `dbt compile` whenever you are unsure. It costs nothing, touches no warehouse, and answers the question exactly rather than approximately.

When reviewing someone’s pull request on a complex templated model, ask for the compiled SQL. Reviewing Jinja is guessing; reviewing compiled SQL is reading.

Learn to tell compiled/ from run/ on sight: compiled/ is your SELECT, run/ is that SELECT inside a create statement.

Compilation is free

`dbt compile` opens no warehouse connection for the model SQL itself, so it costs nothing and is nearly instant. There is no reason to guess what a model will produce when you can look.

Remember this

A model file is a template. compiled/ is what it became; run/ is what was issued. When something surprises you, read the second one.