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


##########
sql/core/src/test/scala/org/apache/spark/sql/DataFrameWindowFunctionsSuite.scala:
##########
@@ -1682,6 +1682,39 @@ class DataFrameWindowFunctionsSuite extends 
SharedSparkSession
     }
   }
 
+  test("SPARK-58404: bypass partial WindowGroupLimit") {
+    val df = Seq(
+      ("a", 0, "c"),
+      ("a", 1, "x"),
+      ("a", 2, "y"),
+      ("b", 1, "h"),
+      ("b", 1, "n"),
+      ("c", 1, "z"),
+      ("c", 2, "a")).toDF("key", "value", "order")
+
+    val window = Window.partitionBy($"key").orderBy($"order")
+    val expected = Seq(
+      Row("a", 0, "c", 1),
+      Row("b", 1, "h", 1),
+      Row("c", 2, "a", 1))
+
+    Seq(true, false).foreach { bypass =>
+      withSQLConf(
+        SQLConf.BYPASS_PARTIAL_WINDOW_GROUP_LIMIT.key -> bypass.toString,
+        SQLConf.WINDOW_GROUP_LIMIT_THRESHOLD.key -> "100") {
+        val result = df.withColumn("rn", 
row_number().over(window)).where($"rn" === 1)
+        checkAnswer(result, expected)
+
+        val limits = collect(result.queryExecution.executedPlan) {
+          case w: WindowGroupLimitExec => w
+        }
+        // When bypassed, only the final WindowGroupLimit remains; otherwise 
both partial and
+        // final are present since a shuffle is required.
+        assert(limits.size === (if (bypass) 1 else 2))
+      }
+    }
+  }

Review Comment:
   **Finding 1** (Blocking) **and finding 2** (Non-blocking), both on this 
point -- I read the empty-`partitionSpec` case the other way round, so flagging 
it here rather than opening a new thread.
   
   The partial pass never reduces cardinality *across* partitions -- not here, 
and not with a partition spec either. It's a `mapPartitions` filter, so all it 
ever does is prune *within* one input partition. And that pruning is sound 
because a row's global rank is always >= its rank inside a single input 
partition: anything with global rank <= `limit` also has local rank <= `limit`, 
so the partial can't drop a row the final needs. What it gives you is a hard 
output bound of `limit` rank groups **per input partition**, independent of the 
data.
   
   That bound is why the empty-`partitionSpec` case is where the partial is 
worth *most*, not least:
   
   ```
   -- bypass off
   WindowGroupLimitExec Final          <- AllTuples
   +- Sort [o]
      +- Exchange SinglePartition      <- carries ~ numPartitions * limit rank 
groups
         +- WindowGroupLimitExec Partial
            +- Sort [o]                <- one local sort per input partition, 
in parallel
               +- Scan t
   
   -- bypass on
   WindowGroupLimitExec Final
   +- Sort [o]                         <- sorts the whole table, one task
      +- Exchange SinglePartition      <- carries the whole table
         +- Scan t
   ```
   
   So the flag here costs a full-table shuffle into one reducer plus a 
single-threaded sort of everything, and buys back only the parallel local sorts 
it skipped. Downside unbounded, upside bounded.
   
   With a partition spec it's the opposite: the exchange is a hash exchange to 
many reducers so post-shuffle work stays parallel, and the partial's bound is 
`limit` rank groups per input partition **per key** -- many keys with few rows 
each means it prunes nothing and is pure overhead. That is the "low reduction 
ratio" workload the config is for, and it can only really happen when there is 
a partition spec.
   
   This is the same asymmetry `bypassPartialAggregation` ran into -- 
`AggUtils.scala:136-141`:
   
   > The bypass is only beneficial when there are grouping keys 
(`groupingExpressions.nonEmpty`): global aggregations (no GROUP BY) always 
produce a single output row, so the pre-shuffle partial aggregation achieves 
the maximum possible reduction ratio and should never be skipped. Bypassing a 
global aggregation would shuffle all raw rows to a single partition with no 
benefit, which is strictly worse than the normal Partial+Final path.
   
   An empty `partitionSpec` is that same situation, so I'd mirror the guard in 
`SparkStrategies.scala:808`:
   
   ```scala
   val finalChild = if (conf.bypassPartialWindowGroupLimit && 
partitionSpec.nonEmpty) {
   ```
   
   If leaving it ungated is a deliberate "the user asked for it, don't 
second-guess" call, that's defensible, but then it's worth stating in the 
config doc.
   
   Either way, the comment added at 
`DataFrameWindowFunctionsSuite.scala:1723-1727` needs rewording -- it currently 
writes "the partial pass (which cannot reduce cardinality across partitions 
here)" into the test as the justification. Separately in that same comment, the 
`row_number()` note drops a condition: `InferWindowGroupLimit.scala:118-122` 
only rewrites to `Limit` when `partitionSpec` is empty **and** `limit < 
topKSortFallbackThreshold`. It holds for `limit = 1` here, so the test is fine, 
but the exclusion isn't unconditional.
   



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