Skip to the page
Tecyfy

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

A hand pulls a switch marked AQE as a train runs on, sending it onto a new track: Spark re-planning a query while it runs

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:

Try itWhat adaptive query execution changes in a plan
Feature

Tasks16

Largest read110.4 MiB

Slowest task1,238 ms

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

SettingSpark defaultWhat it does
spark.sql.adaptive.enabledtrueAQE 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.

SettingSpark defaultWhat it does
spark.sql.adaptive.coalescePartitions.enabledtrueMerge neighbouring small partitions after a shuffle
spark.sql.adaptive.coalescePartitions.parallelismFirsttrueIgnore the target size, keep only the 1 MB minimum, to run as many tasks as possible
spark.sql.adaptive.advisoryPartitionSizeInBytes64 MBThe target size of a merged partition (and of a split skewed one)
spark.sql.adaptive.coalescePartitions.minPartitionSize1 MBThe smallest a merged partition may be
spark.sql.adaptive.coalescePartitions.initialPartitionNumspark.sql.shuffle.partitionsThe 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.

SettingSpark defaultWhat it does
spark.sql.adaptive.autoBroadcastJoinThresholdsame as spark.sql.autoBroadcastJoinThreshold, 10 MBSwitch to broadcast when one side's runtime size is under this. On Databricks, spark.databricks.adaptive.autoBroadcastJoinThreshold, 30 MB
spark.sql.adaptive.localShuffleReader.enabledtrueAfter a switch, read shuffle files locally instead of over the network
spark.sql.adaptive.maxShuffledHashJoinLocalMapThreshold0Above 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.

SettingSpark defaultWhat it does
spark.sql.adaptive.skewJoin.enabledtrueSplit skewed partitions of a sort-merge join
spark.sql.adaptive.skewJoin.skewedPartitionFactor5.0Skewed if over this many times the median partition
spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes256 MB...and over this size, both at once
spark.sql.adaptive.forceOptimizeSkewedJoinfalseSplit skew even when it costs an extra shuffle
spark.sql.adaptive.optimizeSkewsInRebalancePartitions.enabledtrueSplit 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.
  • minPartitionNum on 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.