You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by we...@apache.org on 2026/07/23 13:05:42 UTC
(spark) branch master updated: [SPARK-58290][SQL] Fix O(n^2) partition index access in ShufflePartitionsUtil.coalescePartitionsWithSkew
This is an automated email from the ASF dual-hosted git repository.
cloud-fan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 3082cdfda92f [SPARK-58290][SQL] Fix O(n^2) partition index access in ShufflePartitionsUtil.coalescePartitionsWithSkew
3082cdfda92f is described below
commit 3082cdfda92f4507542ccbf99cf777a51f29414c
Author: Xiduo You <ul...@gmail.com>
AuthorDate: Thu Jul 23 21:05:21 2026 +0800
[SPARK-58290][SQL] Fix O(n^2) partition index access in ShufflePartitionsUtil.coalescePartitionsWithSkew
### What changes were proposed in this pull request?
In `ShufflePartitionsUtil.coalescePartitionsWithSkew`, the local `partitionIndices` is materialized from `inputPartitionSpecs`, whose inner collection is a `List` in practice. The coalescing loop then repeatedly accesses this sequence by index (`partitionIndices(i - 1)`, `partitionIndices(i)`, and the inner skew-section scan `partitionIndices(repeatIndex)`). Because `List.apply(i)` is O(i), the loop degrades to O(n^2) in the number of shuffle partitions.
This PR calls `.toArray` on `partitionIndices` so all indexed accesses become O(1), restoring the loop to O(n). The change is local; all downstream usages (`.head`, `.last`, indexed access, `.length`) are valid on `Array`, and the sequence is only read.
### Why are the changes needed?
A production job hung, and the driver thread dump showed it stuck in `ShufflePartitionsUtil.coalescePartitionsWithSkew`, spinning inside `List.drop` / `List.apply` (indexed list access):
```
scala.collection.immutable.List.drop(List.scala:79)
scala.collection.LinearSeqOps.apply(LinearSeq.scala:130)
scala.collection.immutable.List.apply(List.scala:79)
org.apache.spark.sql.execution.adaptive.ShufflePartitionsUtil$.coalescePartitionsWithSkew(ShufflePartitionsUtil.scala:166)
org.apache.spark.sql.execution.adaptive.ShufflePartitionsUtil$.coalescePartitions(ShufflePartitionsUtil.scala:75)
```
With a large number of shuffle partitions (skew join scenarios often produce many), the O(n^2) behavior can hang the driver during AQE planning.
Local micro-benchmark driving `coalescePartitions` on the skew path, `List` vs `Array` (same machine, same inputs):
| partitions | List (ms) | Array (ms) |
|-----------:|----------:|-----------:|
| 5000 | 72 | <1 |
| 10000 | 291 | <1 |
| 20000 | 1166 | 1 |
The `List` version's per-element cost grows linearly with partition count (confirming O(n^2)), while the `Array` version stays flat (O(n)).
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Existing tests, all passing locally (39 tests):
```
build/sbt "sql/testOnly *ShufflePartitionsUtilSuite *CoalesceShufflePartitionsSuite"
```
This is a pure performance fix with no behavior change.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 4.8)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Closes #57459 from ulysses-you/SPARK-58290.
Authored-by: Xiduo You <ul...@gmail.com>
Signed-off-by: Wenchen Fan <we...@databricks.com>
---
.../spark/sql/execution/adaptive/ShufflePartitionsUtil.scala | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/adaptive/ShufflePartitionsUtil.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/adaptive/ShufflePartitionsUtil.scala
index e0bad017b8f8..b8fdaa6154e7 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/execution/adaptive/ShufflePartitionsUtil.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/adaptive/ShufflePartitionsUtil.scala
@@ -159,8 +159,11 @@ object ShufflePartitionsUtil extends Logging {
}
// The indices may look like [0, 1, 2, 2, 2, 3, 4, 4, 5], and the repeated `2` and `4` mean
- // skewed partitions.
- val partitionIndices = partitionIndicesSeq.head
+ // skewed partitions. Materialize into an `Array` for O(1) indexed access: the loop below
+ // indexes into this sequence repeatedly, which degrades to O(n^2) when it is backed by a
+ // `List` (the default here) since `List.apply(i)` is O(i). With a large number of shuffle
+ // partitions this quadratic behavior can hang the driver.
+ val partitionIndices = partitionIndicesSeq.head.toArray
// The fist index must be 0.
assert(partitionIndices.head == 0)
val newPartitionSpecsSeq = Seq.fill(mapOutputStatistics.length)(
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org