skip to content

In dbt, what does compilation turn {{ ref('stg_orders') }} into, and where is the compiled SQL written?

level: middleimportance: should knowfreq 58%

answer

  1. templating leaves before SQL arrives
  2. three phases, not one
  3. there is a folder full of the answer
  4. the name comes out of the manifest, per target

basics

~20 s

Compilation renders the Jinja and replaces the call with the referenced model's fully qualified relation for the active target. The plain SELECT is written under target/compiled; the wrapped DDL dbt actually executes is written under target/run when the model builds.

solid answer

~40 s

dbt parses every file first, building a **manifest** of nodes and their configs. Compiling then renders each model's Jinja against that manifest: `{{ ref('stg_orders') }}` is looked up by model name and replaced with the relation address — database, schema and identifier — that `stg_orders` resolves to for the **current target**, honouring any `database`, `schema` or `alias` config. The result is ordinary warehouse SQL with no templating left. `dbt compile` writes it to `target/compiled/<project>/models/...`, which is the file you read when you want to see exactly what will run; when the model is actually built, the materialization's wrapper — the `create table as` or the merge — lands under `target/run/`. Nothing is sent to the warehouse by `dbt compile` beyond metadata queries, which makes it the cheapest way to debug templated SQL.

code

bash · 2 lines
bash
dbt compile --select fct_orders
cat target/compiled/jaffle_shop/models/marts/fct_orders.sql

go deeper

for a junior

Know that Jinja is rendered before any SQL reaches the warehouse, and that target/compiled holds the rendered query you can copy into a console.

for a middle

Walk through parse, compile and run, and explain that ref() resolves through the manifest to a database, schema and identifier for the active target. Name where each artifact is written.

for a senior

Use compilation as a diagnostic reflex: read the compiled file before theorising, and know that manifest.json is what state-based CI selection diffs against.

for a principal

Treat compilation artifacts as the project's contract surface — manifest-driven lineage, state selection and docs all depend on them, so choices like overriding the schema-name macro have project-wide consequences worth deciding deliberately.

## Parse, compile, run dbt executes a project in three distinct phases, and conflating them is the usual source of confusion. **Parse.** dbt reads every file in the project — models, YAML, macros, seeds, snapshots — and builds the **manifest**: an in-memory (and on-disk, at `target/manifest.json`) catalogue of every node, its configuration, and its dependencies. Dependencies come from statically extracting `ref()` and `source()` calls. This phase is where duplicate model names, missing references and dependency cycles are caught, before a single query is sent anywhere. **Compile.** For each selected node, dbt renders the Jinja in the file. `{{ config(...) }}` calls contribute settings and render to nothing. `{{ ref('stg_orders') }}` is resolved by looking `stg_orders` up in the manifest and asking for its relation, which is then rendered as a fully qualified, quoted name for the adapter's dialect. Macros are expanded. Variables from `vars` are substituted. What comes out the other side is plain SQL with no templating left in it. **Run.** dbt takes the compiled `SELECT`, hands it to the materialization macro for the model, and the macro emits the statements that actually execute — a `create or replace view`, a `create table as`, or the incremental strategy's temp-table-plus-merge sequence. ## What a ref resolves to A `ref()` does not resolve to the model's *name*; it resolves to its **relation**, which has three parts: - **database** — from the target, unless the model or its directory sets a `database` config, in which case a macro composes the final value. - **schema** — from the target, similarly composable; by default a model configured with a custom `schema` lands in `<target_schema>_<custom_schema>`, because the default schema-name macro concatenates rather than replaces. Projects frequently override that macro. - **identifier** — the model name, or the `alias` config if one is set. So the same `{{ ref('stg_orders') }}` can compile to a personal dev schema, a CI schema, or the production one, from an unchanged file. This is precisely why hard-coding a name is fatal to environment separation. ## Where the output lands After `dbt compile` (or any command that compiles), look in the `target/` directory: - `target/compiled/<project>/models/<path>/<model>.sql` — the rendered `SELECT`, exactly as you wrote it but with Jinja gone. This is what you paste into a query console to debug. - `target/run/<project>/models/<path>/<model>.sql` — populated when the model is built, containing the full statement including the materialization's DDL wrapper. - `target/manifest.json` — the full node graph and configuration, and the artifact that state-based selection compares against. - `target/run_results.json` — timings and statuses from the last execution. A useful habit: when a model produces wrong results, read the compiled file *first*. Nine times in ten the bug is visible there — a variable that rendered empty, an `is_incremental()` branch that took the path you did not expect, or a ref pointing at a schema you did not intend. ## The ephemeral exception One materialization changes what `ref()` produces. An **ephemeral** model is never built as a relation; instead, a `ref()` to it inlines that model's compiled SQL as a common table expression in the referencing model. So in the compiled output you will see a `with __dbt__cte__my_model as (...)` block rather than a table name. This is worth knowing because it explains why an ephemeral model cannot be queried directly in the warehouse and why an error inside one surfaces in its consumer's compiled SQL. ## Compilation is cheap and offline-ish `dbt compile` does not build anything. It connects to the warehouse only for metadata it needs (and later dbt versions can compile without a connection in some configurations), which makes it fast enough to run on every save. Two adjacent commands are worth knowing: `dbt ls` prints which nodes a selector resolves to without compiling deeply, and `dbt show --select my_model` compiles a model and previews a small result set — handy for checking output shape without materialising anything. ## Why interviewers ask The question checks whether you understand that dbt is a **SQL compiler**, not a runtime that interprets your query. Candidates who have only run `dbt run` from a scheduler often assume the warehouse somehow understands `ref()`. Knowing the parse/compile/run split, knowing where the rendered file lives, and reaching for it as a debugging tool is the tell of someone who has actually worked in a project.

  • Which artifact records the dependency graph, and what else uses it?
    `target/manifest.json`. It holds every node, its config, its compiled SQL and its dependencies. Documentation is generated from it, selection operators traverse it, and state-based selection such as `state:modified+` compares the current manifest against a stored one from a previous run to find what changed.
  • Why is reading target/compiled/ the fastest way to debug a templated model?
    Because it shows the exact SQL dbt will send, with every macro expanded, every variable substituted and every conditional branch already taken. Most puzzling results — an empty var, an incremental filter that did not apply, a ref resolving to the wrong schema — are visible on sight there, and the file can be pasted straight into a query console.
  • Does dbt compile run any queries against the warehouse?
    It builds nothing. It may issue metadata queries — for instance to introspect existing relations when a macro asks — but no model DDL or DML is executed, so compiling is safe on production credentials and fast enough to run on every file save.

saying these in an interview costs you the question

  • Thinks the warehouse itself understands ref() or Jinja
  • Says compiled SQL is only visible in logs
  • Believes ref() always inlines the upstream model's SQL
  • Cannot distinguish target/compiled from target/run
  • Assumes dbt compile builds tables

context