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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -1761,9 +1761,38 @@ object CollapseWindow extends Rule[LogicalPlan] {
       s1.zip(s2).forall(e => e._1.semanticEquals(e._2))
   }
 
+  /**
+   * Returns true if the given window expression can be evaluated under any 
ordering of the rows
+   * within a partition without changing the result, so that it can be merged 
into another window
+   * with a different (non-empty) order spec.
+   *
+   * The frame determines whether the ordering matters. When the frame is the 
whole partition
+   * (`UNBOUNDED PRECEDING` to `UNBOUNDED FOLLOWING`), it always covers all 
the rows of the
+   * partition regardless of the ordering, so the ordering does not affect the 
result: aggregates
+   * such as `count` or `sum` give the same value under any ordering, and 
functions whose result
+   * does depend on the row order, such as `collect_list` or `first`, are 
non-deterministic when
+   * the order spec is empty, so evaluating them under any ordering yields a 
valid result. On the
+   * other hand, a bounded frame (e.g. `ROWS BETWEEN UNBOUNDED PRECEDING AND 
CURRENT ROW`) is
+   * order-sensitive: which rows are in the frame depends on the ordering, so 
even `count` or
+   * `sum` would change value, and such a window must not be merged.
+   */
+  private def orderInsensitive(windowExpression: NamedExpression): Boolean =
+    windowExpression match {
+      case Alias(WindowExpression(_, WindowSpecDefinition(_, _,
+          SpecifiedWindowFrame(_, UnboundedPreceding, UnboundedFollowing))), 
_) => true
+      case _ => false
+    }
+
   private def windowsCompatible(w1: Window, w2: Window): Boolean = {
     specCompatible(w1.partitionSpec, w2.partitionSpec) &&
-      specCompatible(w1.orderSpec, w2.orderSpec) &&
+      // The order specs can differ when one of them is empty, as long as the 
window expressions
+      // of the window with the empty order spec are insensitive to the row 
order. In that case,
+      // they can be evaluated under the non-empty order spec of the other 
window.
+      (specCompatible(w1.orderSpec, w2.orderSpec) ||
+        (w1.orderSpec.isEmpty && w2.orderSpec.nonEmpty &&
+          w1.windowExpressions.forall(orderInsensitive)) ||
+        (w2.orderSpec.isEmpty && w1.orderSpec.nonEmpty &&
+          w2.windowExpressions.forall(orderInsensitive))) &&

Review Comment:
   Late here, and I'm not re-opening this -- @ulysses-you's answer on FP 
arithmetic reads fine to me. Two things I measured on this head that seem worth 
having on record.
   
   First, in @ulysses-you's favour: the exposure is narrower than it looks. 
`PushDownLocalSort` (`spark.sql.execution.pushDownLocalSort`, default on since 
4.3.0) already widens the lower local sort through an empty-order `WindowExec`, 
so the base plan for the inner-window shape is already a single `Sort [c1 ASC, 
c2 ASC]` feeding both windows. With default confs I get identical results 
before and after this PR in both merge directions. 
`PushDownLocalSort.isOrderPreserving` even has `case _: WindowExecBase => true` 
with a comment making this PR's argument. That, rather than 
`EliminateSorts.isOrderIrrelevantAggs`, is the precedent I'd cite in the 
description -- as written, the description cites a helper that argues the other 
way.
   
   Second, a case the "use decimal type" answer doesn't cover. With 
`spark.sql.execution.pushDownLocalSort=false`, a *deterministic* Scala UDAF 
changes value:
   
   ```scala
   case class FirstBuf(seen: Boolean, value: Int)
   object FirstIntAgg extends Aggregator[Int, FirstBuf, Int] { /* returns the 
first row it sees */ }
   spark.udf.register("udaf_first", udaf(FirstIntAgg))
   ```
   
   ```sql
   SELECT DISTINCT c1, cl, fst, udafFst FROM (
     SELECT c1,
       collect_list(c2) OVER (PARTITION BY c1) AS cl,
       first(c2)        OVER (PARTITION BY c1) AS fst,
       udaf_first(c2)   OVER (PARTITION BY c1) AS udafFst,
       row_number() OVER (PARTITION BY c1 ORDER BY c2) AS rk
     FROM t3) WHERE rk >= 1
   ```
   
   ```
   base: [0, [8, 4, 2, 10, 6], 8, 8]
   PR:   [0, [2, 4, 6, 8, 10], 2, 2]
   ```
   
   `isOrderIrrelevantAggs` has `case _: UserDefinedExpression => false` for 
precisely this, and a user cannot switch a UDAF to decimal. Same for a pandas 
`GROUPED_AGG` UDF, which is deterministic unless marked otherwise.
   
   Worth noting the two merge directions are not equally exposed. When the 
empty-order window is the *parent* -- the shape in the PR description and the 
one the benchmark measures -- the merge cannot change anything: 
`WindowExecBase.requiredChildOrdering` for an empty order spec is just 
`[partitionSpec]`, which the ordered child window's `[partitionSpec ++ 
orderSpec]` output ordering already satisfies, so no sort is inserted and the 
row order is identical. I measured that direction as unchanged even with 
`pushDownLocalSort=false`. Everything above is the *child*-empty direction. So 
restricting the relaxation to the parent-empty case removes this question 
entirely -- and it also fixes a `WindowGroupLimit` regression I raised 
separately as finding 1.
   



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