You run `dbt run` on a model that is five lines of SELECT. Describe what dbt does between reading that file and the table existing.
The project — work out what dbt does with it before reading on
The source rows
| order_id | customer_id | amount | status | updated_at |
|---|---|---|---|---|
| 1 | 10 | 50 | shipped | 2026-03-01 09:00:00 |
| 2 | 11 | 30 | pending | 2026-03-01 10:00:00 |
| 3 | 10 | 70 | shipped | 2026-03-01 11:00:00 |
The project
models/stg_orders.sql
Five lines of SQL and one line of Jinja. Nothing here says CREATE.
{{ config(materialized='table') }}
select
order_id,
customer_id,
amount,
status,
updated_at
from {{ ref('raw_orders') }}
What gets run
load the sourcedbt seed
build the modeldbt run --select stg_orders
Why they ask this
It is the first question in most dbt interviews and it separates people who have used dbt from people who understand it. Everything downstream — incremental behaviour, why a macro cannot loop over query results, why dbt cannot load data — follows from this one answer.
Say this
dbt renders the Jinja in your file to produce plain SQL, wraps that SQL in the DDL the materialization calls for, and sends the result to the warehouse over a normal connection. It computes nothing itself.
The reasoning
Three steps, and it is worth naming all three. First dbt parses the whole project and renders the Jinja: `{{ ref(...) }}` becomes a fully-qualified relation, `{% if %}` picks a branch, macros expand. The output is a plain SQL SELECT with no Jinja left in it, and it is written to `target/compiled/`.
Second, the materialization wraps that SELECT. This is the part people leave out. Your file never said CREATE — the `table` materialization is itself a macro that emits `create table ... as (<your select>)`, and it writes that to `target/run/`. Different materializations wrap the identical SELECT in completely different statements, which is why the same model can be a view, a table or an incremental merge without one character of the SELECT changing.
Third, dbt opens a connection to the warehouse and executes that statement. That is the whole of it. dbt has no execution engine, holds no data in memory, and does not stream rows anywhere — the warehouse does one hundred percent of the computation. This is why 'dbt is slow' is almost always 'my warehouse is doing something expensive', and why the fix is nearly always in the SQL rather than in dbt.
The practical payoff is knowing which file to open. If the numbers are wrong, read `target/compiled/` — that is your logic with the Jinja resolved. If the object in the warehouse is the wrong shape, or the run failed with a database error, read `target/run/` — that is the statement the warehouse actually received.
What dbt did — 2 commands, in order run on dbt-core 1.12.2 / duckdb
dbt-core 1.12 against duckdb. The DDL is adapter-specific; the two-step shape is not.
load the sourcedbt seed
build the modeldbt run --select stg_orders
Compiled SQLmodels/stg_orders.sql
select
order_id,
customer_id,
amount,
status,
updated_at
from "analytics"."main"."raw_orders"What dbt actually ranmodels/stg_orders.sql
create table
"analytics"."main"."stg_orders__dbt_tmp"
as (
select
order_id,
customer_id,
amount,
status,
updated_at
from "analytics"."main"."raw_orders"
);The warehouse now holds
| order_id | customer_id | amount | status | updated_at |
|---|---|---|---|---|
| 1 | 10 | 50 | shipped | 2026-03-01 09:00:00 |
| 2 | 11 | 30 | pending | 2026-03-01 10:00:00 |
| 3 | 10 | 70 | shipped | 2026-03-01 11:00:00 |
The answer most people give
"dbt extracts the data, transforms it and loads it." dbt does only the T, and even then it does not do the transforming — it generates SQL and the warehouse transforms. Candidates who say this usually cannot then explain why dbt has no connector catalogue.
They’ll ask next
So what happens if two developers run the same model at the same time against the same target?
