BigQuery Shuffle Joins in dbt: Fix the Hidden Cost in 2026
Shuffle joins in BigQuery dbt models are one of the most costly and least visible performance problems a data team can have. Here is how to find and fix them.
BigQuery Shuffle Joins in dbt Models Are Costing You More Than You Think
Unoptimised shuffle joins in dbt BigQuery models silently inflate your cloud bill and slow your pipelines — often without a single alert being triggered. When BigQuery cannot broadcast a smaller table to each worker, it falls back to a shuffle join, redistributing rows from both sides of the join across the network. At scale, that shuffle is expensive. The fix is rarely complicated, but you have to know where to look.
This is one of those problems that hides in plain sight. A model runs. It completes. No test fails. But behind the scenes, BigQuery has just moved terabytes of data across its internal network to complete a join that could have been resolved in a fraction of the time — and a fraction of the cost — with a few targeted changes. We see this pattern repeatedly across fast-growing companies on BigQuery, and it is almost always invisible until the bill arrives.
In 2026, BigQuery charges $6.25 per TB scanned on on-demand pricing. That number alone does not convey the real exposure. The actual damage comes from large, unfiltered shuffle joins that push bytes across your cluster and consume slot-hours that compound across every dbt run. This post explains exactly how BigQuery chooses its join strategy, where dbt models make it worse, and what to do about it.
How Does BigQuery Choose Between a Shuffle Join and a Broadcast Join?
Before you can fix a shuffle join problem, you need to understand why BigQuery chose a shuffle join in the first place.
BigQuery supports two primary join types: a hash join (also called a shuffle join) and a broadcast join. The shuffle join is used when two large tables are being joined together — BigQuery uses a hash function to redistribute rows from both tables so that matching keys end up on the same worker, enabling the join to happen locally.
If one of the tables is small enough to fit in memory, BigQuery skips the shuffle entirely and uses a broadcast join instead — sending the small table to every worker that is processing the large table. This is dramatically cheaper: no network redistribution of your fact table rows, no slot-time consumed on data movement.
BigQuery charges based on slot time consumed, which directly relates to the computational work performed. A shuffle join on two large tables consumes more slots because every row from both tables needs to be hashed, redistributed, and processed. A broadcast join concentrates the network overhead on the small table while allowing the large table to be processed in place.
The problem is that BigQuery makes this decision based on what the query engine sees at execution time. In dbt, the way you structure your models — what you materialise, how you filter, what columns you select — directly influences which strategy BigQuery picks. And most teams have no idea their models are forcing a shuffle join on every run.

Why dbt Models Make Shuffle Joins Worse
The structural patterns that make dbt models easy to write are also the ones that most reliably trigger expensive shuffle joins. Here are the four we encounter most often in practice.
1. Joining unfiltered CTEs of large staging models
The most common offender is a dbt model that references two large staging or intermediate models and joins them without pre-filtering. Because dbt compiles CTEs into a single SQL statement, BigQuery sees both full table scans hitting the join simultaneously. There is no partition pruning on the join side, no row reduction before the shuffle, just the full dataset from both sides crossing the network.
A pattern we see repeatedly: a payments model joins a full stg_transactions table (hundreds of millions of rows, months of history) to a stg_users table that has been built without any filters. The CTE for users returns every row ever written. BigQuery picks a shuffle join because neither side looks small. The fix — filtering stg_users to active or relevant records before the join, or materialising it with appropriate clustering — drops the shuffle volume dramatically.
2. Missing clustering on join keys
Clustering both tables on the keys being joined allows BigQuery to prune irrelevant data blocks, significantly reducing the scan size. When your join keys are not the clustering keys — or when there are no clustering keys at all — BigQuery has to scan the full table to find matching rows before it can even start the shuffle. This doubles the problem: you pay for the full scan and for the shuffle.
Clustering on join or filter keys improves locality. When BigQuery shuffles on join keys, clustered inputs reduce shuffle volume and speed up the hash build and probe phases.
In dbt, you set clustering on materialised models through the cluster_by config. If your intermediate and mart models are not configured with cluster_by on the columns they are most commonly joined on, you are leaving this optimisation on the table entirely.
3. Joining before filtering
Do not wait until after the join to narrow down your results. By applying WHERE filters to individual tables before joining them, you significantly decrease the volume of data being processed. In a dbt model, this means filtering inside a CTE before the join clause — not in a WHERE condition applied to the joined result.
This is a subtle but important distinction. When the filter sits after the join, BigQuery must complete the full shuffle before discarding rows. When it sits inside the CTE, BigQuery can reduce the row count before any shuffling begins. The execution plan looks identical to the analyst writing the SQL, but the slot consumption is vastly different.
4. Joining tables with high-cardinality skewed keys
A hidden danger in shuffle joins is data skew. Hash partitioning assumes data distributes evenly across nodes. If a specific join key appears disproportionately often — a NULL value, a generic fallback ID, or a dominant merchant — all rows matching that key land on a single node, creating a straggler task where one worker carries the entire load while others sit idle.
This is particularly brutal in fintech and payments pipelines where a small number of enterprise merchants or top-tier users can account for 60–70% of transaction volume. The shuffle completes quickly for the long tail. But that one high-volume key forces a single worker to process millions of rows while the rest of the cluster waits. Your model appears to run slowly "for no reason" — and the reason is skew.
How to Diagnose a Shuffle Join Problem in Your dbt Models
You cannot fix what you cannot see. The diagnosis workflow has three layers.
Layer 1: The execution plan in the BigQuery console
Open the query that underpins your dbt model in the BigQuery console and look at the execution plan. Each stage of the execution plan shows input rows, output rows, compute time, slot time consumed, and wait time. Look for JOIN stages with high slot time relative to the number of output rows — this signals the join is working harder than the result justifies. BigQuery execution stages will show Repartitions for shuffle operations. Any stage showing a repartition step is shuffling data across the network.
Layer 2: INFORMATION_SCHEMA.JOBS
For systematic diagnosis across your entire dbt project, query INFORMATION_SCHEMA.JOBS to identify which models consume the most slot-time and bytes billed. If you are not already doing this, you can read our detailed guide on BigQuery INFORMATION_SCHEMA cost attribution — it covers exactly how to build this kind of query-level visibility. Once you have a ranked list of your most expensive models, cross-reference with the execution plan to confirm whether a shuffle join is the culprit.
Layer 3: Row counts in vs. row counts out
Large input rows but tiny output rows is a good sign — filtering is working. The inverse — where input rows and output rows are similar in magnitude — tells you the join is not reducing data before it shuffles. Any join stage where the input row count looks close to the output row count, and both are large, is a candidate for intervention.
If you are looking to implement a systematic approach to query cost visibility and dbt model governance across your stack, explore how Fintel Analytics approaches this — we work with growth-stage businesses globally to design and deliver exactly this kind of cost-aware data engineering practice.

The Fix: A Practical Playbook for Shuffle Join Optimisation in dbt
Once you have identified the offending models, the fix generally follows one of five paths.
1. Pre-filter CTEs before the join
Move your WHERE conditions inside the CTE, not after the join. A model that filters on event_date after joining a 500M-row events table to a 100M-row users table should instead filter each CTE to the relevant date range before the join is evaluated. This reduces both sides of the shuffle and almost always enables a broadcast join where BigQuery previously chose a shuffle.
2. Add cluster_by on join keys in your dbt model config
For any intermediate or mart model that is materialised as a table and subsequently used as a join input in downstream models, set cluster_by on the join key column. This is a one-line config change in dbt that can make a significant difference to every downstream query that touches that model.
{{ config(
materialized='table',
cluster_by=['user_id']
) }}
3. Reduce column surface before joining
Switching from SELECT * to specific columns reduces shuffle volume significantly. In a documented real-world case, this single change reduced shuffle volume from 12TB to 800GB. In dbt, staging models that propagate all columns from a raw source into a join are carrying dead weight through the shuffle. Select only the columns the downstream model actually needs.
4. Materialise frequently joined intermediate models
If a large intermediate model is referenced as a join input in multiple downstream models, materialising it as a table (with clustering) rather than a view means BigQuery computes it once and reads it from storage on each subsequent join. A view forces BigQuery to re-evaluate the full underlying logic for every query that references it, multiplying the shuffle cost across every model run. For a deep dive on staging layer materialisation decisions, see our post on dbt staging layer materialisation in BigQuery.
5. Handle NULL and skewed join keys explicitly
If your join key contains NULLs, filter them out before the join. NULLs do not match in a join predicate anyway — they simply inflate the shuffle by forcing BigQuery to move NULL-keyed rows to the same worker before discarding them. For skewed keys, consider salting (adding a synthetic randomisation prefix to the key) or pre-aggregating the skewed records before the join, then unioning the result.
What Does the Improvement Actually Look Like?
The gains from addressing shuffle join inefficiencies are not marginal. A pattern we encounter regularly: a Series A fintech running a nightly dbt job across 15–20 mart models, each of which joins a large transactions table to various dimension tables. Before optimisation, the job consumes significant slot-hours and takes 40–60 minutes to complete. After applying pre-filtering, clustering on join keys, and column selection discipline across the worst five models, the same job runs in under 10 minutes — with a proportional reduction in bytes billed.
In one engagement with a global payments company, a reconciliation model that was performing a large shuffle join across two unfiltered staging tables was rebuilt with proper pre-filtering and clustering configuration. The model's slot consumption dropped by over 80%, and the bytes billed on each run fell from several terabytes to under 200GB. The impact was visible in the billing dashboard within the first week.
The biggest cost trap in BigQuery is full-table scans on large datasets. Partitioning and clustering reduce query costs by 60–90% by limiting data scanned. Shuffle joins amplify this — because a full-table scan feeding a shuffle join means you are paying for the scan and for the data movement.
Multi-table analytical queries often involve joining fact tables with multiple dimension tables, and BigQuery's join performance depends heavily on table ordering and join strategies. When you understand BigQuery's distributed join algorithms, you can structure your analytical queries to minimise data shuffling and reduce execution time from minutes to seconds.
Frequently Asked Questions
Q: How do I know if my dbt model is using a shuffle join in BigQuery?
A: Open the compiled SQL for your dbt model in the BigQuery console and inspect the execution plan. Look for stages labelled as Repartition — these indicate a shuffle operation. High slot time consumed on a JOIN stage relative to output rows is the clearest signal that BigQuery chose a shuffle join instead of a broadcast join.
Q: When does BigQuery automatically use a broadcast join instead of a shuffle join?
A: BigQuery automatically selects a broadcast join when one side of the join is small enough to fit in memory — typically under a few hundred megabytes. You can influence this by pre-filtering large tables before the join, reducing the row count on one side so BigQuery recognises it as a broadcast candidate. You can also use the BROADCAST_JOIN hint, though this carries risk if the "small" table grows over time.
Q: Does adding cluster_by in dbt actually reduce shuffle join cost in BigQuery?
A: Yes. When both tables in a join are clustered on the join key, BigQuery can prune irrelevant data blocks before the shuffle begins, reducing the volume of data that needs to be redistributed. This is particularly effective for large-to-large joins where a broadcast join is not possible — clustering reduces the shuffle volume even when the shuffle itself cannot be avoided.
Q: What is data skew in a BigQuery shuffle join and how do I fix it in dbt?
A: Data skew occurs when a single join key value accounts for a disproportionately large share of rows — for example, a dominant merchant ID or a NULL value. All matching rows for that key are routed to a single worker, which becomes a bottleneck. The fix is to either filter NULLs before the join, pre-aggregate skewed keys, or split the query into separate paths for high-volume and standard-volume keys.
Q: How does table ordering affect shuffle join cost in BigQuery dbt models?
A: BigQuery's optimiser uses table position to decide which side to broadcast. Placing your largest table first (left side) helps the optimiser identify the smaller table for broadcasting. While BigQuery is generally smart about this, explicitly ordering tables in your dbt model SQL ensures the optimiser has the clearest signal — particularly in complex multi-table joins where the size differential between tables is not obvious from metadata alone.
Unoptimised shuffle joins in BigQuery dbt models are one of the most consistently expensive problems we diagnose during data stack reviews — they are invisible in your test results, silent in your monitoring, and cumulative in your billing. At Fintel Analytics, we have helped fintech, payments, and e-commerce companies identify and fix exactly these patterns as part of structured data engineering engagements — from execution plan analysis through to model refactoring and cost governance. If your dbt job runtimes are climbing and your BigQuery bill is growing faster than your data volume, the shuffle join is the first place to look.