Uh oh!
There was an error while loading. Please reload this page.
[SPARK-58974][SQL] Apply the narrowed-partitioning skew guard regardless of requireAllClusterKeysForDistribution - #58338
Conversation
…ess of requireAllClusterKeysForDistribution ### What changes were proposed in this pull request? `KeyedPartitioning.groupedSatisfies` refuses a narrowed, non-grouped partitioning unless `spark.sql.sources.v2.bucketing.allowKeysSubsetOfPartitionKeys.enabled` is set, because grouping it would merge partitions that held distinct keys in the original finer-grained partitioning. That guard lived inside the `requireAllClusterKeys = false` arm, so it never ran when `spark.sql.requireAllClusterKeysForDistribution` was enabled. This moves the guard above the `requireAllClusterKeys` check. The skew risk does not depend on which key sets count as matching, so the decision should not either. The config doc for `allowKeysSubsetOfPartitionKeys.enabled` is updated as well: it did not mention this second thing the config gates, which is not tied to `requireAllClusterKeysForDistribution` being false. ### Why are the changes needed? With `spark.sql.requireAllClusterKeysForDistribution = true` a narrowed partitioning was accepted, `EnsureRequirements` inserted a `GroupPartitionsExec`, and the skew exposure was taken with the opt-in config still off. Note this is the *stricter* of the two settings, which makes it the more surprising direction. Reproduced with a table partitioned by `(id, dept)` projected down to `id` -- keys collapsing to `[1, 1, 2]` -- and joined on `id`: with `requireAllClusterKeys = true` the plan gets a `GroupPartitionsExec` and no shuffle while `allowKeysSubsetOfPartitionKeys` is off, whereas with `requireAllClusterKeys = false` the same query correctly shuffles both sides. Please note that the guard's condition is imprecise, and the hoist makes that imprecision reachable for one more config value. `isNarrowed && !isGrouped` is a proxy for "the narrowing collapsed distinct keys", but `!isGrouped` has causes that have nothing to do with narrowing: a source that reports several splits per partition key, or a union whose children have distinct keys individually and repeat keys across children. Grouping such a partitioning merges only same-key partitions, which is what `GroupPartitionsExec` does for any non-narrowed partitioning and needs no opt-in, yet the condition refuses it. Until now that false refusal could only happen with `requireAllClusterKeysForDistribution = false`; after this change it can happen with either value. Tightening the condition to actual key collapse is a separate change, which I am working on as a follow-up; this PR keeps the condition as it is and only fixes where it is evaluated. ### Does this PR introduce _any_ user-facing change? Yes, a plan-level change. With `requireAllClusterKeysForDistribution` enabled, Spark previously coalesced partitions derived from a narrowed partitioning without `allowKeysSubsetOfPartitionKeys.enabled`, risking skewed partitions; it now inserts a shuffle unless that config is enabled. Query results are unchanged. No migration guide entry: the guard, and with it the bypass, arrived in 4.3.0, which is unreleased, so no released version behaves the old way. This goes to `branch-4.3` as well. ### How was this patch tested? * New unit test in `ProjectedOrderingAndPartitioningSuite` calling `groupedSatisfies` directly on a narrowed, ungrouped partitioning, for both values of `requireAllClusterKeys` and both values of the opt-in. It fails on master with `kp.groupedSatisfies(required) was true` for `requireAllClusterKeys = true`. * New end-to-end test in `KeyGroupedPartitioningSuite` asserting the guard behaves identically for both values of `requireAllClusterKeys`: no `GroupPartitionsExec`, and a shuffle instead. It fails on master for `requireAllClusterKeys = true`. It also varies `v2BucketingShuffleEnabled`, because that decides whether the refused side is laid out on the other side's declared partition keys (one shuffle) or both sides are shuffled (two). * That test also covers `allowKeysSubsetOfPartitionKeys = true` for both values of `requireAllClusterKeys`, since this is the first time the opt-in affects `groupedSatisfies` when `requireAllClusterKeysForDistribution` is enabled -- it must restore the coalescing and avoid the shuffles. Neither suite had any coverage of `requireAllClusterKeysForDistribution`, which is how the bug survived. * Also ran `DistributionSuite`, `KeyGroupedPartitioningSuite`, `EnsureRequirementsSuite`, `PlannerSuite`, `ProjectedOrderingAndPartitioningSuite`, `DataFrameSetOperationsSuite`, `AdaptiveQueryExecSuite`, `CoalesceShufflePartitionsSuite`, and the TPC-DS plan stability suites -- no golden file changed. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 5)
peter-toth
commented
Aug 27, 2026
peter-toth
commented
Aug 27, 2026
cc @ulysses-you |
ulysses-you
left a comment
There was a problem hiding this comment.
0 blocking, 1 non-blocking.
Verified
Confirmed by tracing every call site and by running the tests locally:
groupedSatisfieshas exactly two callers:EnsureRequirements.scala:78(nonGrouped.find(_.groupedSatisfies(distribution)), i.e. always invoked on a non-grouped KP) andKeyedPartitioning.satisfies0(partitioning.scala:574, guarded byisGrouped &&). So the delta surface of this move is exactly one route:requireAllClusterKeys=true+ narrowed + ungrouped.- For
spark.sql.requireAllClusterKeysForDistribution=falsethis is provably a no-op: the old check order wasallowSubset? -> narrowed guard -> attributes.forall, the new order isguard(&& !allowSubset) -> allowSubset? -> attributes.forall. Conjoining the guard with!allowSubsetmakes the reorder equivalent. - Via
satisfies0the hoisted!isGroupedclause can never fire (guarded upstream byisGrouped &&), so theOrderedDistributionarm and all other behavior are untouched. - Ran the unit test against unpatched master: fails with
kp.groupedSatisfies(required) was trueforrequireAllClusterKeys=true, matching the description verbatim; with the patch applied, both new tests pass. Also ranKeyGroupedPartitioningSuite,EnsureRequirementsSuiteandProjectedOrderingAndPartitioningSuitewith the patch - no failure attributable to it. - Neither direction introduces a wrong-results axis: accepting groups partitions by equal projected key values (rows still routed correctly); refusing falls back to a standard hash shuffle. So plan shape / skew only, as the description states.
Finding 1 (non-blocking): the imprecise condition widens a safe-case regression to requireAllClusterKeysForDistribution=true users
isNarrowed && !isGrouped is a proxy for "the narrowing collapsed distinct keys", but !isGrouped also holds when a source natively reports multiple splits per partition key (e.g. uncommitted Iceberg files). Concretely: table partitioned by (a, b, c) whose scan emits 2 splits per key, then a projection keeping exactly the operator's clustering (a, b) without collapsing any projected key. Grouping here would merge only same-key partitions - precisely what GroupPartitionsExec does for any un-narrowed ungrouped KP - yet the guard now refuses it under requireAllClusterKeysForDistribution=true and inserts an avoidable shuffle (results identical, plan worse).
Peer code showing the intended tolerance for duplicate keys: createKeyedShuffleSpec.allClusterKeysCovered in EnsureRequirements.scala (~line 794-809), whose comment says "Key order and duplicated cluster keys don't matter" for the same skew concern. After this change, groupedSatisfies tolerates duplicated keys strictly less than that adjacent co-partitioning gate does.
Agreed on deferring the tightened condition to a follow-up rather than expanding this diff - but the follow-up should reconcile with allClusterKeysCovered too, so both SPJ skew gates end up sharing one notion of "collapsed keys". Non-blocking given results are unaffected; just flagging that the follow-up shouldn't be dropped.
| if (isNarrowed && !isGrouped && | ||
| !SQLConf.get.v2BucketingAllowKeysSubsetOfPartitionKeys) { |
There was a problem hiding this comment.
[Non-blocking] isNarrowed && !isGrouped is a proxy for "the narrowing collapsed distinct keys", but !isGrouped also holds when a source natively reports multiple splits per partition key (e.g. uncommitted Iceberg files under an (a, b, c) partitioning). In that case a projection keeping exactly the operator's clustering (a, b) need not collapse any projected key, and grouping would merge only same-key partitions - precisely what GroupPartitionsExec does for any un-narrowed ungrouped KP - yet this guard now refuses it even for requireAllClusterKeysForDistribution=true, inserting an avoidable shuffle (results unchanged).
Peer code showing the intended tolerance: createKeyedShuffleSpec.allClusterKeysCovered in EnsureRequirements.scala (~lines 794-809) says "Key order and duplicated cluster keys don't matter" for the same skew concern; after this change groupedSatisfies tolerates duplicated keys strictly less than that adjacent co-partitioning gate.
OK to defer tightening to the follow-up, but please make sure it reconciles with allClusterKeysCovered so both SPJ skew gates share one notion of "collapsed keys".
There was a problem hiding this comment.
Agreed, and thanks for the allClusterKeysCovered pointer - its comment ("Key order and duplicated cluster keys don't matter") is exactly the tolerance I want this guard to have. The follow-up is already written: the flag will mean actual key collapse - a projection mapping keys that were distinct in the input onto the same projected key - instead of "positions were dropped", so a source that reports several splits per partition key no longer trips it. Measured on your shape, with the opt-in off: before, no GroupPartitionsExec and 2 shuffles; after, 1 GroupPartitionsExec and no shuffle. I will check allClusterKeysCovered against the tightened notion explicitly and report what I find on that PR, and link it here.
| required match { | ||
| case c @ ClusteredDistribution(requiredClustering, requireAllClusterKeys, _, _) => | ||
| if (requireAllClusterKeys) { | ||
| if (isNarrowed && !isGrouped && |
There was a problem hiding this comment.
[Non-blocking] The !isGrouped conjunct here is load-bearing in a way the class-level doc contradicts. The doc above ("groupedSatisfies(): called on non-grouped KPs ...") predates this change, but satisfies0 also calls groupedSatisfies on grouped KPs (isGrouped && groupedSatisfies(required)), and grouped-but-narrowed KPs are constructible: PartitioningPreservingUnaryExecNode recomputes isGrouped from the projected keys while isNarrowed stays sticky, which is exactly the state exercised by the existing test "SPARK-46367: narrowing projection with distinct projected keys does not require allowKeysSubsetOfPartitionKeys".
If a later cleanup trusts the class doc and drops the "redundant" !isGrouped, grouped narrowed KPs would stop satisfying ClusteredDistribution with the config off and pick up unnecessary shuffles (that test would fail). Could you touch up the class-level doc (or add a short note here) so the conjunct's purpose is recorded?
There was a problem hiding this comment.
You are right, and this was the most useful comment on the PR - I had convinced myself the conjunct was redundant. It is not: nonGroupedSatisfies is the default Partitioning implementation, so a grouped KP reaches a ClusteredDistribution only through groupedSatisfies, and a narrowing projection that keeps its keys distinct is precisely a grouped narrowed KP. Dropping the term would cost it a shuffle, and the test you name pins that. Fixed the class-level doc to describe both callers and to record the reason, and added a note at the guard itself.
| if (isNarrowed && !isGrouped && | ||
| !SQLConf.get.v2BucketingAllowKeysSubsetOfPartitionKeys) { | ||
| // A narrowed, non-grouped partitioning carries the same skew risk as using a subset of | ||
| // partition keys for a join: GroupPartitionsExec will merge partitions that held |
There was a problem hiding this comment.
[Nit] "GroupPartitionsExec will merge partitions that held distinct keys in the original finer-grained partitioning" states the risky case as fact, but when the source was already ungrouped (multiple splits per identical key) and the narrowing collapsed nothing, the merged partitions held the same original key. Since isNarrowed && !isGrouped is a proxy (as the PR description itself notes), "may merge" would be accurate here and in the isNarrowed scaladoc. As written, the docs make the guard look exact, which could make the promised follow-up tightening look unnecessary to a future reader.
There was a problem hiding this comment.
Done, in the guard comment and in the isNarrowed scaladoc, together with the reason for "may": the duplicate keys can also come from a source reporting several splits per partition key.
| case c @ ClusteredDistribution(requiredClustering, requireAllClusterKeys, _, _) => | ||
| if (requireAllClusterKeys) { | ||
| if (isNarrowed && !isGrouped && | ||
| !SQLConf.get.v2BucketingAllowKeysSubsetOfPartitionKeys) { |
There was a problem hiding this comment.
[Nit] SQLConf.get.v2BucketingAllowKeysSubsetOfPartitionKeys is now read twice in this match arm (here and in the else branch below). Hoisting it into a local val at the top of the case c @ ClusteredDistribution(...) arm would make it explicit that both branches are gated by the same switch, which is the core of this fix, and would keep a future edit from decoupling the two reads.
There was a problem hiding this comment.
Done - one allowKeysSubsetOfPartitionKeys val per ClusteredDistribution arm, so both branches visibly read the same switch.
| val t2cols = Array(Column.create("id", LongType), Column.create("data", StringType)) | ||
| withTable("t1", "t2") { | ||
| createTable("t1", cols, Array(identity("id"), identity("dept"))) | ||
| sql("INSERT INTO testcat.ns.t1 VALUES (1, 'x', 'a1'), (1, 'y', 'a2'), (2, 'z', 'a3')") |
There was a problem hiding this comment.
[Nit] The directly analogous test "SPARK-46367: narrowing projection requires allowKeysSubsetOfPartitionKeys" builds this same shape from the suite's standing items/itemsColumns + purchases/purchasesColumns fixtures and the selectWithMergeJoinHint helper (it only generates the hint prefix from alias strings, so the subquery alias u works with it too -- the SPARK-46367 test itself passes "sub"). Reusing those here would keep the pre-fix and post-fix narrowing tests directly comparable instead of introducing a parallel t1/t2 schema.
There was a problem hiding this comment.
Done. The test now builds on items/purchases and selectWithMergeJoinHint, with the same data as the neighbouring SPARK-46367: narrowing projection requires allowKeysSubsetOfPartitionKeys test (id=1 mapping to two partitions), so the two are directly comparable.
| } | ||
| } | ||
| test("SPARK-58974: the narrowing guard applies for either value of requireAllClusterKeys") { |
There was a problem hiding this comment.
[Nit] The requireAll = false iteration exactly duplicates Scenario 1 of the existing test "SPARK-46367: narrowing projection with duplicate keys requires allowKeysSubsetOfPartitionKeys to satisfy ClusteredDistribution" (same keys, same ProjectExec fixture, same groupedSatisfies asserts; ClusteredDistribution's requireAllClusterKeys defaults to false). If the explicit both-values contrast pair is intentional, fine -- otherwise this test could cover only requireAll = true, or the loop could be folded into the existing test so the two don't have to be kept in lockstep.
There was a problem hiding this comment.
Kept both values: "the guard answers the same either way" is the claim this test makes, and the false iteration is its control. I added a comment saying so, so the overlap with the SPARK-46367 scenario does not read as accidental.
Documentation and test changes only, no behaviour change. - Correct the class-level doc on `KeyedPartitioning`: `groupedSatisfies()` has two callers, and `satisfies0()` calls it on *grouped* KPs, where it is the only route to satisfying a `ClusteredDistribution` since `nonGroupedSatisfies()` is false for one. Record that this is why the narrowing guard is a conjunction with `!isGrouped`: a narrowing projection whose keys stayed distinct is grouped, and refusing it would only cost a shuffle. - Say "may merge" rather than "will merge" in the guard comment and the `isNarrowed` scaladoc: the condition is a proxy, since duplicate keys can also come from a source that reports several splits per partition key. - Read `v2BucketingAllowKeysSubsetOfPartitionKeys` once per `ClusteredDistribution` arm instead of in both branches. - Build the end-to-end test on the suite's `items`/`purchases` fixtures and `selectWithMergeJoinHint`, so it is directly comparable to the neighbouring `SPARK-46367: narrowing projection requires allowKeysSubsetOfPartitionKeys` test instead of introducing a parallel schema. - Note in the unit test that covering both values of `requireAllClusterKeys` is deliberate: the `false` iteration is the control for the claim that the guard answers the same either way.
dongjoon-hyun
commented
Aug 27, 2026
Let's merge this, @peter-toth ! 😄 |
…ess of requireAllClusterKeysForDistribution ### What changes were proposed in this pull request? `KeyedPartitioning.groupedSatisfies` refuses a narrowed, non-grouped partitioning unless `spark.sql.sources.v2.bucketing.allowKeysSubsetOfPartitionKeys.enabled` is set, because grouping it would merge partitions that held distinct keys in the original finer-grained partitioning. That guard lived inside the `requireAllClusterKeys = false` arm, so it never ran when `spark.sql.requireAllClusterKeysForDistribution` was enabled. This moves the guard above the `requireAllClusterKeys` check. The skew risk does not depend on which key sets count as matching, so the decision should not either. The config doc for `allowKeysSubsetOfPartitionKeys.enabled` is updated as well: it did not mention this second thing the config gates, which is not tied to `requireAllClusterKeysForDistribution` being false. ### Why are the changes needed? With `spark.sql.requireAllClusterKeysForDistribution = true` a narrowed partitioning was accepted, `EnsureRequirements` inserted a `GroupPartitionsExec`, and the skew exposure was taken with the opt-in config still off. Note this is the *stricter* of the two settings, which makes it the more surprising direction. Reproduced with a table partitioned by `(id, dept)` projected down to `id` -- keys collapsing to `[1, 1, 2]` -- and joined on `id`: with `requireAllClusterKeys = true` the plan gets a `GroupPartitionsExec` and no shuffle while `allowKeysSubsetOfPartitionKeys` is off, whereas with `requireAllClusterKeys = false` the same query correctly shuffles both sides. Please note that the guard's condition is imprecise, and the hoist makes that imprecision reachable for one more config value. `isNarrowed && !isGrouped` is a proxy for "the narrowing collapsed distinct keys", but `!isGrouped` has causes that have nothing to do with narrowing: a source that reports several splits per partition key, or a union whose children have distinct keys individually and repeat keys across children. Grouping such a partitioning merges only same-key partitions, which is what `GroupPartitionsExec` does for any non-narrowed partitioning and needs no opt-in, yet the condition refuses it. Until now that false refusal could only happen with `requireAllClusterKeysForDistribution = false`; after this change it can happen with either value. Tightening the condition to actual key collapse is a separate change, which I am working on as a follow-up; this PR keeps the condition as it is and only fixes where it is evaluated. ### Does this PR introduce _any_ user-facing change? Yes, a plan-level change. With `requireAllClusterKeysForDistribution` enabled, Spark previously coalesced partitions derived from a narrowed partitioning without `allowKeysSubsetOfPartitionKeys.enabled`, risking skewed partitions; it now inserts a shuffle unless that config is enabled. Query results are unchanged. No migration guide entry: the guard, and with it the bypass, arrived in 4.3.0, which is unreleased, so no released version behaves the old way. This goes to `branch-4.3` as well. ### How was this patch tested? * New unit test in `ProjectedOrderingAndPartitioningSuite` calling `groupedSatisfies` directly on a narrowed, ungrouped partitioning, for both values of `requireAllClusterKeys` and both values of the opt-in. It fails on master with `kp.groupedSatisfies(required) was true` for `requireAllClusterKeys = true`. * New end-to-end test in `KeyGroupedPartitioningSuite` asserting the guard behaves identically for both values of `requireAllClusterKeys`: no `GroupPartitionsExec`, and a shuffle instead. It fails on master for `requireAllClusterKeys = true`. It also varies `v2BucketingShuffleEnabled`, because that decides whether the refused side is laid out on the other side's declared partition keys (one shuffle) or both sides are shuffled (two). * That test also covers `allowKeysSubsetOfPartitionKeys = true` for both values of `requireAllClusterKeys`, since this is the first time the opt-in affects `groupedSatisfies` when `requireAllClusterKeysForDistribution` is enabled -- it must restore the coalescing and avoid the shuffles. Neither suite had any coverage of `requireAllClusterKeysForDistribution`, which is how the bug survived. * Also ran `DistributionSuite`, `KeyGroupedPartitioningSuite`, `EnsureRequirementsSuite`, `PlannerSuite`, `ProjectedOrderingAndPartitioningSuite`, `DataFrameSetOperationsSuite`, `AdaptiveQueryExecSuite`, `CoalesceShufflePartitionsSuite`, and the TPC-DS plan stability suites -- no golden file changed. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 5) Closes#58338 from peter-toth/SPARK-58974-narrowing-guard-requireallclusterkeys. Authored-by: Peter Toth <peter.toth@gmail.com> Signed-off-by: Peter Toth <peter.toth@gmail.com> (cherry picked from commit 9d33e01) Signed-off-by: Peter Toth <peter.toth@gmail.com>
…ess of requireAllClusterKeysForDistribution `KeyedPartitioning.groupedSatisfies` refuses a narrowed, non-grouped partitioning unless `spark.sql.sources.v2.bucketing.allowKeysSubsetOfPartitionKeys.enabled` is set, because grouping it would merge partitions that held distinct keys in the original finer-grained partitioning. That guard lived inside the `requireAllClusterKeys = false` arm, so it never ran when `spark.sql.requireAllClusterKeysForDistribution` was enabled. This moves the guard above the `requireAllClusterKeys` check. The skew risk does not depend on which key sets count as matching, so the decision should not either. The config doc for `allowKeysSubsetOfPartitionKeys.enabled` is updated as well: it did not mention this second thing the config gates, which is not tied to `requireAllClusterKeysForDistribution` being false. With `spark.sql.requireAllClusterKeysForDistribution = true` a narrowed partitioning was accepted, `EnsureRequirements` inserted a `GroupPartitionsExec`, and the skew exposure was taken with the opt-in config still off. Note this is the *stricter* of the two settings, which makes it the more surprising direction. Reproduced with a table partitioned by `(id, dept)` projected down to `id` -- keys collapsing to `[1, 1, 2]` -- and joined on `id`: with `requireAllClusterKeys = true` the plan gets a `GroupPartitionsExec` and no shuffle while `allowKeysSubsetOfPartitionKeys` is off, whereas with `requireAllClusterKeys = false` the same query correctly shuffles both sides. Please note that the guard's condition is imprecise, and the hoist makes that imprecision reachable for one more config value. `isNarrowed && !isGrouped` is a proxy for "the narrowing collapsed distinct keys", but `!isGrouped` has causes that have nothing to do with narrowing: a source that reports several splits per partition key, or a union whose children have distinct keys individually and repeat keys across children. Grouping such a partitioning merges only same-key partitions, which is what `GroupPartitionsExec` does for any non-narrowed partitioning and needs no opt-in, yet the condition refuses it. Until now that false refusal could only happen with `requireAllClusterKeysForDistribution = false`; after this change it can happen with either value. Tightening the condition to actual key collapse is a separate change, which I am working on as a follow-up; this PR keeps the condition as it is and only fixes where it is evaluated. Yes, a plan-level change. With `requireAllClusterKeysForDistribution` enabled, Spark previously coalesced partitions derived from a narrowed partitioning without `allowKeysSubsetOfPartitionKeys.enabled`, risking skewed partitions; it now inserts a shuffle unless that config is enabled. Query results are unchanged. No migration guide entry: the guard, and with it the bypass, arrived in 4.3.0, which is unreleased, so no released version behaves the old way. This goes to `branch-4.3` as well. * New unit test in `ProjectedOrderingAndPartitioningSuite` calling `groupedSatisfies` directly on a narrowed, ungrouped partitioning, for both values of `requireAllClusterKeys` and both values of the opt-in. It fails on master with `kp.groupedSatisfies(required) was true` for `requireAllClusterKeys = true`. * New end-to-end test in `KeyGroupedPartitioningSuite` asserting the guard behaves identically for both values of `requireAllClusterKeys`: no `GroupPartitionsExec`, and a shuffle instead. It fails on master for `requireAllClusterKeys = true`. It also varies `v2BucketingShuffleEnabled`, because that decides whether the refused side is laid out on the other side's declared partition keys (one shuffle) or both sides are shuffled (two). * That test also covers `allowKeysSubsetOfPartitionKeys = true` for both values of `requireAllClusterKeys`, since this is the first time the opt-in affects `groupedSatisfies` when `requireAllClusterKeysForDistribution` is enabled -- it must restore the coalescing and avoid the shuffles. Neither suite had any coverage of `requireAllClusterKeysForDistribution`, which is how the bug survived. * Also ran `DistributionSuite`, `KeyGroupedPartitioningSuite`, `EnsureRequirementsSuite`, `PlannerSuite`, `ProjectedOrderingAndPartitioningSuite`, `DataFrameSetOperationsSuite`, `AdaptiveQueryExecSuite`, `CoalesceShufflePartitionsSuite`, and the TPC-DS plan stability suites -- no golden file changed. Generated-by: Claude Code (Opus 5) Closes#58338 from peter-toth/SPARK-58974-narrowing-guard-requireallclusterkeys. Authored-by: Peter Toth <peter.toth@gmail.com> Signed-off-by: Peter Toth <peter.toth@gmail.com> (cherry picked from commit 9d33e01) Signed-off-by: Peter Toth <peter.toth@gmail.com>
peter-toth
commented
Aug 28, 2026
peter-toth
commented
Aug 28, 2026
Thank you @dongjoon-hyun and @ulysses-you for the reivew. |
peter-toth
commented
Aug 29, 2026
The |
What changes were proposed in this pull request?
KeyedPartitioning.groupedSatisfiesrefuses a narrowed, non-grouped partitioning unlessspark.sql.sources.v2.bucketing.allowKeysSubsetOfPartitionKeys.enabledis set, because grouping it would merge partitions that held distinct keys in the original finer-grained partitioning. That guard lived inside therequireAllClusterKeys = falsearm, so it never ran whenspark.sql.requireAllClusterKeysForDistributionwas enabled.This moves the guard above the
requireAllClusterKeyscheck. The skew risk does not depend on which key sets count as matching, so the decision should not either.The config doc for
allowKeysSubsetOfPartitionKeys.enabledis updated as well: it did not mention this second thing the config gates, which is not tied torequireAllClusterKeysForDistributionbeing false.Why are the changes needed?
With
spark.sql.requireAllClusterKeysForDistribution = truea narrowed partitioning was accepted,EnsureRequirementsinserted aGroupPartitionsExec, and the skew exposure was taken with the opt-in config still off. Note this is the stricter of the two settings, which makes it the more surprising direction.Reproduced with a table partitioned by
(id, dept)projected down toid-- keys collapsing to[1, 1, 2]-- and joined onid: withrequireAllClusterKeys = truethe plan gets aGroupPartitionsExecand no shuffle whileallowKeysSubsetOfPartitionKeysis off, whereas withrequireAllClusterKeys = falsethe same query correctly shuffles both sides.Please note that the guard's condition is imprecise, and the hoist makes that imprecision reachable for one more config value.
isNarrowed && !isGroupedis a proxy for "the narrowing collapsed distinct keys", but!isGroupedhas causes that have nothing to do with narrowing: a source that reports several splits per partition key, or a union whose children have distinct keys individually and repeat keys across children. Grouping such a partitioning merges only same-key partitions, which is whatGroupPartitionsExecdoes for any non-narrowed partitioning and needs no opt-in, yet the condition refuses it. Until now that false refusal could only happen withrequireAllClusterKeysForDistribution = false; after this change it can happen with either value. Tightening the condition to actual key collapse is a separate change, which I am working on as a follow-up; this PR keeps the condition as it is and only fixes where it is evaluated.Does this PR introduce any user-facing change?
Yes, a plan-level change. With
requireAllClusterKeysForDistributionenabled, Spark previously coalesced partitions derived from a narrowed partitioning withoutallowKeysSubsetOfPartitionKeys.enabled, risking skewed partitions; it now inserts a shuffle unless that config is enabled. Query results are unchanged.No migration guide entry: the guard, and with it the bypass, arrived in 4.3.0, which is unreleased, so no released version behaves the old way. This goes to
branch-4.3as well.How was this patch tested?
ProjectedOrderingAndPartitioningSuitecallinggroupedSatisfiesdirectly on a narrowed, ungrouped partitioning, for both values ofrequireAllClusterKeysand both values of the opt-in. It fails on master withkp.groupedSatisfies(required) was trueforrequireAllClusterKeys = true.KeyGroupedPartitioningSuiteasserting the guard behaves identically for both values ofrequireAllClusterKeys: noGroupPartitionsExec, and a shuffle instead. It fails on master forrequireAllClusterKeys = true. It also variesv2BucketingShuffleEnabled, because that decides whether the refused side is laid out on the other side's declared partition keys (one shuffle) or both sides are shuffled (two).allowKeysSubsetOfPartitionKeys = truefor both values ofrequireAllClusterKeys, since this is the first time the opt-in affectsgroupedSatisfieswhenrequireAllClusterKeysForDistributionis enabled -- it must restore the coalescing and avoid the shuffles. Neither suite had any coverage ofrequireAllClusterKeysForDistribution, which is how the bug survived.DistributionSuite,KeyGroupedPartitioningSuite,EnsureRequirementsSuite,PlannerSuite,ProjectedOrderingAndPartitioningSuite,DataFrameSetOperationsSuite,AdaptiveQueryExecSuite,CoalesceShufflePartitionsSuite, and the TPC-DS plan stability suites -- no golden file changed.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)