← Back to list

Optimizing dbt Performance at Scale: Threading, Incrementals, Warehouse Tuning, and DAG Design

High-quality models are useless if they are too slow or too expensive to run at scale. As your dbt project grows—more models, more data…

Abhishek Kumar Gupta in Tech with Abhishek · 2025-11-26 08:52 · 100 claps · 6.6 min read paywalled
#big-data #dbt-performance #data-engineering #dbt #cicd-automation
Open on Medium ↗
Wiki topics: 🔧 · Data Engineering 📚 · Books & Reading

Optimizing dbt Performance at Scale: Threading, Incrementals, Warehouse Tuning, and DAG Design

Optimizing dbt for speed, cost, and scalability across modern cloud warehouses.

Optimizing dbt for speed, cost, and scalability across modern cloud warehouses.

High-quality models are useless if they are too slow or too expensive to run at scale. As your dbt project grows—more models, more data, more teams — the performance bottlenecks start showing up in long run times, failed CI pipelines, and rising warehouse bills.

This article is a one-stop guide to understanding and tuning dbt performance: from threads and incrementals to DAG shape, stateful runs, and warehouse-level optimisations.

The goal is simple: help analytics engineers ship faster, cheaper, and more reliable transformations without guesswork.

How dbt Actually Runs (And Where Performance Lives)

dbt itself is an orchestrator and compiler: it parses your project, builds a DAG, compiles SQL, and then sends queries to your warehouse. The warehouse then does the heavy lifting of executing queries and moving data.​

So when you talk about “dbt performance”, you’re really talking about three interacting layers:

dbt engine & graph:

  • Parse time, compilation time, DAG resolution.

Warehouse execution:

  • Query plans, joins, partitions, caches, IO, concurrency.

Orchestration & job design:

  • How many models you run together, how often, and in which dependency groups.

Fusion and modern dbt improvements reduce parsing/graph overhead, but if your SQL and warehouse settings are inefficient, you’ll still be slow.​

Threading and Concurrency: Getting Parallelism Right

Threads control how many dbt models can run in parallel. dbt builds a DAG of models and then executes as many independent paths as possible up to the thread limit.​

Too few threads:

  • You underutilise your warehouse and runs are slower.

Too many threads:

  • You hit warehouse concurrency limits, cause queueing, and sometimes degrade performance.

Configuring Threads in profiles.yml

Threads are set per target in profiles.yml (dbt Core) or in job settings (dbt Cloud).​

Example:

my_project:
  target: prod
  outputs:
    prod:
      type: snowflake
      account: your_account
      user: your_user
      role: TRANSFORMER
      database: ANALYTICS_PROD
      schema: DBT_PROD
      threads: 8  # tune this

Tuning threads controls how many models run in parallel and how hard dbt pushes your warehouse

Tuning threads controls how many models run in parallel and how hard dbt pushes your warehouse

Best practices:

  • Start around 4 threads, then benchmark; adjust up or down based on warehouse size and concurrency limits.​
  • Use different thread counts by environment if needed (e.g., fewer in shared dev, more in dedicated prod).
  • Remember you can override with --threads on the CLI for specific runs.​

For very large projects, combine threading with job partitioning: separate jobs for staging, core marts, and heavy aggregates that can run in parallel.​

Incremental Models: Your Main Performance Lever

Incremental models let dbt process only new or changed data instead of rebuilding entire tables, drastically reducing runtime and cost.​

A simple incremental pattern:

{{ config(
    materialized='incremental',
    unique_key='id'
) }}

select *
from {{ source('raw', 'events') }}
{% if is_incremental() %}
  where event_timestamp > (select max(event_timestamp) from {{ this }})
{% endif %}

Key pieces:

  • materialized='incremental' – tells dbt to reuse existing table and append/merge new rows.
  • unique_key – controls how dbt merges new data; required for many incremental strategies.
  • The is_incremental() block – limits source scans to deltas for incremental runs.​

Partitioning and Clustering for Big Tables

For large fact tables, combine incrementals with partition_by and cluster_by to limit scanned data and improve query performance.​

Example (BigQuery / Snowflake style):

{{ config(
    materialized='incremental',
    unique_key='order_id',
    partition_by={'field': 'order_date', 'data_type': 'date'},
    cluster_by=['customer_id'],
    incremental_strategy='merge',
    incremental_predicates=[
      "DBT_INTERNAL_DEST.order_date > dateadd(day, -7, current_date)"
    ]
) }}

select *
from {{ source('raw', 'orders') }}
{% if is_incremental() %}
  where order_date >= dateadd(day, -7, current_date)
{% endif %}

Patterns that scale:

  • Sliding window: recompute last X days/weeks to handle late-arriving data.
  • Watermark: use max timestamp or ID from target as lower bound.
  • Periodic full refresh: schedule a weekly/monthly --full-refresh for critical models to reset drift.

Done well, these patterns cut costs by orders of magnitude on warehouses like BigQuery and Snowflake.​

Incremental models with partitioning and sliding windows reduce scanned data and runtime on large tables.

Incremental models with partitioning and sliding windows reduce scanned data and runtime on large tables.

Model and DAG Design for Performance

The structure of your models and DAG has a huge impact on performance.

Layered Architecture

The common dbt pattern — sources → staging → core → marts — is not just for cleanliness; it’s a performance pattern.​

Staging:

  • Clean, type-cast, and lightly transform raw tables.

Core:

  • Model business entities (customers, orders, products).

Marts:

  • Denormalized, BI-friendly tables with aggregates.

Benefits:

  • Smaller, simpler models are easier for the warehouse optimizer.
  • You can cache heavy joins in staging/core tables instead of repeating them in every mart.​

Ephemeral vs Materialized

Ephemeral models are compiled into downstream queries as CTEs — no persistent table.​

Use ephemeral when:

  • Data volume is small/medium.
  • You want to avoid cluttering the warehouse with many intermediate tables.

Avoid ephemeral when:

  • The logic is reused across many heavy models.
  • The compiled SQL becomes huge and hard to optimize.

You can flip a model from ephemeral to table/view by changing config:

{{ config(materialized='table') }}

select ...

This extra I/O often pays off in simplified query plans and better cache reuse for very large datasets.​

A layered dbt architecture keeps transformations modular and makes performance tuning much easier

A layered dbt architecture keeps transformations modular and makes performance tuning much easier

Using Tags and Selectors to Control the DAG

Tags and selectors give you fine-grained control over what runs, which is crucial for performance in large projects.

Example dbt_project.yml snippet:

models:
  my_project:
    staging:
      +tags: ['staging']
    core:
      +tags: ['core']
    marts:
      +tags: ['marts', 'critical']

Useful commands:

  • Run only staging: dbt build --select tag:staging
  • Run only critical marts: dbt build --select tag:critical

Combine with graph operators:

  • Parents: +model_name
  • Children: model_name+
  • Both: +model_name+

Example:

# Run an impacted mart and all upstream dependencies
dbt build --select +fact_sales

This approach reduces unnecessary work and focuses compute where it matters.

Slim CI and Stateful Runs: Only Run What Changed

Running the full DAG on every pull request doesn’t scale. dbt’s state comparison methods let you run only modified nodes and their dependents.​

You use a previous one manifest.json as the baseline for comparison:

dbt build \
  --select state:modified+ \
  --state path/to/previous/artifacts \
  --target ci

Key concepts:

  • state:modified – selects resources whose definition or dependencies changed.
  • state:modified+ – also includes their downstream dependencies.
  • You can also use subselectors like state:modified.body or state:modified.configs for more control.​

This “Slim CI” pattern can cut CI times massively in large projects.​

Stateful selection runs only modified models and their dependents, cutting CI times in large projects

Stateful selection runs only modified models and their dependents, cutting CI times in large projects

Warehouse-Level Tuning: Snowflake, BigQuery, Databricks

dbt performance is tightly coupled with how your warehouse is configured and used.

Snowflake

  1. Size warehouses appropriately: Use larger warehouses for short, heavy runs; scale down when idle.​
  2. Leverage:
  • Clustering keys on large tables.
  • Micro-partition pruning.
  • Result caching for repeated queries.​

BigQuery

  • Always partition and cluster large tables.​
  • Avoid full table scans; use incremental predicates and filters.
  • Watch for “hot partitions” and optimize accordingly.

Databricks / Delta

  • Use OPTIMIZE and ZORDER on frequently queried tables.
  • Tune cluster size and auto-scaling for dbt jobs.
  • Lean on Delta’s file compaction features to keep IO in check.​

The pattern is the same: combine dbt configs (partition_by, cluster_by, incremental_strategy) with good warehouse hygiene.​

Finding and Fixing Slow Models with Artifacts

dbt’s run_results.json is a goldmine for performance analysis. It has status, execution time, and timing breakdowns for every node.​

From the docs:

Multiple run_results.json files can be aggregated to calculate:

  • Average model runtime.
  • Test failure rates.
  • Snapshot changes, etc.​

You can:

  • Load run_results.json into a table and build a performance dashboard.
  • Identify top N slowest models and prioritize refactoring.​

Example (conceptual model):

select
  unique_id,
  avg(execution_time) as avg_runtime_s,
  max(execution_time) as max_runtime_s,
  count(*) as run_count
from {{ ref('run_results_flat') }}
group by unique_id
order by avg_runtime_s desc
limit 20;

This shows where to focus your optimization efforts instead of guessing.

Analyzing dbt artifacts like run_results.json reveals your slowest models and long-term performance trends.

Analyzing dbt artifacts like run_results.json reveals your slowest models and long-term performance trends.

Practical Checklist for dbt Performance

Use this as a quick review for your project:

Threads:

  • Are threads tuned per environment and warehouse, not just set to an arbitrary high number?​

Incrementals:

  • Are your largest tables incremental, with correct unique keys and partitioning?​
  • Do you handle late-arriving data with sliding windows or periodic full refresh?

DAG & models:

  • Are heavy joins materialized once instead of repeated everywhere?
  • Are you using ephemeral only where appropriate?​

Selection & CI:

  • Are you using tags and state:modified for Slim CI instead of running the full DAG every time?​

Warehouse tuning:

  • Are big tables partitioned/clustered?
  • Do you monitor query plans and cache behavior in your warehouse?​

Observability:

  • Are you analyzing run_results.json to track slow models and trends over time?​

If you can check most of these off, your dbt performance is ahead of many teams.

🎯 Conclusion

Optimizing dbt performance is not a single tweak — it’s a mindset and a collection of patterns. Threads, incrementals, DAG design, Slim CI, and warehouse tuning work together. When handled thoughtfully, they turn dbt from “a lot of SQL files” into a fast, reliable, and cost-efficient transformation layer for your modern data stack.​

As your project scales, performance work stops being an optional “nice-to-have” and becomes part of the craft of analytics engineering.

💡 Final Thoughts

Teams that invest in dbt performance early unlock three compounding benefits: faster developer feedback, lower cloud bills, and more trust from the business. Instead of waiting an hour to learn a change broke something, you get answers in minutes — and you can iterate confidently.

👉 If you’ve discovered clever performance tricks — thread tuning, unusual incremental strategies, or warehouse-specific hacks — share them. The dbt community learns fastest when practitioners surface their real-world wins and war stories


메타데이터
post_id
8e77e08d461a
slug
optimizing-dbt-performance-at-scale-threading-incrementals-warehouse-tuning-and-dag-design-8e77e08d461a
url
https://medium.com/tech-with-abhishek/optimizing-dbt-performance-at-scale-threading-incrementals-warehouse-tuning-and-dag-design-8e77e08d461a
canonical_url
https://medium.com/tech-with-abhishek/optimizing-dbt-performance-at-scale-threading-incrementals-warehouse-tuning-and-dag-design-8e77e08d461a
author_url
https://medium.com/@abhishekkrgupta0
status
ok
fetched_at
2026-06-26 03:39:16