A Time AQE Made It Worse

Concepts covered: Adaptive Query Execution, Configuration Tuning

Interviewers ask for a backfire story because a story is hard to fake: the details either connect or they do not. A credible one has 4 parts in order: what changed, what the Spark UI showed, the mechanism that explains it, and the fix together with what the fix cost. Here is one built on a mechanism you can reproduce, with numbers that hold together.

The job

A nightly job joins a day of order events to customers, groups by order_id, and runs a Python UDF that parses each order's JSON payload into 40 fields. The team had tuned spark.sql.shuffle.partitions to 400 for this job on a cluster with 200 executor cores, so the UDF stage ran 400 tasks in 2 waves of about 2.5 minutes each, roughly 5 minutes for the stage. Then the platform team moved the cluster to a Spark version with adaptive execution on by default, and set spark.sql.adaptive.coalescePartitions.parallelismFirst to false to cut the count of tiny tasks on the shared cluster. The next night the job missed its deadline.

What the Spark UI showed

On the SQL tab, the exchange before the UDF had an AQEShuffleRead node above it reporting the coalesced partition count and roughly 1.2GB of shuffle data. The page showed the plan as AdaptiveSparkPlan isFinalPlan=true, and the initial plan beside it still read hashpartitioning(order_id, 400). The event timeline showed most executors empty for the length of the stage, and the task summary showed durations clustered tightly: no long tail, every task uniformly slow.

The mechanism

With parallelismFirst off, coalescing packs adjacent shuffle partitions together until each reaches spark.sql.adaptive.advisoryPartitionSizeInBytes, 64MB by default. The job wrote roughly 1.2GB of shuffle output, and dividing that by 64MB gives roughly 19 partitions. The tuned 400 had become only the starting count. The flaw is in what the 64MB measures: compressed shuffle bytes, the input to the stage. The expensive work in this stage was the UDF, whose cost follows row count and payload length, and repetitive JSON compresses very well. Each remaining task now carried about 21 times the rows of an old one, so a 2.5 minute task became a 53 minute task, while 181 of the 200 cores waited.

Fixes, and what each one costs

FixEffect on the UDF stageCost
Set `spark.sql.adaptive.coalescePartitions.enabled` to false for this jobBack to 400 tasks, about 5 minutesEvery other shuffle in the job loses coalescing, and its cheap stages return to 400 tiny tasks
Lower the advisory size to 8MB for this jobAbout 154 tasks in a single wave, roughly 6.5 minutesEvery shuffle in the job is sized finer, including the cheap ones
`repartition(400)` just before the UDF400 tasks, since AQE leaves an explicit partition count aloneAn extra shuffle of the aggregated rows and a further stage
parsed = (
    orders.join(customers, "customer_id")
    .groupBy("order_id")
    .agg(F.first("payload").alias("payload"))
    .repartition(400)
    .withColumn("fields", parse_payload(F.col("payload")))
)

The team chose the repartition(400). It confined the fix to the single expensive step and kept coalescing for the cheap shuffles elsewhere in the job. The extra stage wrote less than the 1.2GB the aggregation had shuffled, a small price next to a 48 minute regression. They rejected turning coalescing off, because that fixes this stage and silently regresses every small shuffle in the job.

Other backfires with the same shape

BackfireMechanismWhat you would see
Runtime broadcast exhausts memoryThe conversion reads serialized shuffle size; the hash table built from those rows in memory can be several times largerDriver or executor memory errors around a broadcast exchange that the initial plan did not have
Skew split multiplies readsA skewed partition cut into N pieces re-reads the other side's matching partition N timesThe join stage's task count jumps and its shuffle read exceeds the shuffle written into it

Telling it

About this lesson

This section is part of the The Architecture Recital lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.