ulysses-you commented on code in PR #59863:
URL: https://github.com/apache/spark/pull/59863#discussion_r4438940180
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -824,8 +825,9 @@ case class EnsureRequirements(
// Otherwise, query result could be incorrect.
val canReplicateLeft = ShuffledJoin.canDuplicateLeftSide(joinType)
val canReplicateRight = ShuffledJoin.canDuplicateRightSide(joinType)
+ replicateSideChosen = canReplicateLeft || canReplicateRight
Review Comment:
Fixed in 5346f9f5880: `replicateSideChosen` is set in the branch that
chooses a side. It stays true if the join type subsequently declines the choice.
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/exchange/EnsureRequirementsSuite.scala:
##########
@@ -1066,15 +1066,22 @@ class EnsureRequirementsSuite extends
SharedSparkSession {
// simple case
var smjExec = SortMergeJoinExec(
exprA :: exprB :: exprC :: Nil, exprA :: exprC :: exprB :: Nil, Inner,
None, plan1, plan2)
+ // A side whose alignment is the pass-through keeps its own layout, so
the regrouping node
+ // is optional: peel the sort, then read the node's own report, or the
dummy plan's if none.
+ def plannedPartitioning(plan: SparkPlan): Partitioning = plan match {
+ case SortExec(_, _, child, _) => plannedPartitioning(child)
+ // The node's own report is what these assertions are about. Only a
side whose alignment
+ // is the identity has no node, and there the child's layout is the
aligned one.
+ case g: GroupPartitionsExec => g.outputPartitioning
+ case d: DummySparkPlan => d.outputPartitioning
+ case other => fail(s"expected a dummy plan, got $other")
+ }
+
applyEnsureRequirementsWithSubsetKeys(smjExec) match {
- case SortMergeJoinExec(_, _, _, _,
- SortExec(_, _,
- GroupPartitionsExec(DummySparkPlan(_, _, left:
KeyedPartitioning, _, _),
- _, _, _, _, _, _, _, _), _),
- SortExec(_, _,
- GroupPartitionsExec(DummySparkPlan(_, _, right:
KeyedPartitioning, _, _),
- _, _, _, _, _, _, _, _), _),
- _) =>
+ case smj: SortMergeJoinExec =>
+ assert(groupPartitionsNodes(smj).size == 1, "the pruning side is
regrouped")
Review Comment:
Updated in 5346f9f5880: the skip now requires shuffle-stage leaves, so both
sides retain their nodes in these three cases. I restored the original
plan-shape assertions requiring a node on each side.
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -10759,26 +10909,18 @@ class KeyGroupedPartitioningSuite
val plan = df.queryExecution.executedPlan
assert(ValidateRequirements.validate(plan),
s"the executed plan must satisfy every operator's required
distribution:\n$plan")
- // Locate the marked side's node: no reducers, and output partition
i holds exactly input
- // partition i. That is weaker than `identityGrouping`, which also
asks for one output per
- // input, so the assertions below are what pin the claim and the
three keys.
- val identityGpe = collectAllGroupPartitions(plan).find { g =>
- g.reducers.isEmpty && g.groupedPartitions.zipWithIndex.forall {
- case ((_, inputIndices), outputIndex) =>
- inputIndices.lengthCompare(1) == 0 && inputIndices.head ==
outputIndex
- }
- }
- assert(identityGpe.isDefined,
- s"expected a reducer-free identity GroupPartitionsExec,
got:\n$plan")
- identityGpe.get.outputPartitioning match {
- case k: KeyedPartitioning =>
- assert(k.mayContainUnknownPartitionKeys,
- "the identity grouping must keep the unknown-keyed claim")
- assert(k.numPartitions === 3,
- s"the marked side keeps all three of its declared
keys:\n$plan")
- case other =>
- fail(s"expected a KeyedPartitioning output, got $other")
- }
+ // The marked side's alignment is the pass-through, so no node is
built for it: the
+ // claim has to reach the second join on the side itself. Assert on
that join, since the
+ // first join's keyed shuffle already carries a marked three-key
member of its own.
+ val secondJoin = collect(plan) { case j: SortMergeJoinExec => j
}.head
+ assert(collectAllShuffles(plan).size == 1,
+ s"only the first join shuffles:\n$plan")
+ val marked =
PartitioningCollection.representativeOf(secondJoin.left.outputPartitioning)
+ .filter(_.mayContainUnknownPartitionKeys)
+ assert(marked.exists(_.numPartitions == 3),
+ s"the second join's marked side keeps its claim over its three
keys:\n$plan")
Review Comment:
Updated in 5346f9f5880: the marked input is not a shuffle-stage subtree, so
it keeps its identity grouping. The test again requires that node and checks
its claim on the second join input.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -924,15 +928,27 @@ case class EnsureRequirements(
// them. Each side stamps why it is left ungrouped (`UngroupingOrigin`),
so a reader of the
// finished plan judges the pair on the layouts it holds; the stamp also
decides the node's
// routing (`GroupPartitionsExec.distributePartitions`).
- (
- GroupPartitionsExec(rawLeft, leftSpec.joinKeyPositions,
- Some(mergedPartitionKeys), leftReducers,
+ def regroupOrIdentity(
+ child: SparkPlan,
+ spec: KeyedShuffleSpec,
+ reducers: Option[Seq[Option[KeyReducer]]],
+ replicates: Boolean): SparkPlan = {
+ val gpe = GroupPartitionsExec(child, spec.joinKeyPositions,
+ Some(mergedPartitionKeys), reducers,
ungroupingOrigin = Option.when(applyPartialClustering)(
- if (replicateLeftSide) REPLICATED_FOR_JOIN else SPLIT_FOR_JOIN)),
- GroupPartitionsExec(rawRight, rightSpec.joinKeyPositions,
- Some(mergedPartitionKeys), rightReducers,
- ungroupingOrigin = Option.when(applyPartialClustering)(
- if (replicateRightSide) REPLICATED_FOR_JOIN else SPLIT_FOR_JOIN)))
+ if (replicates) REPLICATED_FOR_JOIN else SPLIT_FOR_JOIN))
+ // Keep both alignment nodes when a replicate side is chosen, even if
the join type
+ // declines it: a re-run peels a local sort only through a
`GroupPartitionsExec`, and
+ // that peel is how the replicate-side choice is reproduced.
+ if (!replicateSideChosen &&
+ gpe.outputPartitioning == child.outputPartitioning) {
+ child
+ } else {
+ gpe
+ }
+ }
+ (regroupOrIdentity(rawLeft, leftSpec, leftReducers, replicateLeftSide),
Review Comment:
Fixed in 5346f9f5880: the push message is logged only when the alignment is
committed and at least one node is retained. Added a test for both cases.
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -1158,6 +1164,61 @@ class KeyGroupedPartitioningSuite
testWithCustomersAndOrders(customers_partitions, Array.empty, 2, 0)
}
+ test("SPARK-60618: Skip the pass-through GroupPartitionsExec in a
storage-partitioned " +
+ "join's alignment") {
+ withSQLConf(
+ SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+ // `items` reports more partition keys than the join uses, so the
aligned layout has to be
+ // projected onto the join keys. The shuffled side is laid out on that
same projection, so
+ // its `GroupPartitionsExec` would only re-state the keys its shuffle
already has and must
+ // not be built.
+ SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "true",
Review Comment:
Fixed in 5346f9f5880: moved the plan-shape comment above `withSQLConf`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]