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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -7452,12 +7452,18 @@ object SQLConf {
         "single-column null-aware anti join for which Spark uses the broadcast 
hash join " +
         "optimization. This configuration takes effect only when " +
         "spark.sql.optimizeNullAwareAntiJoin is enabled. A negative value 
allows the " +
-        "optimization regardless of the estimated size, while zero disables 
it. If the " +
-        "estimated size exceeds a positive value, Spark falls back to regular 
join planning. " +
+        "optimization regardless of the estimated size. For a nonnegative 
value, the " +
+        "optimization is also allowed when regular join planning considers the 
right side " +
+        "broadcastable. " +
+        "Regular planning uses spark.sql.adaptive.autoBroadcastJoinThreshold 
for runtime " +

Review Comment:
   **MEDIUM**
   
   The adaptive sentence holds for join selection but cannot apply to the other 
consumer. `Statistics(isRuntime = true)` originates only in 
`QueryStageExec.computeStats` and reaches logical plans through 
`LogicalQueryStage`, while `PushDownLeftSemiAntiJoin` is registered only in the 
main optimizer's `operatorOptimizationRuleSet` 
(`catalyst/optimizer/Optimizer.scala:109`) and in no `AQEOptimizer` batch. 
Every plan that rule sees therefore reports estimated statistics and always 
uses the static threshold. Read together with the last sentence, which ties the 
same decision to the aggregate pushdown, the doc suggests 
`spark.sql.adaptive.autoBroadcastJoinThreshold` can widen that pushdown. It 
cannot; scoping the sentence to join selection would say what the code does.
   
   Separately, dropping "If the estimated size exceeds the effective threshold, 
Spark falls back to regular join planning" left the next sentence's "The 
fallback" with no antecedent in the paragraph.



##########
sql/core/src/test/scala/org/apache/spark/sql/JoinSuite.scala:
##########
@@ -1308,7 +1308,7 @@ class JoinSuite extends SharedSparkSession with 
AdaptiveSparkPlanHelper
     }
   }
 
-  test("SPARK-36082: left-broadcast NAAJ fallback uses nested-loop join") {
+  test("SPARK-36082: automatic threshold enables NAAJ hash join despite left 
broadcast hint") {

Review Comment:
   **MEDIUM**
   
   The hint in this query is now unobservable. `JoinSelection` matches the NAAJ 
shape before any hint handling and hardcodes the build side 
(`SparkStrategies.scala:343`), never reading `j.hint`, so deleting `/*+ 
BROADCAST(naajHintedLeft) */` leaves all four assertions passing. The positive 
fact this case now records is already covered at unit level by the `auto = 
10MB` plus dedicated `0` block in `JoinSelectionHelperSuite`.
   
   What the old version pinned was a differential: the same relations and query 
giving `BuildLeft` because of the hint, with the no-hint case below it as the 
control giving `BuildRight`. Nothing now covers a broadcast hint naming the 
left side while the NAAJ hash join is rejected. This was the only `BROADCAST` 
hint on a NAAJ query in the repo, and `JoinSelectionHelperSuite`'s NAAJ fixture 
hardcodes `JoinHint.NONE`. The path stays reachable, for instance `auto = 0` 
with dedicated `0`. One more `withSQLConf` block here would restore it, and 
would also be the only case asserting rows through a `BuildLeft` anti 
nested-loop join, since the case above expects an empty answer.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/joins.scala:
##########
@@ -438,12 +438,26 @@ trait JoinSelectionHelper extends Logging {
       getBroadcastBuildSide(join, hintOnly = true, conf).orElse {
         if (noShufflePlannedBefore) getBroadcastBuildSide(join, hintOnly = 
false, conf) else None
       }
-    // `JoinSelection` always builds from the right for this shape. A negative 
threshold preserves
-    // the original unbounded NAAJ behavior, while zero disables the broadcast 
hash optimization.
+    // `JoinSelection` always builds from the right for this shape. Do not 
reject the hash
+    // optimization when regular join planning would broadcast the right side, 
as the fallback
+    // would still broadcast it with a slower nested-loop join.
     case j @ ExtractSingleColumnNullAwareAntiJoin(_, _) =>
-      val threshold = conf.nullAwareAntiJoinBroadcastThreshold
-      val rightSize = j.right.stats.sizeInBytes
-      if (threshold < 0 || (threshold > 0 && rightSize >= 0 && rightSize <= 
threshold)) {
+      val dedicatedThreshold = conf.nullAwareAntiJoinBroadcastThreshold
+      val canBroadcast = if (dedicatedThreshold < 0) {
+        true
+      } else {
+        val automaticBroadcastDisabled = conf.autoBroadcastJoinThreshold <= 0 
&&

Review Comment:
   **LOW**
   
   `automaticBroadcastDisabled` compares with `<= 0` while `canBroadcastBySize` 
admits `size >= 0 && size <= threshold`, so at threshold `0` with an estimated 
size of `0` the two disagree: this short-circuit returns false where the 
disjunct below it would have returned true.
   
   Reachable with `spark.sql.autoBroadcastJoinThreshold=0`, the adaptive 
threshold unset, dedicated `0`, and `SELECT * FROM t WHERE c NOT IN (SELECT id 
FROM range(0))`. `Range.computeStats` reports `sizeInBytes = 0` for zero 
elements, and `PropagateEmptyRelation` does not remove the join, its `isEmpty` 
matching only an empty `LocalRelation`.
   
   The consequence is negligible, since both plans are cheap over an empty 
right side, but it contradicts this branch's own rationale and the new doc 
sentence about regular planning considering the right side broadcastable. The 
same corner lets the adaptive threshold change the decision for a plan with no 
runtime statistics. Comparing `< 0` in both halves removes it, and the 
short-circuit case uses -1 and -2, so it stays green.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/JoinSelectionHelperSuite.scala:
##########
@@ -195,48 +200,125 @@ class JoinSelectionHelperSuite extends PlanTest with 
JoinSelectionHelper {
     }
   }
 
-  test("getBroadcastHashJoinBuildSide uses the null-aware anti join broadcast 
threshold") {
-    val leftKey = left.output.head
-    val rightKey = right.output.head
-    val condition = Or(EqualTo(leftKey, rightKey), IsNull(EqualTo(leftKey, 
rightKey)))
-    val nullAwareAntiJoin = Join(left, right, LeftAnti, Some(condition), 
JoinHint.NONE)
+  test("NAAJ broadcast threshold is floored by the automatic broadcast 
threshold") {
+    val autoThresholdRight = right.copy(
+      rowCount = 10 * 1024 * 1024,
+      size = Some(10 * 1024 * 1024))
+    val betweenThresholdsRight = right.copy(
+      rowCount = 8 * 1024 * 1024,
+      size = Some(8 * 1024 * 1024))
     val largeRight = right.copy(rowCount = 20000000, size = Some(20000000))
-    val negativeSizeRight = right.copy(size = Some(-1))
+
+    withSQLConf(
+      SQLConf.OPTIMIZE_NULL_AWARE_ANTI_JOIN.key -> "true",
+      SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "10MB",
+      SQLConf.NULL_AWARE_ANTI_JOIN_BROADCAST_THRESHOLD.key -> "0") {
+      assert(getBroadcastHashJoinBuildSide(nullAwareAntiJoin(), SQLConf.get) 
=== Some(BuildRight))
+      assert(getBroadcastHashJoinBuildSide(
+        nullAwareAntiJoin(autoThresholdRight), SQLConf.get) === 
Some(BuildRight))
+      assert(getBroadcastHashJoinBuildSide(
+        nullAwareAntiJoin(largeRight), SQLConf.get).isEmpty)
+    }
+
+    withSQLConf(
+      SQLConf.OPTIMIZE_NULL_AWARE_ANTI_JOIN.key -> "true",
+      SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "10MB",
+      SQLConf.NULL_AWARE_ANTI_JOIN_BROADCAST_THRESHOLD.key -> "20MB") {
+      assert(getBroadcastHashJoinBuildSide(
+        nullAwareAntiJoin(largeRight), SQLConf.get) === Some(BuildRight))
+    }
+
+    withSQLConf(
+      SQLConf.OPTIMIZE_NULL_AWARE_ANTI_JOIN.key -> "true",
+      SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "10MB",
+      SQLConf.NULL_AWARE_ANTI_JOIN_BROADCAST_THRESHOLD.key -> "5MB") {
+      assert(getBroadcastHashJoinBuildSide(
+        nullAwareAntiJoin(betweenThresholdsRight), SQLConf.get) === 
Some(BuildRight))
+    }
+
+    withSQLConf(
+      SQLConf.OPTIMIZE_NULL_AWARE_ANTI_JOIN.key -> "true",
+      SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+      SQLConf.NULL_AWARE_ANTI_JOIN_BROADCAST_THRESHOLD.key -> "0") {
+      assert(getBroadcastHashJoinBuildSide(nullAwareAntiJoin(), 
SQLConf.get).isEmpty)
+    }
+  }
+
+  test("NAAJ broadcast threshold is unlimited by default") {
     val overLongMaxRight = right.copy(
       rowCount = BigInt(Long.MaxValue) + 1,
       size = Some(BigInt(Long.MaxValue) + 1))
 
     withSQLConf(
       SQLConf.OPTIMIZE_NULL_AWARE_ANTI_JOIN.key -> "true",
-      SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "10MB") {
-      assert(getBroadcastHashJoinBuildSide(nullAwareAntiJoin, SQLConf.get) === 
Some(BuildRight))
-      assert(getBroadcastHashJoinBuildSide(
-        nullAwareAntiJoin.copy(right = largeRight), SQLConf.get) === 
Some(BuildRight))
+      SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1") {
       assert(getBroadcastHashJoinBuildSide(
-        nullAwareAntiJoin.copy(right = overLongMaxRight), SQLConf.get) === 
Some(BuildRight))
+        nullAwareAntiJoin(overLongMaxRight), SQLConf.get) === Some(BuildRight))
+    }
+  }
+
+  test("NAAJ broadcast threshold uses the adaptive threshold for runtime 
statistics") {
+    case class RuntimeStatsPlan(size: BigInt) extends LeafNode {
+      override def output: Seq[Attribute] = right.output
+      override def computeStats(): Statistics = Statistics(sizeInBytes = size, 
isRuntime = true)
     }
+    val runtimeRight = RuntimeStatsPlan(5 * 1024 * 1024)
 
-    withSQLConf(SQLConf.NULL_AWARE_ANTI_JOIN_BROADCAST_THRESHOLD.key -> "-2") {
+    withSQLConf(
+      SQLConf.OPTIMIZE_NULL_AWARE_ANTI_JOIN.key -> "true",
+      SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "10MB",
+      SQLConf.ADAPTIVE_AUTO_BROADCASTJOIN_THRESHOLD.key -> "1MB",

Review Comment:
   **MEDIUM**
   
   Both blocks of this case use a runtime-statistics right side, and these two 
lines are the only places in the suite that set 
`spark.sql.adaptive.autoBroadcastJoinThreshold`, each alongside a positive 
static threshold. Two mutations survive that.
   
   Drop the `plan.stats.isRuntime` test in `canBroadcastBySize`, so the 
adaptive threshold applies whenever it is set, and both blocks still pass (5MB 
> 1MB, then 5MB <= 10MB). A block with `auto = 10MB`, `adaptive = 1MB`, 
dedicated `0` and an ordinary non-runtime 5MB right side, asserting 
`Some(BuildRight)`, would catch it.
   
   Delete `&& conf.getConf(ADAPTIVE_AUTO_BROADCASTJOIN_THRESHOLD).forall(_ <= 
0)` from `automaticBroadcastDisabled` and the whole suite stays green: every 
block that reaches that gate either has a positive static threshold or leaves 
the adaptive one unset. A block with `auto = -1`, `adaptive = 10MB`, dedicated 
`0` and the runtime 5MB right side, asserting `Some(BuildRight)`, would pin the 
new conjunct.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/LeftSemiAntiJoinPushDownSuite.scala:
##########
@@ -142,6 +142,33 @@ class LeftSemiAntiJoinPushDownSuite extends PlanTest {
     comparePlans(optimized, originalQuery.analyze)
   }
 
+  test("Aggregate: NAAJ pushdown follows the effective broadcast threshold") {

Review Comment:
   **LOW**
   
   `testRelation` and `testRelation1` are empty `LocalRelation`s, so 
`computeStats` gives `sizeInBytes = getSizePerRow(output) * data.length`, which 
is 0 (`LocalRelation.scala:107`). `0 <= 10MB` then holds trivially, and the 
first block pins only that automatic broadcasting is enabled, not any size 
comparison: an implementation that admits whenever the automatic threshold is 
positive, ignoring the size, passes it. The second block is sound, and the 
analyzed plan really is a fixpoint of this suite's batch once the pushdown is 
refused.
   
   A third block with a `StatsTestPlan` right side of 20MB, `auto = 10MB` and 
dedicated `0`, expecting no pushdown, would pin the size dimension too.



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