LuciferYang commented on code in PR #58531:
URL: https://github.com/apache/spark/pull/58531#discussion_r3945718698


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -501,24 +521,38 @@ case class EnsureRequirements(
     var newLeft = left
     var newRight = right
 
-    val specs = Seq(left, right).zip(requiredChildDistribution).map { case (p, 
d) =>
-      if (!d.isInstanceOf[ClusteredDistribution]) return None
-      val cd = d.asInstanceOf[ClusteredDistribution]
-      val specOpt = createKeyedShuffleSpec(p.outputPartitioning, cd)
-      if (specOpt.isEmpty) return None
-      specOpt.get
-    }
-
-    val leftSpec = specs.head
-    val rightSpec = specs(1)
+    def candidatesFor(plan: SparkPlan, required: Distribution): 
Seq[KeyedShuffleSpec] =
+      required match {
+        case cd: ClusteredDistribution => 
createKeyedShuffleSpecs(plan.outputPartitioning, cd)
+        case _ => Nil
+      }
+    val leftCandidates = candidatesFor(left, requiredChildDistribution.head)
+    val rightCandidates = candidatesFor(right, requiredChildDistribution(1))
+    if (leftCandidates.isEmpty || rightCandidates.isEmpty) return None
+
+    // Each side may offer several members, and the right one is the one the 
other side can pair
+    // with, which neither side can tell on its own. So pick the pair rather 
than a member per side,
+    // and rank the pairs that agree on the keys by the parallelism they 
offer, the same trade
+    // `ensureDistributionAndOrdering` makes between children when it picks 
`bestSpecOpt`.
+    val agreeingPairs = for {

Review Comment:
   The PR description's argument that the as-is pair "can never be lost" only 
holds under strict inequality. Projected counts never exceed physical ones, so 
no pair strictly outranks the as-is pair, but `maxByOption` breaks ties by 
enumeration order, and a coarse member whose projected distinct count happens 
to equal the physical count, listed before the identity member, ties it and 
wins. Each side then gets an extra `GroupPartitionsExec`, and the grouping 
drops the other key the identity member still carried. This is not a regression 
against the old code, which picked the same head members, so the minimal fix is 
correcting the claim in the description. If you want the cheap improvement, 
break ties in favor of the pair whose specs' partitionings appear verbatim in 
both children.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1253,12 +1241,16 @@ case class ShufflePartitionIdPassThrough(
     copy(expr = newChildren.head.asInstanceOf[DirectShufflePartitionID])
 }
 
-trait ShuffleSpec {
-  /**
-   * Returns the number of partitions of this shuffle spec
-   */
-  def numPartitions: Int
-
+/**
+ * Describes how a child's data is laid out, for the purpose of deciding 
whether two children are
+ * co-partitioned and, if not, what to shuffle the other one onto.
+ *
+ * A [[LeafShuffleSpec]] is one concrete layout. A [[ShuffleSpecCollection]] 
stands for a choice
+ * between several. A collection can answer [[isCompatibleWith]], which 
succeeds when any member
+ * matches. It cannot answer anything that needs one member: which one is 
right depends on what the
+ * other side matched, and only the caller comparing the two sides can see 
that.
+ */
+sealed trait ShuffleSpec {

Review Comment:
   The `ShuffleSpec` family is public in 4.0.0, and this commit removes 
members, adds an abstract `flatten`, and seals the trait. Downstream code that 
still extends catalyst internals sees a real source and binary break: `extends 
ShuffleSpec` no longer compiles, and old bytecode calling `numPartitions` hits 
NoSuchMethodError. The whole catalyst package is in the defaultExcludes section 
of `MimaExcludes` and treated as internals, though, so CI will not flag it and 
no excludes are needed; the KeyGrouped* to Keyed* rename set that precedent.
   
   I'd still add a line to the user-facing-change section of the PR 
description: `sealed` is a source break for external `extends ShuffleSpec` 
code, which needs to move to `LeafShuffleSpec`.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -501,24 +521,38 @@ case class EnsureRequirements(
     var newLeft = left
     var newRight = right
 
-    val specs = Seq(left, right).zip(requiredChildDistribution).map { case (p, 
d) =>
-      if (!d.isInstanceOf[ClusteredDistribution]) return None
-      val cd = d.asInstanceOf[ClusteredDistribution]
-      val specOpt = createKeyedShuffleSpec(p.outputPartitioning, cd)
-      if (specOpt.isEmpty) return None
-      specOpt.get
-    }
-
-    val leftSpec = specs.head
-    val rightSpec = specs(1)
+    def candidatesFor(plan: SparkPlan, required: Distribution): 
Seq[KeyedShuffleSpec] =
+      required match {
+        case cd: ClusteredDistribution => 
createKeyedShuffleSpecs(plan.outputPartitioning, cd)
+        case _ => Nil
+      }
+    val leftCandidates = candidatesFor(left, requiredChildDistribution.head)
+    val rightCandidates = candidatesFor(right, requiredChildDistribution(1))
+    if (leftCandidates.isEmpty || rightCandidates.isEmpty) return None
+
+    // Each side may offer several members, and the right one is the one the 
other side can pair
+    // with, which neither side can tell on its own. So pick the pair rather 
than a member per side,
+    // and rank the pairs that agree on the keys by the parallelism they 
offer, the same trade
+    // `ensureDistributionAndOrdering` makes between children when it picks 
`bestSpecOpt`.
+    val agreeingPairs = for {
+      l <- leftCandidates
+      r <- rightCandidates
+      if l.areKeysCompatible(r)
+    } yield (l, r)
+    val (leftSpec, rightSpec) = agreeingPairs
+      .maxByOption { case (l, r) => l.numPartitions.max(r.numPartitions) }

Review Comment:
   The pair ranking `l.numPartitions.max(r.numPartitions)` measures one side's 
projected count, but what the push branch delivers is the merged key set. Under 
partition filter + Inner that is the intersection, and its size is not monotone 
in member granularity: a strictly finer pair can win the ranking while its 
intersection with the other side comes out far smaller than the coarse pair's, 
so the join ends up with fewer tasks than the old head-of-list pick produced.
   
   Ranking by the key count the pair actually merges would match the delivered 
parallelism, but it means computing reduced keys for every candidate pair, 
which is expensive and can surface reduced-types mismatches at ranking time for 
pairs that would never be chosen. At minimum, the comment should say what `max` 
measures and where it stops matching the delivered parallelism.



-- 
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]

Reply via email to