How dbt Works
dbt sits between your raw data and your final analytics tables. It transforms raw data inside your warehouse using SQL. Understanding the mechanics of how dbt works helps you use it effectively and troubleshoot problems quickly.
The ELT Pattern
dbt is built for the ELT pattern: Extract, Load, Transform. A different tool (like Fivetran, Airbyte, or a custom script) extracts data from source systems and loads it into the warehouse. dbt then handles the Transform step entirely inside the warehouse.
[Source Systems] [Warehouse] [Analytics] CRM Database ──E+L──> raw_customers ──dbt──> dim_customers Sales App ──E+L──> raw_orders ──dbt──> fct_orders Web Events ──E+L──> raw_events ──dbt──> fct_sessions
The "Transform" step is entirely SQL that you write. dbt compiles and runs that SQL inside your warehouse. Your data never leaves the warehouse.
The DAG: Dependency Graph
dbt builds a Directed Acyclic Graph (DAG) from your models. A DAG is a map that shows which models depend on other models and in what order dbt must run them.
What "Directed" Means
Data flows in one direction only. Model B receives data from Model A. Data never flows backward from B to A.
What "Acyclic" Means
There are no loops. Model A cannot depend on Model B while Model B also depends on Model A. dbt enforces this rule automatically.
raw_orders ──────┐
v
raw_customers ──> stg_customers ──> fct_orders ──> report_revenue
^
raw_products ────┘
dbt reads your ref() calls to build this graph automatically. You never manually specify run order.
How dbt Compiles SQL
Your model files use Jinja templating. Before running SQL, dbt compiles the Jinja into plain SQL.
Your model file (models/stg_orders.sql)
select
order_id,
customer_id,
order_date,
amount
from {{ source('raw', 'orders') }}
where order_date >= '2024-01-01'
After compilation (compiled/stg_orders.sql)
select
order_id,
customer_id,
order_date,
amount
from my_database.raw_schema.orders
where order_date >= '2024-01-01'
dbt replaces {{ source('raw', 'orders') }} with the actual database and schema name. The compiled SQL lives in the target/compiled/ folder so you can inspect it any time.
The dbt Run Lifecycle
When you run dbt run, dbt performs these steps in sequence:
Phase 1 – Parse
dbt reads all files in your project: .sql models, .yml config files, macros, and seeds. It checks for syntax errors and builds the dependency graph.
Phase 2 – Compile
dbt converts every Jinja template in your SQL files into plain SQL. It saves compiled versions to the target/compiled/ directory.
Phase 3 – Execute
dbt runs the compiled SQL in topological order (parents before children) against your warehouse. Each model creates or replaces a table or view depending on its materialization setting.
Phase 4 – Log results
dbt prints success or failure for each model to the terminal. It also writes detailed logs to logs/dbt.log.
dbt run | v [1] Parse project files | v [2] Build DAG | v [3] Compile Jinja → SQL | v [4] Execute SQL in warehouse (in DAG order) | v [5] Report results: OK / ERROR
How dbt Talks to the Warehouse
dbt uses a profile to connect to your warehouse. The profile lives in a file called profiles.yml on your computer. It stores credentials like host, port, username, password, and database name.
When you run any dbt command, dbt reads your project's dbt_project.yml to find the profile name, then looks up that profile in profiles.yml to get connection details.
dbt_project.yml
profile: my_warehouse ──> profiles.yml
my_warehouse:
target: dev
outputs:
dev:
type: snowflake
account: xyz.us-east-1
user: analyst
...
Materializations: How dbt Stores Results
dbt can store each model's output in different ways called materializations. The four main types are:
View
dbt creates a SQL view. No data is stored on disk. The view re-runs your SELECT every time someone queries it.
Table
dbt drops the old table and creates a new one by running your SELECT and storing the results on disk.
Incremental
dbt only adds new or changed rows to an existing table instead of rebuilding the entire table every run. Useful for large datasets.
Ephemeral
dbt never creates a database object. It inlines the model's SQL as a CTE inside models that reference it.
The target/ Folder
Every time you run dbt, it writes output to a folder called target/ inside your project. This folder contains:
target/compiled/— SQL after Jinja compilationtarget/run/— Final SQL that dbt sent to the warehouse (includes CREATE TABLE wrappers)target/manifest.json— A full description of every node in your DAGtarget/run_results.json— Success/failure data for each model from the last run
A Complete Mini Example
Suppose you have two models:
models/stg_customers.sql
select customer_id, name, email
from {{ source('raw', 'customers') }}
models/fct_orders.sql
select
o.order_id,
c.name as customer_name,
o.amount
from {{ source('raw', 'orders') }} o
join {{ ref('stg_customers') }} c
on o.customer_id = c.customer_id
When you run dbt run, dbt detects that fct_orders references stg_customers via ref(). It runs stg_customers first, then runs fct_orders. Your warehouse ends up with two new views (or tables) in the correct state, every time, automatically.
