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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpression.scala:
##########
@@ -183,6 +215,15 @@ object RewriteWithExpression extends Rule[LogicalPlan] {
         val newExpr = c.withNewAlwaysEvaluatedInputs(newAlwaysEvaluatedInputs)
         // Use transformUp to handle nested With.
         newExpr.transformUpWithPruning(_.containsPattern(WITH_EXPRESSION)) {
+          case w @ With(_, defs) if defs.exists(shouldPreEvaluateInBranch(_, 
commonExprIdSet)) =>

Review Comment:
   Thanks for the pointer on lazy memoization. I went and tried the closest 
thing reachable in the plan, and it does not hold up. Reporting the negative 
result here since it rules out a direction rather than just failing to reach 
one.
   
   **The guard experiment.** The idea was to keep the eager pre-evaluation but 
wrap the column in `If(branch is reached, definition, default)`, so a stateful 
generator is only advanced on the rows that reach the branch. Once-when-taken, 
zero-times-otherwise, expressed in the plan.
   
   It breaks on subexpression elimination. A guard in a plain `Project` is 
outside conditional evaluation, so to CSE it is an `alwaysEvaluatedInputs` of 
the `If`: a part repeated across guards gets hoisted to the top of the 
projection and evaluated up front 
(`spark.sql.subexpressionElimination.skipForShortcutExpr` is false by default). 
A condition that can raise then raises on rows the branch never reached:
   
   ```sql
   SELECT CASE WHEN a = 0 THEN false
               WHEN 6 / a > 2 THEN rand(1) BETWEEN 0 AND 1
               WHEN 6 / a < -2 THEN rand(2) BETWEEN 0 AND 1
               ELSE false END
   FROM t   -- t contains a = 0
   ```
   
   gives `DIVIDE_BY_ZERO`, frame in `project_subExpr_0$`. Limiting how many 
guards a projection may hold does not help, since a single guard repeating `6 / 
a` internally does the same.
   
   The only fix I found is to build guards out of nodes that cannot raise, and 
that leaves very little:
   
   | condition | guard |
   |---|---|
   | `a > 0`, `a > b`, `a IS NULL`, `a IN (1,2,3)`, `s = 'x'` | kept |
   | `a + b > 0`, `a * 2 > b`, `abs(a) > 0`, `upper(s) = 'X'`, `cast(a as long) 
> 0` | dropped |
   
   Any arithmetic, function call or cast on a column loses it, and `WHEN a + b 
> 0` is an ordinary thing to write. It is also dropped when the guard reads the 
other side of a join, when the condition carries a subquery, when a preceding 
condition is nondeterministic, and for conditional expressions outside 
`If`/`CaseWhen`/`Coalesce`/`NaNvl`. All silently. A mechanism that only applies 
to plain column comparisons and fails invisibly elsewhere does not deliver the 
property you asked for, so I left it out of the PR rather than shipping a 
version of it.
   
   **On excluding stateful generators.** I don't think that can be satisfied 
together with the fix. The generators whose extra evaluations are observable 
are exactly the ones that produce the wrong result: `rand`, `randn`, `uuid`, 
`monotonically_increasing_id`. What remains after excluding them — 
`spark_partition_id`, `input_file_name`, the input-file-block pair — is 
constant within a task, so evaluating it twice was never wrong to begin with. 
Excluding them removes the fix rather than narrowing it, and the end-to-end 
test in `ColumnExpressionSuite` would have to go with it.
   
   For what the eager path does cost: Spark makes no commitment about which row 
draws which value from a seeded generator. The seed is `seed + partitionIndex`, 
so the sequence already moves with partitioning, splits and AQE, and 
`spark.sql.alwaysInlineCommonExpr` already makes it configuration-dependent. 
There is precedent for shipping that kind of shift as a migration note — since 
4.3 nondeterministic filters are no longer pushed to DSv2, for the same class 
of reason. Happy to add a migration-guide entry here if you want one.
   
   **Lazy memoization itself.** Filed as SPARK-58902 with the shapes it would 
fix. It is worth more than the guard was: the allowlist in this PR turns 
expressions down by shape rather than behavior, so `randstr(3, 0) BETWEEN 'a' 
AND 'b'` inside a branch still gets two independent draws today even though 
only `randstr(-1, 0)` raises. Memoization would fix that class in one go, and 
it would also close the pre-existing join TODO in this rule. It needs an 
evaluable expression with per-row caching plus codegen support, which `With` / 
`CommonExpressionDef` / `CommonExpressionRef` cannot be today, so it is a 
separate change rather than something to fold in here.
   
   Your call on the scope of this PR.
   



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