[SPARK-58099][SQL] Remove the local sort dangling below the shuffle added by skew join optimization#57221
[SPARK-58099][SQL] Remove the local sort dangling below the shuffle added by skew join optimization#57221ulysses-you wants to merge 2 commits into
Conversation
…dded by skew join optimization ### What changes were proposed in this pull request? Enhance `RemoveRedundantSorts` to also strip a local sort that dangles right below a shuffle which neither consumes nor exposes an ordering (its `requiredChildOrdering` and `outputOrdering` are both empty), and move `RemoveRedundantSorts` after `OptimizeSkewedJoin` in both `AdaptiveSparkPlanExec` and `QueryExecution` so it can see the extra shuffle added by skew join optimization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
cc @viirya @cloud-fan @LuciferYang thank you |
cloud-fan
left a comment
There was a problem hiding this comment.
0 blocking, 0 non-blocking, 0 nits.
Small, correct, well-motivated enhancement. The new RemoveRedundantSorts case is result-preserving, the rule reorder is deliberate and safe, and the test exercises the branch end-to-end.
No findings.
Verification
Traced the Shuffle(SortExec(local, child)) -> Shuffle(child) rewrite: a shuffle repartitions rows across the network, so dropping an input within-partition sort cannot change the output row multiset or partition assignment (equivalent for empty / single / many rows, NULLs, and duplicates). The one case where within-partition determinism matters -- round-robin / null-aware partitioning (SPARK-23207) -- is handled by the shuffle's own internal local sort in prepareShuffleDependency, never via an external SortExec child, so it is unaffected. The guards (requiredChildOrdering.head.isEmpty && outputOrdering.isEmpty) exclude any shuffle that needs or exposes an ordering, and .head is safe since a shuffle is a UnaryExecNode. The reorder to run after OptimizeSkewedJoin does not break any rule that previously ran after RemoveRedundantSorts (RemoveRedundantWindowGroupLimits, DisableUnnecessaryBucketedScan, OptimizeSkewedJoin are all insensitive to whether a redundant sort was already removed).
|
The change looks correct to me. I verified the tricky interactions: the A few comments:
|
Address review comments on apache#57221: - Clarify in `RemoveRedundantSorts` doc why the `outputOrdering.isEmpty` guard is needed (a custom `ShuffleExchangeLike` that preserves the child ordering reports a non-empty `outputOrdering` and must keep its local sort). - Update the stale `QueryExecution.preparations` comment and note that `RemoveRedundantSorts` is placed after `DisableUnnecessaryBucketedScan` to mirror the AQE rule order. - Add UTs to `RemoveRedundantSortsSuite` (both AE / non-AE variants): one that exercises the new dangling-sort removal branch directly on a physical plan, and one regression test for the reorder-vs-`DisableUnnecessaryBucketedScan` correctness hazard (a local sort satisfied only by a bucketed scan output ordering must be kept). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@viirya thanks for the careful review! Addressed all four points in the followup commit:
|
What changes were proposed in this pull request?
This PR enhances
RemoveRedundantSortsto remove a dangling local sort -- a localSortExecsitting directly below a shuffle that neither consumes nor exposes an ordering (both itsrequiredChildOrderingandoutputOrderingare empty). Such a shuffle destroys the child ordering, so the sort has no effect and is dead.To make this effective for the skew-join case,
RemoveRedundantSortsis moved afterOptimizeSkewedJoinin the AQEqueryStagePreparationRules(AdaptiveSparkPlanExec), and the same reorder is applied to the non-AQEQueryExecution.preparationsto keep the two rule chains consistent. In both chainsRemoveRedundantSortsnow runs afterDisableUnnecessaryBucketedScan(see the correctness note below).Why are the changes needed?
Consider a stage with a
ShuffledHashJoinfeeding aSortMergeJoinon the same key, where the SHJ output partitioning already satisfies the SMJ requirement (no exchange in between, only the SMJ's local sort). When the SHJ is skewed and skew-join optimization is applied, an extra shuffle is inserted between the two joins.EnsureRequirementswraps that shuffle on top of the existingSort -> SHJ, and adds a new sort above the shuffle for the SMJ. The original local sort is then left dangling right below the newly inserted shuffle, computed in the SHJ's stage for nothing.The current
RemoveRedundantSortscannot remove it: it only strips a sort whose child already satisfies the ordering, whereas this sort is dead because it sits directly under an ordering-destroying shuffle -- a different kind of redundancy. Running beforeOptimizeSkewedJoinalso means it never sees the added shuffle.Does this PR introduce any user-facing change?
No. It only removes a redundant sort operator from the physical plan (and fixes the latent ordering-loss hazard described above).
How was this patch tested?
New UT
SPARK-58099inAdaptiveQueryExecSuitereproduces the SHJ->SMJ skew-join scenario: with the rule disabled it asserts a local sort dangles below the added shuffle, and with the rule enabled it asserts the sort is removed while both joins remain skew joins and results are unchanged.New UTs in
RemoveRedundantSortsSuite(both AE / non-AE variants):remove local sort dangling below a shuffle that does not require orderingexercises the new removal branch directly on a physical plan (the dangling-sort shape cannot be produced from SQL/DataFrame because the logicalEliminateSortsstrips such a local sort before physical planning), covering local-sort removal, global-sort preservation, and the rule-disabled case.keep local sort satisfied only by a bucketed scan output orderingis a regression test for the reorder-vs-DisableUnnecessaryBucketedScancorrectness hazard; it fails with the old rule order and passes with the new one.Also verified
RemoveRedundantSortsSuite(both AE / non-AE variants),DisableUnnecessaryBucketedScanSuite, and the fullAdaptiveQueryExecSuitepass.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 4.8)