spark-test commented on code in PR #59916:
URL: https://github.com/apache/spark/pull/59916#discussion_r4416577000


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/InjectRuntimeFilter.scala:
##########
@@ -415,12 +424,15 @@ object InjectRuntimeFilter extends Rule[LogicalPlan] with 
PredicateHelper with J
       case join @ ExtractEquiJoinKeys(joinType, leftKeys, rightKeys, _, _, 
left, right, hint) =>
         var newLeft = left
         var newRight = right
+        // The side the join is likely to broadcast, picked as `JoinSelection` 
picks it
+        val broadcastSide = getBroadcastBuildSide(join, hintOnly = true, conf)
+          .orElse(getBroadcastBuildSide(join, hintOnly = false, conf))

Review Comment:
   **Nit (P3):** This fallback is what keeps counting an `onlyInBroadcast` DPP 
filter that reuses an automatic (unhinted) broadcast, which is the default way 
such a filter finds its broadcast. No test in this change reaches it. Every new 
`InjectRuntimeFilterSuite` case runs with `AUTO_BROADCASTJOIN_THRESHOLD=2000`, 
where neither the filtered `bf2` (3409 bytes) nor the `bf5part` side fits, and 
forces each broadcast with a hint. `StorageFilterPushDownSuite` runs with 
broadcasts disabled. So removing this `.orElse(...)`, or moving it ahead of the 
hint-only call, changes no assertion.
   
   Without it, a key whose DPP filter reuses an automatic broadcast would also 
get a redundant bloom subquery. With the calls swapped, sizes would override a 
broadcast hint when the other side also fits the threshold. Results would not 
change either way, but the default path is left unpinned.
   
   See **Shared repair plan 1** in the review body.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/InjectRuntimeFilter.scala:
##########
@@ -415,12 +424,15 @@ object InjectRuntimeFilter extends Rule[LogicalPlan] with 
PredicateHelper with J
       case join @ ExtractEquiJoinKeys(joinType, leftKeys, rightKeys, _, _, 
left, right, hint) =>
         var newLeft = left
         var newRight = right
+        // The side the join is likely to broadcast, picked as `JoinSelection` 
picks it

Review Comment:
   **Nit (P3):** This says the side is picked as `JoinSelection` picks it, but 
the code only chains `getBroadcastBuildSide(join, hintOnly = true, conf)` and 
the size-based `getBroadcastBuildSide(join, hintOnly = false, conf)`. After a 
broadcast hint, `JoinSelection` tries a `SHUFFLE_MERGE` hint, a `SHUFFLE_HASH` 
hint and `SHUFFLE_REPLICATE_NL` before it falls back to a size-based broadcast 
(`SparkStrategies.scala:339-347`). It also never plans a broadcast hash join on 
keys that `hashJoinSupported` rejects. The size-based call honors only the 
`NO_BROADCAST_HASH` / `NO_BROADCAST_AND_REPLICATION` hints.
   
   For example, take `select /*+ merge(bf2) */ * from (select f5, sum(a5) from 
bf5part group by f5) t join bf2 on t.f5 = bf2.c2 where bf2.a2 = 62`, with the 
filtered `bf2` under the broadcast threshold and `t` above it. Here 
`broadcastSide` is `Some(BuildRight)`, so the `onlyInBroadcast` DPP filter 
counts and `t.f5` gets no bloom filter. Physical planning picks a sort merge 
join, finds no broadcast of `bf2` to reuse, and replaces the DPP filter with 
`true`. The key ends up with no runtime filter, which is the outcome this PR 
fixes for unhinted joins. Results are unchanged and this matches the merge 
base, but the comment hides the gap.
   
   The repo already models this precedence. `getBroadcastHashJoinBuildSide` 
vetoes the size-based broadcast under a `SHUFFLE_HASH` hint that 
`JoinSelection` would honor, and 
`PushDownJoinThroughUnion.broadcastsRightForEveryBranch` adds the 
`SHUFFLE_MERGE` / `SHUFFLE_REPLICATE_NL` veto. Reusing that logic here would 
make the comment true. Otherwise the comment should say which hints are not 
modeled.
   
   See **Shared repair plan 1** in the review body.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/InjectRuntimeFilter.scala:
##########
@@ -380,18 +380,27 @@ object InjectRuntimeFilter extends Rule[LogicalPlan] with 
PredicateHelper with J
   }
 
   // This checks if there is already a DPP filter, as this rule is called just 
after DPP.
+  // `broadcastSide` is the side the join is likely to broadcast. A DPP filter 
that runs only by
+  // reusing a broadcast counts only if physical planning is likely to find 
one, which needs both:
+  // - exchange reuse is on;
+  // - `broadcastSide` is the filter's build side, or the filter has a 
broadcast value projection.
   @tailrec
   private def hasDynamicPruningSubquery(
       left: LogicalPlan,
       right: LogicalPlan,
       leftKey: Expression,
-      rightKey: Expression): Boolean = {
+      rightKey: Expression,
+      broadcastSide: Option[BuildSide]): Boolean = {
+    def isKept(dpp: DynamicPruningSubquery, buildSide: BuildSide): Boolean =
+      !dpp.onlyInBroadcast || (conf.exchangeReuseEnabled &&
+        (broadcastSide.contains(buildSide) || 
dpp.usableBroadcastValueProjection.isDefined))
     (left, right) match {
-      case (Filter(DynamicPruningSubquery(pruningKey, _, _, _, _, _, _), 
plan), _) =>
-        pruningKey.fastEquals(leftKey) || hasDynamicPruningSubquery(plan, 
right, leftKey, rightKey)
-      case (_, Filter(DynamicPruningSubquery(pruningKey, _, _, _, _, _, _), 
plan)) =>
-        pruningKey.fastEquals(rightKey) ||
-          hasDynamicPruningSubquery(left, plan, leftKey, rightKey)
+      case (Filter(dpp: DynamicPruningSubquery, plan), _) =>
+        (dpp.pruningKey.fastEquals(leftKey) && isKept(dpp, BuildRight)) ||
+          hasDynamicPruningSubquery(plan, right, leftKey, rightKey, 
broadcastSide)
+      case (_, Filter(dpp: DynamicPruningSubquery, plan)) =>
+        (dpp.pruningKey.fastEquals(rightKey) && isKept(dpp, BuildLeft)) ||

Review Comment:
   **Nit (P3):** This arm maps a DPP filter on the right child to a broadcast 
of the left side, the only side of this join whose broadcast the filter can 
reuse. The shape is common: TPC-DS q42 lists `date_dim` before `store_sales`, 
so its DPP filter on `ss_sold_date_sk` sits on the right child and reuses a 
`BuildLeft` broadcast of `date_dim`. But no test in this change reaches the 
arm. Every new and modified DPP case in `InjectRuntimeFilterSuite` puts 
`bf5part`, or the aggregate over it, on the join's left side, and 
`StorageFilterPushDownSuite` keeps the probe on the left. Changing this to 
`isKept(dpp, BuildRight)`, or dropping the `isKept` check, changes no assertion 
in these suites. A broken mapping would give right-child keys a redundant bloom 
subquery, or no runtime filter at all. Results would not change.
   
   **Verification:**
   - **Behavior:** With the partitioned table on the right side of a shuffle 
join and a selective filter on the left side, the right-side key gets the 
runtime bloom filter, and its DPP filter is planned as true. The coverage fails 
if the right-child arm counts the filter unconditionally.
   - **Regression:** With the left side broadcast and reused by the right-child 
DPP filter, and a shuffle below the pruned right side so that the same query 
without DPP gets the bloom filter, no bloom filter is added. The coverage fails 
if the right-child arm checks the wrong build side.



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