Spark AQE settings: what each one does and when to change it
Adaptive query execution is on by default since Spark 3.2 and re-plans a query after each shuffle: it merges small partitions, switches a sort-merge join to a broadcast and splits skewed partitions. Every setting behind those 3, its default in Apache Spark and on Databricks, the few worth changing, and how to see in the plan that it worked.
Tecyfy · Published · 8 min read

Adaptive query execution, AQE, has been on by default since Spark 3.2.0, and most jobs should leave it alone. It runs a query 1 shuffle at a time, measures what each shuffle wrote, and re-plans the rest: it merges small partitions, switches a sort-merge join to a broadcast when one side turns out small, and splits a skewed partition of a join. 2 settings are worth changing often: parallelismFirst on a shared cluster, and the skew threshold when skew goes undetected.
This article lists every setting behind those 3 features, with its default in Apache Spark and on Databricks, and shows on real plans from Spark 4.2 what each one changes.
See what it does first
Each feature leaves a mark in the final plan. Here are all 3, on queries we ran on Spark 4.2.0 with 4 cores:
Tasks
Largest read
Slowest task
SortMergeJoin [customer_id], [customer_id], Inner
:- Sort
: +- Exchange hashpartitioning(customer_id, 16)Every order of customer 0 lands in 1 partition: 110.4 MiB against a median of 2 MiB. That task runs 1,238 ms while the median task takes 44 ms, and the stage waits for it.
orders.join(customers, 'customer_id'): 8 in 10 orders belong to customer 0, 16 partitions, no broadcast. The skew thresholds were set to 16 MB because the data is small; the defaults are 256 MB and 64 MB. Spark 4.2.0, 4 cores.
To see the marks on your own query, run it and open it in the Spark UI's SQL / DataFrame tab, or call explain() on the same DataFrame after it ran. Before the run, explain() prints isFinalPlan=false and none of these marks; SQL EXPLAIN, which never runs the query, shows only the initial plan. How to read a Spark physical plan walks through one line by line.
The master switch
| Setting | Spark default | What it does |
|---|---|---|
spark.sql.adaptive.enabled | true | AQE on or off. On Databricks the switch is spark.databricks.optimizer.adaptive.enabled, also on |
AQE applies to batch queries with at least 1 shuffle, broadcast or subquery. It does not apply to Structured Streaming, and it never reorders joins: that is the cost-based optimizer's job, from table statistics.
Merging small partitions
A shuffle writes spark.sql.shuffle.partitions partitions, 200 unless set. For a small shuffle that is 200 tasks with almost nothing to do. In the widget's first query, a groupBy over 10 million orders produced 201 partial sums, and AQE merged the 200 planned tasks into 1.
| Setting | Spark default | What it does |
|---|---|---|
spark.sql.adaptive.coalescePartitions.enabled | true | Merge neighbouring small partitions after a shuffle |
spark.sql.adaptive.coalescePartitions.parallelismFirst | true | Ignore the target size, keep only the 1 MB minimum, to run as many tasks as possible |
spark.sql.adaptive.advisoryPartitionSizeInBytes | 64 MB | The target size of a merged partition (and of a split skewed one) |
spark.sql.adaptive.coalescePartitions.minPartitionSize | 1 MB | The smallest a merged partition may be |
spark.sql.adaptive.coalescePartitions.initialPartitionNum | spark.sql.shuffle.partitions | The partitions to write before merging |
When to change it. parallelismFirst at true favours speed for 1 job on an empty cluster: many small tasks, every core busy. On a cluster that many jobs share, those small tasks compete for cores and each pays its start-up cost; Spark's own docs recommend false there, so merged partitions grow toward 64 MB.
spark.conf.set("spark.sql.adaptive.coalescePartitions.parallelismFirst", "false")
On Databricks merged partitions already aim at 64 MB, close to but no larger, and never fewer than 2 per core (minPartitionNum, which Databricks says not to set). Databricks also accepts spark.sql.shuffle.partitions set to auto, which picks the number from the plan and the input size.
Merging cannot help a partition that is too big. If you set 200 partitions for a 2 TB shuffle, each is 10 GB and AQE leaves them alone; set the number high and let AQE merge down. Spark shuffle partitions gives the rule for the number.
Switching the join
Before a query runs, Spark sizes each side of a join from estimates. A filter that leaves 103 rows of 10 million looks, to the planner, like a big table, so it plans a sort-merge joinSort-Merge JoinSpark's default join for 2 large tables: it shuffles both by the join key, sorts each partition, then walks the 2 sorted sides together to find matching rows.. After the shuffles ran, AQE sees the real size and switches to a broadcast hash join, skipping both sorts and reading the shuffle files locally.
| Setting | Spark default | What it does |
|---|---|---|
spark.sql.adaptive.autoBroadcastJoinThreshold | same as spark.sql.autoBroadcastJoinThreshold, 10 MB | Switch to broadcast when one side's runtime size is under this. On Databricks, spark.databricks.adaptive.autoBroadcastJoinThreshold, 30 MB |
spark.sql.adaptive.localShuffleReader.enabled | true | After a switch, read shuffle files locally instead of over the network |
spark.sql.adaptive.maxShuffledHashJoinLocalMapThreshold | 0 | Above 0 (and not below the advisory size), prefer a shuffled hash join when every partition is under it |
When to change it. Rarely. The switch comes after both sides were shuffled, so if you know a side is small, a hint is better: orders.join(F.broadcast(customers), "customer_id") plans the broadcast before anything runs, and Databricks gives the same advice. If a small table is still not broadcast, check the join type first: a left outer join cannot broadcast its left side.
Splitting skewed partitions
When 1 key holds most of the rows, 1 partition of a join holds most of the data, and 1 task runs while the rest wait. AQE splits that partition into pieces near the advisory size and joins each with a copy of the matching rows from the other side. In the widget's skew example the slowest task fell from 1,238 ms to 414 ms.
| Setting | Spark default | What it does |
|---|---|---|
spark.sql.adaptive.skewJoin.enabled | true | Split skewed partitions of a sort-merge join |
spark.sql.adaptive.skewJoin.skewedPartitionFactor | 5.0 | Skewed if over this many times the median partition |
spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes | 256 MB | ...and over this size, both at once |
spark.sql.adaptive.forceOptimizeSkewedJoin | false | Split skew even when it costs an extra shuffle |
spark.sql.adaptive.optimizeSkewsInRebalancePartitions.enabled | true | Split skewed partitions of a REBALANCE hint too |
When to change it. When the Spark UI shows 1 task far slower than the median and the plan has no skew=true, the partition missed 1 of the 2 tests. The usual miss is the 256 MB floor: with many partitions, a heavy one can be 20 times the median and still smaller. Lower the threshold, keeping it above the advisory size as Spark's docs advise:
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "128MB")
Apache Spark splits skew only inside sort-merge joins; Databricks also handles shuffled hash joins. Neither splits a groupBy, where every row of a key must meet in 1 task: salting the key is the fix there. Data skewShuffle and Data SkewA shuffle moves rows between machines so rows with the same key meet, for a join or group by; skew is when one key has far more rows, so one task does most of the work. and how to handle skewed data in PySpark cover both cases.
What to leave alone
spark.sql.adaptive.enabled. Turning AQE off brings back 200 tiny tasks, missed broadcasts and unsplit skew.minPartitionNumon Databricks. Databricks says not to set it, because it overrides the minimum partition size.- The skew factor. 5 times the median is a sound test; the size floor is the one that misses.
- Hints AQE already covers. Databricks recommends AQE's skew handling over the old skew hint.
Check it worked
After a change, run the query and compare the final plan with the initial one: AQEShuffleRead coalesced for merged partitions, AQEShuffleRead local for a switched join, AQEShuffleRead skewed with SortMergeJoin(skew=true) for a split. Then open the stage and compare the slowest task with the median. Our Spark UI guide shows both on a skewed join, before and after, and adaptive query executionAdaptive Query ExecutionSpark's way of re-planning a query while it runs: after each shuffle it looks at the real sizes, then merges small partitions, splits skewed ones and can switch a join to a broadcast. explains the 3 features in short.
Questions
- How do I enable adaptive query execution in Spark?
- It is already on: spark.sql.adaptive.enabled is true by default since Spark 3.2.0, and Databricks turns it on by default too. To check, look for AdaptiveSparkPlan at the top of explain() output. You can turn it off with spark.conf.set('spark.sql.adaptive.enabled', 'false'), which is rarely a good idea.
- What does spark.sql.adaptive.coalescePartitions.parallelismFirst do?
- When true, the default in Apache Spark, AQE ignores the 64 MB target size when it merges small shuffle partitions and only keeps each merged partition over 1 MB, so as many tasks run as possible. Spark's docs recommend setting it to false on a busy cluster, so partitions grow toward 64 MB and fewer small tasks compete for cores.
- Why didn't AQE detect my data skew?
- A partition counts as skewed only when it is larger than 5 times the median partition and larger than 256 MB, both at once. On small data or with many partitions, a heavy partition can be 20 times the median and still under 256 MB. Lower spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes, keeping it above spark.sql.adaptive.advisoryPartitionSizeInBytes. Also, Apache Spark splits skew only in sort-merge joins, not in a group by.
- Why didn't AQE switch my join to a broadcast?
- Either the smaller side was over the adaptive broadcast threshold (10 MB in Apache Spark, 30 MB on Databricks), or the join type cannot broadcast that side: a left outer join cannot broadcast its left side. Databricks also skips the switch when most partitions are empty. A broadcast hint planned before the run is still faster than AQE's switch, which comes after both sides have shuffled.
- Does AQE work with Structured Streaming?
- No. Databricks lists it as applying only to batch queries that have at least one exchange (a join, aggregate or window) or a subquery.
- How can I tell what AQE changed in a query?
- Compare the Final Plan with the Initial Plan, in the Spark UI's SQL / DataFrame tab or with explain() on the DataFrame after it ran. AQEShuffleRead coalesced means partitions were merged, AQEShuffleRead local a join switched to broadcast, AQEShuffleRead skewed and SortMergeJoin(skew=true) a skewed partition split. Older Databricks docs call the reader CustomShuffleReader and the skew flag isSkew.
Sources