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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/NearestByJoin.scala:
##########
@@ -20,10 +20,17 @@ package org.apache.spark.sql.catalyst.plans.logical
 import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression}
 import org.apache.spark.sql.catalyst.plans.{Inner, JoinType, LeftOuter, 
NearestByDirection, NearestByJoinValidation}
 import org.apache.spark.sql.catalyst.trees.TreePattern._
+import org.apache.spark.sql.internal.SQLConf
 
 object NearestByJoin {
   /** @see [[NearestByJoinValidation.MaxNumResults]] */
   val MaxNumResults: Int = NearestByJoinValidation.MaxNumResults
+
+  /** Whether the right side of a NearestByJoin is eligible for broadcast 
execution. */
+  def canBroadcastRight(j: NearestByJoin, conf: SQLConf): Boolean =
+    conf.nearestByBroadcastEnabled &&
+      j.right.stats.sizeInBytes >= 0 &&

Review Comment:
   **Finding 2.** (Carried from round 1, escalated to Blocking — continues the 
round-1 thread on `RewriteNearestByJoin.scala:83`.)
   
   This predicate is fine when `NearestByJoinSelection` calls it, but 
`RewriteNearestByJoin` calls it from the `FinishAnalysis` batch — the 
optimizer's *first* batch. `Optimizer.scala:239-242` states the invariant:
   
   > This batch pushes filters and projections into scan nodes. Before this 
batch, the logical plan may contain nodes that do not report stats. **Anything 
that uses stats must run after this batch.**
   
   Your round-1 reply argued the early read is conservative because estimates 
only shrink. That's true of the *direction*, but the problem is that some 
relations cannot answer at all this early:
   
   **1. DSv2 right side throws.** `DataSourceV2Relation.computeStats` 
(`DataSourceV2Relation.scala:86-103`) throws when `Utils.isTesting`, precisely 
to catch this:
   
   ```
   org.apache.spark.SparkException: [INTERNAL_ERROR] BUG: computeStats called 
before
   pushdown on DSv2 relation: testcat.rightTbl SQLSTATE: XX000
   ```
   
   Reproduced on this head — the same query passes with the flag off:
   
   ```scala
   sql("CREATE TABLE testcat.rightTbl (rid INT, y DOUBLE) USING foo")
   sql("INSERT INTO testcat.rightTbl VALUES (10, 1.0), (11, 2.0), (12, 3.0)")
   Seq((1, 2.5)).toDF("id", "x").nearestByJoin(
     spark.table("testcat.rightTbl"), abs($"x" - $"y"),
     numResults = 2, mode = "exact", direction = "distance").collect()
   // flag on  -> INTERNAL_ERROR above
   // flag off -> [1,2.5,12,3.0], [1,2.5,11,2.0]
   ```
   
   Outside testing it takes the `else` branch, which builds a throwaway scan 
(`table.asReadable.newScanBuilder(options).build()`) inside the optimizer's 
first batch and returns full-table/all-columns stats — the comment there says 
"bad stats are better than failing a query". So on DSv2 this is either a hard 
failure or a decision made on a number the file itself calls bad, plus 
per-query scan-build cost.
   
   **2. Partitioned file tables never fire.** Their pre-pushdown `sizeInBytes` 
is `defaultSizeInBytes` (`Long.MaxValue`), so the predicate is false at any 
threshold:
   
   ```
   right = spark.table("rightPart").filter($"p" === 0)   // partitioned parquet
   analyzed  sizeInBytes = 9223372036854775807
   optimized sizeInBytes = 9398
   BroadcastNearestByJoin fired = false
   ```
   
   A 9 KB right side takes the cross-product path. (Non-partitioned v1 tables 
and in-memory DataFrames are unaffected, which is why the suite is green.)
   
   **Fix.** Broadcast sizing belongs where `JoinSelection` does it — the 
planner. Keep this predicate as-is for the strategy and make the rewrite gate 
stats-free:
   
   ```scala
   // RewriteNearestByJoin
   case j @ NearestByJoin(...) if !SQLConf.get.nearestByBroadcastEnabled =>
   ```
   
   That leaves the planner obligated to handle every `NearestByJoin` when the 
flag is on, which is the "no strategy handles it" hazard your code comment 
calls out. Two ways to discharge it:
   
   - **(a)** Drop the size test from `NearestByJoinSelection` too, so the flag 
means "always use the operator". For an `.internal()`, default-off flag that's 
a reasonable contract, and it makes the two call sites agree by construction. 
It gives up the automatic large-right fallback.
   - **(b)** Keep the size test in the strategy and, when it fails, plan the 
rewrite from there — `planLater(RewriteNearestByJoin.rewriteOne(j))` after 
factoring the rewrite body out of the rule. This preserves today's semantics 
with valid stats; the cost is that the rewritten subtree misses the 
operator-optimization batches. This is essentially the two-pass approach you 
deferred.
   
   Either way, please add a DSv2 right-side test with the flag on — the repro 
above is about 10 lines with `InMemoryTableCatalog`, and it's the shape most 
users on Iceberg/Delta will hit first.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/SparkStrategies.scala:
##########
@@ -178,6 +178,21 @@ abstract class SparkStrategies extends 
QueryPlanner[SparkPlan] {
    *     Supports both equi-joins and non-equi-joins.
    *     Supports only inner like joins.
    */
+
+  object NearestByJoinSelection extends Strategy {

Review Comment:
   **Finding 4.** The new strategy was inserted between the strategy-selection 
scaladoc block (ending `*/` at line 180) and `JoinSelection`, so that block — 
which documents equi-join/non-equi-join support, broadcast hash join build-side 
preferences, and "Shuffle-and-replicate nested loop join ... Supports only 
inner like joins" — now attaches to `NearestByJoinSelection`, which does none 
of those things. `JoinSelection` is left undocumented.
   
   Moving the object below `JoinSelection` restores both (and keeps the 
join-strategy doc adjacent to the code it describes):
   
   ```scala
     object JoinSelection extends Strategy with JoinSelectionHelper {
       ...
     }
   
     object NearestByJoinSelection extends Strategy {
       ...
     }
   ```
   
   Placing it above the scaladoc block would work equally well.
   



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