1. Find the slow models
jq -r '.results[] | [.execution_time, .unique_id] | @tsv' target/run_results.json | sort -rn | head -20
Also look at warehouse query history for dbt queries (query tags help: +query_tag: dbt_{{ model.name }} on Snowflake). Usually 10% of models take 80% of time and cost.
2. Materialisations on purpose
| Materialisation | Use for |
|---|---|
view | Light transformations, rarely queried, small upstream |
table | Small/medium models queried often |
incremental | Large fact tables, append-mostly or with reliable change keys |
ephemeral | Simple CTE helpers (avoid deep nesting, which bloats SQL) |
materialized_view / dynamic tables | Where the platform maintains it efficiently and freshness rules fit |
A common default: staging as views, marts as tables, big facts incremental.
3. Incremental models with the right strategy
{{ config(materialized='incremental', unique_key='order_id', incremental_strategy='merge',
on_schema_change='append_new_columns') }}
select * from {{ ref('stg_orders') }}
{% if is_incremental() %}
where updated_at > (select coalesce(max(updated_at), '1900-01-01') from {{ this }})
{% endif %}
| Strategy | When |
|---|---|
append | Immutable events, no updates |
merge | Updates by key (watch merge cost on huge tables) |
delete+insert | Platforms where merge is slow; replace matching keys |
insert_overwrite | Partitioned tables: replace changed partitions only |
microbatch (dbt 1.9+) | Time-series facts processed in time batches, easy backfills |
Add a lookback window (e.g. updated_at > max - interval '3 days') for late-arriving data, and protect history from --full-refresh on critical models (full_refresh: false; see the full-refresh Sev1 post).
4. Slim CI
Don't rebuild the whole project for every pull request:
dbt build --select state:modified+ --defer --state ./prod-artifacts --target ci
state:modified+: changed models and everything downstream.--defer: unchanged upstream models are read from production instead of rebuilt.- Keep production
manifest.jsonfrom the last deploy as the state artifact. - CI schema per PR, dropped afterwards.
5. Tests that don't cost a fortune
- Tests on big tables: use
whereconfigs to test recent data only (config(where="order_date >= current_date - 7")). - Prefer
unique/not_nullon keys of incremental models, run selectively. - Store failures only where you investigate them.
6. Threads and compute
threads: enough to run independent models in parallel (often 8–16), limited by warehouse concurrency.- Separate warehouse/cluster for dbt, sized by the duration you need (see the Snowflake warehouse post).
- Schedule heavy rebuilds (weekly full refresh of rebuildable models) off-peak.
7. Model hygiene
- Remove unused models (no downstream refs, no exposures, no queries in 90 days).
- Avoid
select *in marts; select only needed columns. - Push filters early (staging) to shrink data before joins.
Tip
Track total run time and warehouse credits per dbt job over time. Every slow model you fix shows up in that chart, and a regression after a PR is easy to spot.
Checklist
- Slowest models identified from run_results
- Materialisations reviewed; big facts incremental
- Correct incremental strategy with lookback and full-refresh protection
- Slim CI with state:modified+ and defer
- Tests scoped on big tables
- Threads, compute and unused models reviewed