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


##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -8185,7 +8204,10 @@ class KeyGroupedPartitioningSuite
       withSQLConf(
           SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> "true",
           "spark.sql.autoBroadcastJoinThreshold" -> "-1",
-          SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+          SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+          // The identity grouping only appears when the marked side is padded 
up to the union
+          // of both sides' keys instead of being narrowed to their 
intersection.
+          SQLConf.V2_BUCKETING_PARTITION_FILTER_ENABLED.key -> "false") {

Review Comment:
   **Finding 1.** The comment misreads why this test fails with filtering on, 
and the pin is what hides finding 2.
   
   I ran the test with only this line changed to `"true"`. `identityGpe` is 
still found, so the identity grouping does still appear. The failure is one 
assertion later, at line 8229:
   
   ```
   - SPARK-59050: SPJ: a reducer-free identity regrouping keeps the 
unknown-keyed claim *** FAILED ***
     expected a KeyedPartitioning output, got UnknownPartitioning(2) 
(KeyGroupedPartitioningSuite.scala:8229)
   ```
   
   The test's own predicate only checks that output partition i holds input 
partition i, which the intersection satisfies. `identityGrouping` asks more: it 
also requires `grouping.partitions.size == 
child.outputPartitioning.numPartitions`, and the intersection leaves 2 groups 
over the marked side's 3 input partitions. So the marked layout gives up its 
keyed claim, which is finding 2, on the very test that exists to pin that claim 
surviving a regrouping.
   
   Two things follow. The comment should say that, not that the identity 
grouping disappears. And this test should keep running on the shipping default, 
because the branch it pins is reachable there. If finding 2 is closed by not 
filtering a marked side, the pin can go away entirely.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -2547,7 +2547,7 @@ object SQLConf {
         s"enabled.")
       .version("4.0.0")
       .booleanConf
-      .createWithDefault(false)
+      .createWithDefault(true)

Review Comment:
   **Finding 2.** This flip makes SPARK-59272 reachable one opt-in away from 
the defaults, instead of two.
   
   Partition filtering is the only path that shrinks the pushed-down key list 
*below* a side's own declared keys. `pushPartValues` padding always merges to a 
superset, so no side ever has to drop one of its groups. An intersection can, 
and then `alignToExpectedKeys` leaves that side with fewer groups than it has 
input partitions. `identityGrouping` rejects that on the count clause, so a 
marked `KeyedPartitioning` becomes `UnknownPartitioning` 
(`GroupPartitionsExec.scala:101`). Its own doc names this exact case: "A 
grouping that drops trailing declared keys still reads identity for every group 
it keeps, but the partition count shrinks and the hash modulus with it."
   
   The give-up is meant to make the parent shuffle. Nothing reads it here, 
which is the gap you described at 
[r3939155993](https://github.com/apache/spark/pull/58339#discussion_r3939155993)
 and filed as SPARK-59272: "the committed pair then carries a child 
`ValidateRequirements` rejects, at the cost of AQE rules on that stage". That 
ticket is still open and unassigned.
   
   Measured in a worktree at `c2c91ea`, on the query from your own test at 
`KeyGroupedPartitioningSuite.scala:8177` (`pa`=[1,2,3], `pt`=[1,2,3,4], 
`pb`=[1,2], `v2.bucketing.shuffle.enabled=true`, AQE off), reading 
`ValidateRequirements.validate(df.queryExecution.executedPlan)`:
   
   | `partition.filter.enabled` | left `GroupPartitionsExec.outputPartitioning` 
| `validate` |
   |---|---|---|
   | `false` (today's default) | `KeyedPartitioning`, 3 groups over 3 input 
partitions | `true` |
   | `true` (this PR) | `UnknownPartitioning(2)`, 2 groups over 3 | **`false`** 
|
   
   Both arms keep exactly one shuffle, so the join above the give-up node is 
never re-planned.
   
   **Why this is not Blocking, so you can weigh it as I do.** Results stay 
correct, and not by luck. `areKeysCompatible`'s marked path forces the unmarked 
side's declared keys to be a subset of the marked side's, so the merged list 
can never hold a key the marked side does not declare, so the marked side's 
undeclared rows can never match anything on the other side wherever the 
regrouping puts them. I tried the shape that would break that (`pb`=[1,2,4] 
against a marked [1,2,3]) and the gate refuses the pairing there, both sides 
shuffle. The cost is the one you named: `ValidateRequirements` is read only by 
`AdaptiveSparkPlanExec:209` and `OptimizeSkewedJoin:261`, and both revert their 
change when it fails, so coalescing, local read, rebalance skew and skew join 
are all silently skipped for a stage holding a give-up node. I could not 
exhibit that loss. My attempt put a coalescable hash exchange in the same 
stage, and the control arm with no SPJ at all produced no coalesced read 
either, 
 so the harness measured nothing.
   
   So what I can show is the invariant violation and the ticket, not a wrong 
answer or a measured regression. Worth deciding on rather than merging past, 
but I am not blocking on it.
   
   **#58659 implements the pairwise check SPARK-59272 asks for**, which I 
should disclose is my own draft PR. It compares the two rebuilt children's 
declared key sequences before committing the pairing, so it is the 
joint-alignment check you described rather than a per-side `satisfies`. Same 
probe on its head `27ef3a60f1f`:
   
   | `partition.filter.enabled` | plan | `validate` |
   |---|---|---|
   | `false` | 1 shuffle, 2 `GroupPartitionsExec` | `true` |
   | `true` | 2 shuffles, 0 `GroupPartitionsExec` | `true` |
   
   It is a draft though, and it is stacked on #58531 and #58552, so it is not a 
short wait.
   
   Three ways out, in the order I would pick them:
   
   1. Land the two ordering flips now and hold `partition.filter.enabled` until 
SPARK-59272 is closed. Nothing I found argues against the other two.
   2. Keep all three and stop filtering a marked side, so this PR does not 
depend on my stack. This is not the per-side `satisfies` you ruled out, it is a 
guard on the merge input, so partial clustering is untouched. It is also the 
principled version: the intersection argument assumes a side's declared key 
list accounts for all of its rows, and `mayContainUnknownPartitionKeys` says it 
does not.
   
      ```scala
      // EnsureRequirements.scala, at the only call site (~:620)
      var mergedPartitionKeys =
        mergeAndDedupPartitions(leftReducedKeys, rightReducedKeys, joinType, 
reducedKeyOrdering,
          // A marked side holds rows whose key is not in its declared list, so 
an intersection cannot
          // reason about them, and dropping one of its groups forfeits the 
keyed claim it needs.
          filter = !leftPartitioning.mayContainUnknownPartitionKeys &&
            !rightPartitioning.mayContainUnknownPartitionKeys)
          .map((_, 1))
      ```
   
      A new trailing parameter with a default keeps the 
`EnsureRequirementsSuite` call site as it is.
   3. Keep all three and say in the description that the flip puts SPARK-59272 
one opt-in from the defaults. That drops the "None of them adds runtime cost" 
claim for this one.
   



##########
docs/sql-performance-tuning.md:
##########
@@ -688,6 +688,46 @@ The following SQL properties enable Storage Partition Join 
in different join que
       </td>
       <td>4.0.0</td>
     </tr>
+    <tr>
+      
<td><code>spark.sql.sources.v2.bucketing.partition.filter.enabled</code></td>
+      <td>true</td>
+      <td>
+        When enabled, key groups that cannot produce output for the join type 
are not scanned at all, instead of being filled with empty partitions on the 
side that does not hold them. For example, an inner join only scans the key 
groups present on both sides. This config requires both 
<code>spark.sql.sources.v2.bucketing.enabled</code> and 
<code>spark.sql.sources.v2.bucketing.pushPartValues.enabled</code> to be true.
+      </td>
+      <td>4.0.0</td>
+    </tr>
+    <tr>
+      <td><code>spark.sql.sources.v2.bucketing.sorting.enabled</code></td>
+      <td>false</td>
+      <td>
+        When enabled, Spark satisfies a sort on the partition key expressions 
from the partitioning reported by a V2 data source, so no shuffle is added for 
that sort. The parallelism of the sorted output is then whatever the data 
source's partition layout provides, and there is no shuffle stage left for 
adaptive partition coalescing or skew splitting to balance. This config 
requires <code>spark.sql.sources.v2.bucketing.enabled</code> to be true.
+      </td>
+      <td>4.0.0</td>
+    </tr>
+    <tr>
+      
<td><code>spark.sql.sources.v2.bucketing.partitionKeyOrdering.enabled</code></td>
+      <td>true</td>
+      <td>
+        When enabled, Spark derives the output ordering of a V2 scan from its 
partition key expressions, if the source reports a keyed partitioning but no 
explicit ordering. All rows of such a partition share one key value, so the 
partition is trivially sorted by those expressions, and a sort Spark would 
otherwise add becomes unnecessary. This config requires 
<code>spark.sql.sources.v2.bucketing.enabled</code> to be true.
+      </td>
+      <td>4.2.0</td>
+    </tr>
+    <tr>
+      
<td><code>spark.sql.sources.v2.bucketing.preserveKeyOrderingOnCoalesce.enabled</code></td>
+      <td>true</td>
+      <td>
+        When enabled, <code>GroupPartitionsExec</code> reports sort orders 
over partition key expressions after coalescing several input partitions into 
one. The merged partitions share the same partition key value, so these orders 
still hold, while orders over other columns are lost by the concatenation. This 
config requires <code>spark.sql.sources.v2.bucketing.enabled</code> to be true.
+      </td>
+      <td>4.2.0</td>
+    </tr>
+    <tr>
+      
<td><code>spark.sql.sources.v2.bucketing.preserveOrderingOnCoalesce.enabled</code></td>
+      <td>false</td>
+      <td>
+        When enabled, <code>GroupPartitionsExec</code> merges partitions that 
share a key by a sorted merge rather than by concatenation, so it can report 
the child's full ordering instead of only the orderings over partition key 
expressions that 
<code>spark.sql.sources.v2.bucketing.preserveKeyOrderingOnCoalesce.enabled</code>
 preserves. This removes a downstream sort when data is both partitioned and 
sorted, but a sorted merge costs more than concatenation, especially when 
merging many partitions. This config requires 
<code>spark.sql.sources.v2.bucketing.enabled</code> to be true.

Review Comment:
   **Finding 3.** This row gives the per-row cost of the sorted merge but not 
the larger one: enabling it gives up columnar execution. 
`GroupPartitionsExec.supportsColumnar` is `child.supportsColumnar && 
!usesSortedMerge` (`GroupPartitionsExec.scala:488`), so a vectorized Parquet or 
Iceberg scan under the merge gains a `ColumnarToRow`. Your PR description 
states this cost, the row a user reads does not.
   
   ```suggestion
           When enabled, <code>GroupPartitionsExec</code> merges partitions 
that share a key by a sorted merge rather than by concatenation, so it can 
report the child's full ordering instead of only the orderings over partition 
key expressions that 
<code>spark.sql.sources.v2.bucketing.preserveKeyOrderingOnCoalesce.enabled</code>
 preserves. This removes a downstream sort when data is both partitioned and 
sorted, but a sorted merge costs more than concatenation, especially when 
merging many partitions, and it gives up columnar execution for the merged 
plan. This config requires <code>spark.sql.sources.v2.bucketing.enabled</code> 
to be true.
   ```
   



##########
docs/sql-performance-tuning.md:
##########
@@ -688,6 +688,46 @@ The following SQL properties enable Storage Partition Join 
in different join que
       </td>
       <td>4.0.0</td>
     </tr>
+    <tr>
+      
<td><code>spark.sql.sources.v2.bucketing.partition.filter.enabled</code></td>
+      <td>true</td>
+      <td>
+        When enabled, key groups that cannot produce output for the join type 
are not scanned at all, instead of being filled with empty partitions on the 
side that does not hold them. For example, an inner join only scans the key 
groups present on both sides. This config requires both 
<code>spark.sql.sources.v2.bucketing.enabled</code> and 
<code>spark.sql.sources.v2.bucketing.pushPartValues.enabled</code> to be true.

Review Comment:
   **Finding 4.** `pushPartValues.enabled` is not strictly required. 
`EnsureRequirements.scala:548` enters the merge block on 
`v2BucketingPushPartValuesEnabled || 
v2BucketingAllowKeysSubsetOfPartitionKeys`, and `mergeAndDedupPartitions` is 
the only reader of this config. So with `pushPartValues.enabled` false and 
`allowKeysSubsetOfPartitionKeys.enabled` true, filtering still applies.
   
   The same wording is in the `.doc` string, so it is pre-existing, but this PR 
is the one writing it into the user-facing guide.
   
   ```suggestion
           When enabled, key groups that cannot produce output for the join 
type are not scanned at all, instead of being filled with empty partitions on 
the side that does not hold them. For example, an inner join only scans the key 
groups present on both sides. This config requires 
<code>spark.sql.sources.v2.bucketing.enabled</code> to be true, together with 
either <code>spark.sql.sources.v2.bucketing.pushPartValues.enabled</code> or 
<code>spark.sql.sources.v2.bucketing.allowKeysSubsetOfPartitionKeys.enabled</code>.
   ```
   



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