Data as of Aug 16, 2026 · Based on 307 AI responses from ChatGPT Search and Google AI Mode · See how Parse measures this
Brands already showing up
This promptYour brand can be here too.
When dbt runs start taking forever, the biggest wins usually come from finding where the time is going rather than blindly adding compute. A good tuning pass usually focuses on four areas: model materializations, SQL workload, DAG structure, and execution parallelism.
Start with your run artifacts:
target/run_results.jsonexecution_timeA 30-minute model at the top of the DAG can hurt more than 20 smaller slow models because everything waits on it.
A common cause of slow dbt runs is rebuilding large tables every time:
{{ config(materialized='table') }}
select *
from raw_events
For large append-heavy datasets, switch to incremental:
{{ config(
materialized='incremental',
unique_key='event_id'
) }}
select *
from raw_events
{% if is_incremental() %}
where updated_at >= (
select max(updated_at)
from {{ this }}
)
{% endif %}
This avoids reprocessing historical data on every run. dbt incremental models are specifically designed to process only new or changed rows instead of rebuilding everything.
Common candidates:
Be careful with:
unique_key choicesIncremental does not automatically mean fast.
Examples:
appendmergeFor very large time-series tables, microbatch-style processing can reduce the amount processed per query by splitting work into time windows.
A common pattern:
source
↓
staging view
↓
intermediate view
↓
another intermediate view
↓
fact table
Each view can force the warehouse to repeatedly execute upstream logic.
Consider:
Example:
{{ config(materialized='ephemeral') }}
select
customer_id,
lower(email) as email
from {{ ref('customers') }}
dbt can execute independent models concurrently using threads.
Check your profiles.yml:
target: prod
outputs:
prod:
type: snowflake
threads: 8
But more threads are not always faster.
Too many threads can:
Tune threads against your warehouse limits.
Look for:
A
|
B
|
C
|
D
|
E
This runs mostly sequentially.
You want more shapes like:
A
/ | \
B C D
\ | /
E
Ways to improve:
Your SQL may be the real bottleneck.
Check:
select * everywhere?Example improvement:
Before:
select *
from events
join users using(user_id)
After:
select
e.event_id,
e.user_id,
e.event_time,
u.country
from (
select *
from events
where event_date >= current_date - 7
) e
join users u using(user_id)
If your CI pipeline runs the entire project for every PR, use state comparison:
dbt build --select state:modified+
This limits builds to changed models and their downstream dependencies.
Audit your jobs:
Bad:
dbt run --full-refresh
every night.
Better:
Newer dbt versions include performance improvements in graph processing and incremental workflows. Keeping dbt Core and adapters current can help, especially for larger DAGs.
A practical tuning order I’d use:
If you share:
I can suggest more targeted changes.
Before:
select *
from events
join users using(user_id)
After:
select
e.event_id,
e.user_id,
e.event_time,
u.country
from (
select *
from events
where event_date >= current_date - 7
) e
join users u using(user_id)
If your CI pipeline runs the entire project for every PR, use state comparison:
dbt build --select state:modified+
This limits builds to changed models and their downstream dependencies.
Audit your jobs:
Bad:
dbt run --full-refresh
every night.
Better:
Newer dbt versions include performance improvements in graph processing and incremental workflows. Keeping dbt Core and adapters current can help, especially for larger DAGs.
A practical tuning order I’d use:
If you share:
Optimizing a sluggish dbt project requires attacking bottlenecks at both the warehouse layer and the dbt configuration layer. Here is a structured approach to speeding up your runs:
view or table to incremental . This ensures you only process new or updated rows instead of full table scans on every run.SELECT * with explicit column declarations to reduce memory overhead and data transfer.--select and State Comparison , Use --select state:modified+ via the --state flag in production CI/CD pipelines. This ensures you only build models that have actually changed or depend on changed upstream data, rather than rebuilding the entire DAG.threads in your profiles.yml file (matching your warehouse's concurrency limits) so independent models run in parallel rather than serially.ephemeral materializations for small, single-use transformations that act as simple code clean-ups, but avoid them for complex or heavily-reused logic which can bloat the compiled SQL.If you want to dive deeper, let me know:
Optimizing a dbt project for speed requires a mix of configuration tweaks, architectural adjustments, and database-level tuning. Here is a structured approach to speeding up your dbt runs:
incremental using the is_incremental() macro so they only process new or updated rows.--select and state : Avoid running your entire DAG every time. Use state-based execution like dbt run --select state:modified+ to run only models that have changed and their downstream dependencies (compare against a previous manifest artifact).ephemeral if they are referenced multiple times (which re-evaluates the CTE everywhere), and don't materialize everything as a table if a view or incremental model suffices.SELECT * , reduce expensive cross-joins or heavy window functions on raw data, and pre-aggregate early in your DAG.dbt docs generate adds overhead. Keep it out of your primary, high-frequency transformation pipelines unless explicitly needed.--select state:modified ) rather than a full refresh to drastically cut down CI/CD build times.If you'd like to dive deeper, tell me:
I can give you specific configuration snippets or warehouse-level tuning tips for your stack.
When dbt runs take forever, the biggest gains usually come from figuring out where the time is going first: model SQL execution, unnecessary model builds, warehouse contention, or dbt overhead. A good optimization pass usually looks like this:
Start with run timing data:
dbt run
Look at the models with the longest execution times. In many projects, a handful of models account for most of the runtime. dbt recommends analyzing model timing and focusing optimization effort on the biggest bottlenecks rather than tuning everything.
Also inspect:
target/run_results.jsonThe biggest win is often avoiding rebuilding large tables.
Before:
{{ config(materialized='table') }}
select *
from {{ source('app', 'events') }}
After:
{{
config(
materialized='incremental',
unique_key='event_id'
)
}}
select *
from {{ source('app', 'events') }}
{% if is_incremental() %}
where updated_at >= (
select max(updated_at)
from {{ this }}
)
{% endif %}
Incremental models process only new or changed data instead of recreating the entire dataset.
Common improvements:
updated_at or event timestamp columnmerge, append, delete+insert, etc.)full-refresh runsA common mistake is:
dbt run
for every change.
Instead:
dbt build --select my_model+
or:
dbt build --select state:modified+
State-based selection can avoid rebuilding unchanged parts of your project.
For CI pipelines, consider:
dbt build \
--select state:modified+ \
--defer \
--state path/to/prod/artifacts
This lets unchanged models resolve against production artifacts rather than rebuilding everything.
Look for:
Bad:
select *
from huge_table
Better:
select
id,
created_at,
customer_id
from huge_table
where created_at >= '2026-01-01'
Bad:
select *
from (
select *
from events
join users using(user_id)
)
where event_date >= current_date - 7
Better:
select *
from events e
join users u using(user_id)
where e.event_date >= current_date - 7
Push filters as close to the source as possible.
Typical patterns:
| Model type | Usually best materialization |
|---|---|
| Raw cleanup/staging | view |
| Large fact tables | incremental |
| One-use transformations | ephemeral |
| Final marts | table/incremental |
Be careful with long chains of views. A warehouse may repeatedly expand and execute complex SQL when querying downstream models.
Depending on your warehouse:
select *If your warehouse has capacity, increase dbt threads:
profiles.yml
target: prod
outputs:
prod:
type: snowflake
threads: 8
More threads help only when:
More threads can make things slower if you overload the warehouse.
If startup time is the issue (not SQL execution), enable partial parsing:
flags:
partial_parse: true
Partial parsing avoids reparsing unchanged project files and can significantly reduce startup time in large projects.
Tests can quietly add hours.
Look for:
relationships tests on huge tablesConsider:
dbt run of the entire DAGIf you share:
I can suggest a more targeted tuning plan.
Speeding up sluggish dbt (data build tool) runs requires a mix of configuration tweaks, architectural changes, and incremental processing. Here is a prioritized, scannable guide to optimizing your dbt project:
config(materialized='incremental') . Filter for only new or updated records (e.g., using updated_at > (SELECT MAX(updated_at) FROM {this}})) on subsequent runs, and only do a full refresh when necessary via the --full-refresh flag.config(materialized='ephemeral') . This injects the code as a CTE directly into the downstream queries, saving storage and creation overhead.SELECT * , redundant joins, or expensive window functions. On data warehouses like PostgreSQL, Snowflake, or BigQuery, ensure you are defining appropriate clustering keys, partition fields, or sorting keys in your model configs to minimize data scanning.profiles.yml file or via the CLI flag -t 8 (or higher, depending on your data warehouse's concurrency limits and CPU capacity).dbt run --select +my_model (upstream dependencies) or dbt run --select state:modified (using artifact comparison against a production manifest) to run only what changed.manifest.json using --select state:modified+ , you build only the models that were modified and their downstream children, drastically cutting down PR build times.warn severities for non-critical checks, scoping tests with where clauses (e.g., testing only active records), or using custom generic tests that run more efficiently.If you want to dive deeper, let me know:
I can give you more targeted, warehouse-specific recommendations or help you set up a Slim CI workflow.
The biggest dbt speedups usually come from building less data, running fewer models, and making the warehouse do less work. I’d optimize in this order:
Start by separating dbt overhead from warehouse query time.
Look at:
If a model takes 20 minutes because the warehouse query takes 20 minutes, changing dbt configuration won't fix the underlying SQL.
This is often the biggest win.
Instead of:
select *
from {{ ref('events') }}
use an incremental strategy that only processes new/changed records:
{{ config(
materialized='incremental',
unique_key='event_id'
) }}
select
event_id,
user_id,
event_timestamp,
event_type
from {{ ref('events') }}
{% if is_incremental() %}
where event_timestamp >= (
select max(event_timestamp)
from {{ this }}
)
{% endif %}
The important part isn't merely setting materialized='incremental': your SQL needs to avoid scanning/reprocessing the historical dataset.
Also consider your warehouse's best incremental strategy—e.g. merge, delete+insert, or insert-overwrite—and partition/cluster the underlying data appropriately.
Avoid carrying billions of rows through intermediate CTEs only to filter them at the end.
Prefer:
with events as (
select *
from {{ ref('events') }}
where event_date >= current_date - 30
),
users as (
select *
from {{ ref('users') }}
where is_active = true
)
...
over joining massive unfiltered relations and applying predicates afterward.
Also avoid select * in production models when you only need a subset of columns.
A common anti-pattern is having many downstream models independently perform the same expensive transformation.
For example:
raw_events
├── model_a → expensive deduplication
├── model_b → expensive deduplication
└── model_c → expensive deduplication
Instead:
raw_events
↓
deduped_events
├── model_a
├── model_b
└── model_c
Materializing an expensive shared transformation once can dramatically reduce total warehouse work.
The opposite can also happen: creating dozens of unnecessary intermediate tables can increase I/O. Benchmark rather than blindly materializing everything.
dbt can execute independent DAG branches concurrently. If your project has:
A → B → C → D
there isn't much dbt can parallelize.
But:
→ B →
A → → E
→ C →
→ D →
gives the runner more opportunity to execute work concurrently.
Look for unnecessary dependencies such as models referencing another model merely because it was convenient, rather than because the data dependency is real.
Also check your warehouse's concurrency limits—raising dbt's thread count isn't necessarily faster if it causes warehouse contention.
Don't rebuild the entire project for every pull request.
dbt's current documentation highlights dbt State as a way to skip unnecessary rebuilds by identifying nodes whose code/data haven't meaningfully changed.
A typical CI pattern is conceptually:
dbt build --select state:modified+
The + is important: it includes downstream dependencies of modified nodes.
For CI, combine this with a previous production manifest/state directory so a PR only builds the affected portion of the DAG.
Developers shouldn't routinely run the entire warehouse.
Useful patterns include:
dbt build --select my_model+
and limiting development data with a variable or environment-aware filter:
where event_date >= current_date - 7
You can also use --defer with a production manifest so unchanged upstream models can resolve to existing production relations instead of being rebuilt locally.
This is often more important than dbt itself.
Check for:
distinct over huge datasetsorder byFor example, this can be disastrous on a large table:
where cast(event_timestamp as date) = current_date - 1
whereas filtering directly on a partition-friendly timestamp/date column can allow the warehouse to prune most of the table.
A surprisingly slow dbt project can actually be a testing problem.
Tests such as uniqueness and relationships can scan enormous tables. Consider:
If your project has thousands of models/macros, local startup/parsing can become noticeable. dbt supports partial parsing, which avoids unnecessarily reparsing the entire project when only limited files have changed.
If parsing is a significant portion of your runtime, also inspect:
dbt's current tooling also emphasizes faster parsing/development through its Fusion engine and VS Code integration.
If you give me your warehouse (Snowflake, BigQuery, Databricks, Redshift, Postgres, etc.), roughly how many models you have, and a snippet of your slowest model, I can help you identify the highest-impact changes.
A good optimization exercise is:
| Area | What to measure | Typical opportunity |
|---|---|---|
| Model SQL | Query execution time | ⭐⭐⭐⭐⭐ |
| Incrementalization | Rows processed vs. rows needed | ⭐⭐⭐⭐⭐ |
| DAG | Parallelism / unnecessary dependencies | ⭐⭐⭐ |
| CI selection | Models rebuilt per PR | ⭐⭐⭐⭐ |
| Tests | Time spent testing | ⭐⭐⭐ |
| Warehouse config | Scans, partitions, concurrency | ⭐⭐⭐⭐⭐ |
| dbt parsing | Startup/compile time | ⭐⭐ |
The key principle: don't start by tweaking threads. First determine whether you're spending the time parsing dbt, waiting on the warehouse, rebuilding unnecessary data, or running an unnecessarily large DAG. Then optimize that bottleneck.
Slow dbt runs are usually caused by one (or more) of these: too much data being reprocessed, inefficient SQL, poor DAG design, warehouse constraints, or running more models than necessary. dbt itself mostly orchestrates SQL execution, so the biggest wins usually come from your models and warehouse.
Here’s a practical optimization checklist:
Run with timing enabled and identify the bottlenecks:
dbt run --select model_name --log-level debug
Look for:
A model that takes 20 minutes but feeds 2 models may matter less than a 5-minute model feeding 100 models.
If you rebuild large tables every run, incremental models are often the biggest improvement. They process only new/changed rows instead of rebuilding everything.
Example:
{{ config(
materialized='incremental',
unique_key='id'
) }}
select *
from {{ ref('events') }}
{% if is_incremental() %}
where updated_at > (
select max(updated_at)
from {{ this }}
)
{% endif %}
Good candidates:
Poor candidates:
A common slow pattern:
source
↓
view
↓
view
↓
view
↓
table
The warehouse may have to execute the entire chain when the final table runs.
For complex, frequently reused models, consider:
models:
analytics:
staging:
+materialized: table
or:
{{ config(materialized='table') }}
Long chains of views can hurt performance because computation is deferred until downstream queries run.
Avoid:
dbt run
for every development change.
Use selection:
dbt run --select my_model+
Runs your model plus downstream dependencies.
Or:
dbt run --select +my_model
Runs upstream dependencies.
For CI, consider state-based selection:
dbt build --select state:modified+
so unchanged models do not rebuild.
Common wins:
Slow:
select *
from huge_table
join another_table
Better:
select *
from (
select *
from huge_table
where event_date >= current_date - 30
)
join another_table
select *Instead of:
select *
use:
select
customer_id,
created_at,
revenue
If several models calculate the same expensive transformation, materialize it once.
Check for:
Example problem:
fact_orders
join customer_events
If both sides have multiple rows per customer, your row count can explode.
Depending on your platform:
Consider:
Snowflake specifically recommends incremental models for large frequently updated tables because they avoid rebuilding entire datasets.
Consider:
Consider:
dbt can run models concurrently:
profiles.yml
outputs:
prod:
threads: 8
More threads can reduce wall-clock time, but only if:
Too many threads can make everything slower by competing for resources.
Large projects often spend significant time testing.
Instead of:
dbt test
on everything every time, consider:
A healthy DAG looks like:
A
/ | \
B C D
/ \ |
E F G
A slow DAG looks like:
A → B → C → D → E → F → G
Long dependency chains prevent parallel execution.
--select and state-based CI runs.If you share your warehouse (Snowflake/BigQuery/Redshift/Databricks), number of models, and a sample of your slowest model SQL, I can suggest more targeted changes.
🐢 Slow dbt runs can be a massive bottleneck, but there are several high-impact strategies you can implement to speed things up significantly.
Here is a structured checklist to optimize your dbt project for speed:
view or table to incremental . Views recompute on every downstream query, and full tables rebuild from scratch every run. Incremental models only process new or updated data.ephemeral materialization for lightweight, single-use transformations (like simple renames or filters). They get inlined as CTEs into downstream queries, saving you from cluttering your data warehouse with physical tables or views.--select state:modified ): Use dbt's state comparison to run or build only the models that have actually changed since your last production run. You can pass the artifact directory from your last production run using --state ./path/to/artifact and run dbt build --select state:modified+.--defer ): When developing locally or running in CI, use --defer to point your environment at a production database/schema for unmodified models. Instead of building upstream dependencies locally, your local run will query the existing production relations. You can configure this easily using the dbt CLI Guide.profiles.yml file. By default, dbt runs on 1 or 4 threads, but most modern data warehouses (like Snowflake, BigQuery, or Databricks) can easily handle 8, 16, or more concurrent queries. Just ensure your warehouse warehouse-size or concurrency limits can support it.ref() : Audit your dependency graphs (using dbt docs generate and dbt docs serve ) to eliminate redundant JOIN s, expensive window functions early in the DAG, or loops that cause unnecessary fan-outs.To help narrow down the best fix, could you share:
When a dbt project starts taking forever, the culprit is usually a mix of inefficient materialization strategies, poor concurrency settings, or a bloated Directed Acyclic Graph (DAG).
To get your run times down quickly, tackle the bottlenecks in this prioritized order:
profiles.yml or via the --threads CLI flag. By default, dbt might be running on 1 or 4 threads, meaning models execute sequentially rather than in parallel.run_results.json to find your 80/20 bottleneck
run_results.json artifact generated after a run (or use the Model Timing tab in dbt Cloud ). Typically, 10% of your models account for 90% of the runtime. Drop those specific slow queries into your data warehouse's native query analyzer/profile to see where the heavy scans or shuffles happen.full-refresh on massive, append-only fact tables on every run, you are wasting massive amounts of compute. Switch those models to materialized='incremental' so they only process new or updated rows. Ensure you write clean is_incremental() blocks or use the newer microbatch strategies to avoid accidental full-table scans.config blocks. Use partition_by (BigQuery/Databricks) or cluster_by (Snowflake) configurations. This allows the database engine to prune irrelevant data blocks instead of executing full table scans on every downstream join.SELECT * and Filter Early
WHERE filters as early as possible in your CTEs to shrink the dataset before heavy joins and aggregations occur.unique, not_null, relationships ) across millions of rows on every single build adds significant overhead. Restrict tests to critical paths, or scope them down using config blocks or customized where clauses so they don't re-scan entire historical tables.If you want to narrow this down, let me know:
I can give you specific adapter configurations or point you toward profiling strategies for your environment.
The biggest dbt speed wins usually come from reducing how much SQL runs, how much data each model scans, and how much of the DAG you rebuild. dbt itself mostly orchestrates SQL; your warehouse does the heavy lifting.
Here’s a practical optimization checklist:
Start by measuring before changing things:
dbt build --select state:modified+
and inspect:
target/run_results.jsonLook for:
A common dbt anti-pattern is a long chain of views:
source
↓
view
↓
view
↓
view
↓
final table
The warehouse may have to execute the entire chain every time. Materializing expensive intermediate models as tables or incremental models can dramatically reduce runtime.
Example:
{{ config(
materialized='table'
) }}
select *
from {{ ref('stg_orders') }}
For large fact models:
{{ config(
materialized='incremental',
unique_key='order_id'
) }}
select *
from {{ ref('orders_source') }}
{% if is_incremental() %}
where updated_at >
(
select max(updated_at)
from {{ this }}
)
{% endif %}
Incremental models can avoid reprocessing the full dataset by only handling new or changed rows.
Instead of:
dbt build
use targeted selection:
dbt build --select my_model+
or:
dbt build --select +my_model
Useful selectors:
| Goal | Command |
|---|---|
| Model + downstream | model_name+ |
| Model + upstream | +model_name |
| Modified models | state:modified |
| Tag-based runs | tag:finance |
Large dbt projects often improve developer speed by running only relevant graph sections rather than the entire project.
Bad:
model_a
joins huge_customer_table
model_b
joins huge_customer_table
model_c
joins huge_customer_table
Better:
huge_customer_table
↓
dim_customer
↓
model_a
model_b
model_c
Compute expensive transformations once.
Slow:
select *
from huge_events
join users using(user_id)
where event_date >= '2026-01-01'
Better:
select *
from (
select *
from huge_events
where event_date >= '2026-01-01'
) events
join users using(user_id)
select *Instead:
select
order_id,
customer_id,
created_at
from {{ ref('orders') }}
This reduces unnecessary scanning and downstream data movement.
If your warehouse has capacity, increase dbt threads:
profiles.yml
outputs:
prod:
type: snowflake
threads: 8
More threads allow independent models to run concurrently. Snowflake specifically recommends tuning threads based on warehouse capacity.
Don't blindly increase it—too many threads can cause warehouse contention.
Different workloads need different approaches:
Example:
Use:
incremental_strategy: append
Example:
Use:
incremental_strategy: merge
unique_key: customer_id
Use warehouse-specific optimizations:
Tests can become a hidden bottleneck.
Heavy examples:
relationships on billion-row tablesConsider:
A fast dbt architecture usually looks like:
sources
|
↓
staging (views)
|
↓
intermediate (tables/incremental)
|
↓
marts (tables/incremental)
Avoid making every layer a view.
Sometimes dbt is fine and the warehouse is slow.
Look for:
If I inherited a slow dbt project, I’d check in this order:
If you share:
I can suggest more targeted changes.