yashmayya opened a new pull request, #18848:
URL: https://github.com/apache/pinot/pull/18848

   ## What
   
   Adds **probe-side runtime filters for equi-`INNER` joins** in the 
multi-stage query engine (MSE). When
   the build (right) side of a hash join is small or selective, the planner 
builds a reducer from its
   distinct join keys and pushes it down to the probe (left) **leaf scan**, so 
the probe table drops rows
   that cannot possibly match *before* they are shuffled across the network 
into the join.
   
   This is the `INNER`-join counterpart of the existing `SEMI`-join dynamic 
broadcast
   (`PinotJoinToDynamicBroadcastRule`). It is **disabled by default**.
   
   ## Why
   
   For the classic fact ⋈ dim shape — a large fact table joined to a small 
dimension table (or a heavily
   filtered build side) — the MSE today hash-shuffles the **entire** probe 
(fact) side into the join stage,
   even though only the rows whose join key appears on the (tiny) build side 
can contribute to the result.
   That wastes scan, serialization, and network bandwidth proportional to the 
*whole* fact table rather than
   to the matching subset.
   
   The `SEMI`-join path already solves the analogous problem by replacing the 
join with a leaf `IN` filter,
   but that rewrite is only legal for semi-joins (which emit left columns 
only). An inner join projects
   columns from both sides, so the join must still run. This PR therefore makes 
the filter **additive**:
   the real hash join is left completely intact, and we only *add* a reducer on 
the probe leaf.
   
   ## How it works
   
   After exchange insertion (`POST_LOGICAL`), 
`PinotJoinToInnerRuntimeFilterRule` rewrites an eligible
   inner join:
   
   ```
           [ Inner Join ]   (unchanged — still hash-shuffles both sides)
           /            \
      [xChange L]    [xChange R]
          /                \
    [RuntimeFilter]    [build subtree]
       /        \
   [probe leaf] [PIPELINE_BREAKER xChange]
                      |
               [ build keys: Project(rightKeys) -> Filter(IS NOT NULL) -> 
limit(maxBuildRows + 1) ]
   ```
   
   - The join and **both** of its HASH exchanges are kept verbatim — execution 
and results are identical to
     before; the filter is purely additive.
   - A new `RuntimeFilterRel` / `RuntimeFilterNode` is grafted on top of the 
probe leaf subtree
     (`input[0]` = probe pipeline, pass-through; `input[1]` = a 
`PIPELINE_BREAKER` mailbox carrying the
     distinct build-side join keys). The pipeline breaker runs the build side 
first and ships its keys to
     the probe-leaf worker, reusing the same mechanism as the SEMI dynamic 
broadcast.
   - At the probe leaf, `ServerPlanRequestUtils.attachRuntimeFilter` ANDs a 
**tiered, no-false-negative**
     reducer onto the V1 leaf query:
     - **Exact `IN`** for small key sets (≤ a distinct-value threshold), or for 
multi-key / `BIG_DECIMAL`
       keys. This is index-accelerated and drives segment pruning.
     - **Bloom filter** (`IN_ID_SET`) above the threshold, plus a `BETWEEN(min, 
max)` range predicate for
       numeric keys to enable cheap range-based segment pruning. Bloom keeps 
the wire/heap footprint bounded
       for high-cardinality build sides.
   - Because the real hash join is the source of truth, the reducer can be 
**abandoned at any point**
     (empty/over-cap build, oversized bloom, unsupported subtree, mixed-version 
cluster) with no effect on
     results. Bloom false positives are simply re-checked and discarded by the 
join.
   
   This mirrors runtime/dynamic filtering in Trino, Impala, and Spark's 
`InjectRuntimeFilter`.
   
   ## When it helps
   
   - Large fact table joined to a **small or selectively-filtered** 
dimension/build side.
   - The probe side is a leaf scan (table scan, optionally with single-input 
`Project`/`Filter`), so the
     filter can be pushed all the way down to segment scan.
   
   It is **not** beneficial (and is best left off) when the build side is large 
or non-selective, or the
   probe is cheap — there is no cost-based gate yet, so enablement is opt-in 
(see below).
   
   ## How to use
   
   Per-join hint (selects the reducer mode):
   
   ```sql
   SELECT /*+ joinOptions(runtime_filter='auto') */ ...
   FROM fact JOIN dim ON fact.key = dim.key
   WHERE dim.attr = 'x'
   ```
   
   `runtime_filter` accepts `off` | `in` | `bloom` | `auto` (exact `IN` below 
the threshold, else bloom).
   
   Cluster-wide default (enable/disable only; defaults to `auto` when on):
   
   ```
   pinot.broker.enable.runtime.filter.join=true
   ```
   
   Per-query override: `SET runtimeFilterJoin='on'` (or `off`).
   
   ## Correctness & safety
   
   - **No false negatives.** Exact `IN` is exact; a bloom never reports 
present-as-absent and its false
     positives are discarded by the real join; the `BETWEEN(min, max)` bounds 
cover every build key.
   - **Null keys** are excluded (they cannot match an inner equi-join) both at 
the planner (`IS NOT NULL`)
     and defensively at the leaf.
   - **`NaN`** float/double build keys keep the bloom membership but skip the 
range predicate (a finite
     range would wrongly drop probe `NaN` rows).
   - **Truncation-safe.** The build-key stage is capped at `maxBuildRows + 1`; 
if the cap is hit the key set
     is incomplete, so the filter is abandoned (the planner cap and the leaf 
abandon use the same constant).
   - **Mixed-version.** The only wire change is the new `RuntimeFilterNode` 
proto variant; the default-off
     flag is the guard. Enabling the flag (or using the hint) 
mid-rolling-upgrade can fail queries on
     not-yet-upgraded servers — documented on the config constant.
   - The feature is engine-agnostic: it lives entirely in the planner + Java 
leaf, so both the default and
     DataFusion worker runtimes honor it with no native changes.
   
   ## Testing
   
   - `PinotJoinToInnerRuntimeFilterRuleTest` — rule firing/plan shape, 
probe-key/build-key value alignment,
     multi-key, hint/flag/query-option gating, pipeline-breaker distribution, 
negative cases.
   - `ServerPlanRequestUtilsTest` — exact-IN, bloom + range-prune, AUTO 
tiering, multi-key, empty/all-null
     build, null-key skip, `NaN` range omission, `maxBytes`/`maxBuildRows` 
abandon, existing-filter merge.
   - `PlanNodeDeserializerTest` — mixed-version graceful failure on the new 
proto variant.
   - `RuntimeFilterJoinIntegrationTest` — end-to-end cluster self-joins 
asserting results are **identical**
     with the filter on (in/bloom/auto) and off, across 
INT/LONG/DOUBLE/STRING/mixed-type/null/multi-key/
     empty-build cases.
   - Full `pinot-query-planner` (1321) and `pinot-query-runtime` (4431) suites 
pass — no regressions.
   
   ## Limitations / future work
   
   - No cost-based auto-enablement yet (opt-in via hint/flag); a future change 
can gate it on cardinality/
     selectivity stats.
   - The build side is materialized a second time for the key broadcast; a 
shared spool would avoid this.
   - Bloom is single-key (composite-key tuple-encoding deferred); partitioned 
(both-sides-hash) joins use
     the broadcast path. Only the logical (HEP) planner is wired; the 
cost-based physical optimizer is a
     follow-up.
   


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