peter-toth commented on code in PR #58279:
URL: https://github.com/apache/spark/pull/58279#discussion_r3931930149


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/EnsureRequirements.scala:
##########
@@ -634,11 +642,16 @@ case class EnsureRequirements(
                    |""".stripMargin)
               leftLink.get.stats.sizeInBytes < rightLink.get.stats.sizeInBytes
             } else {
-              // As a simple heuristic, we pick the side with fewer number of 
partitions
-              // to apply the grouping & replication of partitions
+              // As a simple heuristic, we pick the side with fewer number of 
partitions to
+              // apply the grouping & replication of partitions. The counts 
read the
+              // pre-alignment plans, for the same reason the statistics do: 
on a re-run both
+              // aligned reports hold the same number of keys, so comparing 
them decides nothing.
               logInfo("Using number of partitions to determine which side of 
join " +
                   "to fully cluster partition values")
-              leftPartKeys.size < rightPartKeys.size
+              
PartitioningCollection.numKeyedPartitions(unwrappedLeft.outputPartitioning)

Review Comment:
   **Finding 12.** This implements [finding 
10](https://github.com/apache/spark/pull/58279#discussion_r3924540492) and the 
invariant it asked for holds now: the fallback reads one number per side, from 
the pre-alignment plan, on every pass. It reaches that by changing what a first 
pass compares, and that half is not stated anywhere.
   
   Base's number is `leftSpec`/`rightSpec`'s key count, i.e. the *aligned* 
report's. Finding 10 was about what that is on a re-run: equal on both sides, 
so the comparison decides nothing. On a first pass it is a third thing. The 
children loop has already wrapped a non-grouped child, so the report holds one 
key per distinct value and base compared **distinct keys**. This head compares 
**raw splits**, and the two differ for exactly the shape partial clustering is 
about, a side with more than one split per key.
   
   Measured on a bare first pass -- two `DummySparkPlan`s under a 
`SortMergeJoinExec`, no `logicalLink`, so the fallback fires -- reading 
`(left.distributePartitions, right.distributePartitions)` back off the planned 
children:
   
   | left | right | `0875765` | this head |
   |---|---|---|---|
   | 3 splits, 1 distinct key | 2 splits, 2 keys | `(false, true)` | `(true, 
false)` |
   | 4 splits, 2 distinct keys | 3 splits, 3 keys | `(false, true)` | `(true, 
false)` |
   | 2 splits, 2 keys | 3 splits, 3 keys | `(false, true)` | `(false, true)` |
   
   I am not asking for the old number back. Splits are the better proxy, and 
your new unit test's first arm is the case that shows it: one split against 
three, and the base picks the three-split side to replicate. The two coherent 
options are this head and reverting finding 10, and this head is the right one.
   
   What is missing is one sentence in each of two places. The comment says "on 
a re-run both aligned reports hold the same number of keys, so comparing them 
decides nothing", which reads as if a first pass is untouched, and "Does this 
PR introduce any user-facing change?" lists only the duplicated rows. This is a 
wrong-results fix headed for `branch-4.2` and `branch-4.3`, so a plan change 
riding along in a maintenance backport is worth naming:
   
   ```scala
                 // As a simple heuristic, we pick the side with fewer number 
of partitions to
                 // apply the grouping & replication of partitions. The counts 
read the
                 // pre-alignment plans, for the same reason the statistics do: 
on a re-run both
                 // aligned reports hold the same number of keys, so comparing 
them decides nothing.
                 // This also changes a first pass, which compared the aligned 
report's distinct
                 // keys rather than the splits behind them.
   ```
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/physical/partitioning.scala:
##########
@@ -1133,6 +1133,14 @@ object PartitioningCollection {
     case _ => None
   }
 
+  /**
+   * The number of partitions of the [[KeyedPartitioning]] representing the 
keyed members of
+   * `partitioning`, if any. Collections validate on construction that their 
keyed members agree,
+   * so the representative's count stands for all of them.
+   */
+  def numKeyedPartitions(partitioning: Partitioning): Option[Int] =

Review Comment:
   **Finding 14.** This is public, and its one caller is `EnsureRequirements`, 
so `private[sql]` covers it. Worth narrowing because the method is a one-line 
wrapper over `representativeOf`, which is `private[physical]` deliberately: as 
written, the count of a keyed member becomes reachable from anywhere on the 
classpath in one hop.
   
   ```scala
     private[sql] def numKeyedPartitions(partitioning: Partitioning): 
Option[Int] =
   ```
   
   Separately, the scaladoc understates what holds. `require` at `:1049` 
already forces *every* member to agree on `numPartitions`, keyed or not, so the 
answer is `partitioning.numPartitions` whenever there is a keyed member at all 
-- the representative only decides whether there is one. Saying that is shorter 
than the agreement argument and it is the stronger fact.
   



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