jayzhan211 opened a new pull request, #24579:
URL: https://github.com/apache/datafusion/pull/24579

   ## Rationale for this change
   
   `PiecewiseMergeJoin` (PWMJ) needs to know which buffered (left) rows were 
matched, so that
   `Left` and `Full` joins can emit the unmatched ones in a final pass. Today 
it tracks this
   with a bitmap of one bit per buffered row, guarded by a mutex.
   
   That bitmap is more machinery than the operator needs. PWMJ's whole trick is 
that both
   sides are sorted, so when a streamed row finds its first match at buffered 
index `k`,
   **every** buffered row from `k` to the end matches — and the operator emits 
that entire
   range at once. Marking is therefore never scattered: it is always the suffix
   `[k, buffered_len)`.
   
   A union of suffixes that all end at the last row is just another suffix:
   
   ```
   ⋃ᵢ [kᵢ, len)  =  [min kᵢ, len)
   ```
   
   So the entire bitmap only ever encodes a single number — the smallest `k` 
seen. Keeping a
   bit per row costs, for every matched streamed row, a mutex acquisition plus 
one `set_bit`
   per emitted row, and the final pass then has to walk the bitmap to build an 
index array and
   run `take` over it.
   
   The existence side of this operator (`LeftSemi`/`LeftAnti`) already exploits 
exactly this
   property — it stores one `AtomicUsize` watermark and no bitmap. In fact the 
old
   `build_visited_indices_map` doc comment spelled the property out, but only 
for the existence
   path. This PR carries the same observation across to the classic joins.
   
   On a `LEFT JOIN` with a 1M-row buffered side and a 4k-row streamed side 
(release build,
   `datafusion-cli`):
   
   | | before | after |
   |---|---|---|
   | run 1 | 4.789 s | 2.662 s |
   | run 2 | 4.819 s | 2.645 s |
   
   **~1.8x faster.**
   
   ## What changes are included in this PR?
   
   - `BufferedSideData` drops `visited_indices_bitmap`; its existing 
existence-join field
     `existence_min_marked` is generalized to `min_marked` and is now 
maintained by both
     streams. The bitmap allocation, its memory reservation, the `Mutex`, and 
the
     `BooleanBufferBuilder` / `SharedBitmapBuilder` / `bit_util` imports all go 
away.
   - **Marking** (`classic_join.rs`): a mutex lock plus one `set_bit` per 
emitted row becomes a
     single `fetch_min`. Because `buffer_idx` only moves forward within a 
stream batch, a
     `batch_min_marked` field in `BatchProcessState` means only the batch's 
*first* match
     touches the shared atomic at all.
   - **Final pass**: `get_final_indices_from_shared_bitmap` + 
`take_record_batch` becomes
     `buffered_batch.slice(0, min_marked)`. The unmatched buffered rows are 
exactly the
     complementary prefix, so the final pass is now zero-copy.
   - **Dead code**: `build_visited_indices_map()` is removed — every arm other 
than
     `Full`/`Left` named a join type that `try_new` rejects. A small unused 
row-count
     accumulator in `build_buffered_data`'s `try_fold` goes too.
   
   Net: 136 insertions, 113 deletions across 5 files.
   
   ### Why this is safe
   
   The suffix property is *syntactic*, not a consequence of the merge 
algorithm. The bitmap had
   exactly one writer, and both of its call sites passed the same range:
   
   ```rust
   let count = buffered_len - buffer_idx;   // always runs to the end of the 
array
   … ((buffer_idx, count), (row_idx, count), …)
   ```
   
   So every write was `[buffer_idx, buffered_len)` by construction, and the 
union of those is
   `[min buffer_idx, buffered_len)` — regardless of sortedness, operator 
direction, NULL
   placement, or how partitions interleave. The old and new encodings are 
therefore
   *unconditionally* equal, not equal-under-an-assumption.
   
   Output ordering is unchanged as well: the old reader returned ascending 
indices whose bit was
   false, and the complement of `[min, len)` is `[0, min)`, also ascending.
   
   ## Are these changes tested?
   
   Yes.
   
   The two existing PWMJ fuzz entry points are merged into one differential 
test against a
   `NestedLoopJoin` oracle, `fuzz_pwmj_matches_nested_loop`, now covering **all 
six** supported
   join types (`Inner`/`Left`/`Right`/`Full`/`LeftSemi`/`LeftAnti`) rather than 
the existence
   pair alone — 60 seeds x 4 operators, with NULL keys, duplicate keys, 1–3 
streamed partitions
   executed concurrently, and `batch_size = 3`. The previous existence-only 
test and its
   collector are folded in, so this adds coverage while shrinking the file's 
duplication.
   
   **Why an added test was warranted**, stated precisely: `pwmj.slt` runs 
`partitions=1`
   throughout, and every classic unit test builds the exec with a 
single-partition streamed side
   at the default batch size. Neither reaches several partitions racing to run 
the final pass,
   nor the mid-scan resume path. I confirmed this by mutation:
   
   | mutation | 23 unit tests | `pwmj.slt` | this test |
   |---|---|---|---|
   | `fetch_min(k)` → `fetch_min(k + 1)` | fails ✅ | fails ✅ | fails ✅ |
   | drop the `min(num_rows)` clamp (`min_marked` is `usize::MAX` when nothing 
matched) | **passes** ❌ | **passes** ❌ | fails ✅ |
   
   The second row is the gap this closes — the existing suite already catches 
coarse breakage,
   but not the multi-partition / never-matched cases the new encoding 
introduces.
   
   Also verified unchanged: the 23 PWMJ unit tests, `pwmj.slt`, the full 
`joins::` suite, and
   clippy `-D warnings`. As a further cross-check, old and new release binaries 
produce
   byte-identical results for inner / left / right / full / semi / anti / `<=` 
over a 20k x 3k
   join with NULLs on both sides at `target_partitions = 4`.
   
   ## Are there any user-facing changes?
   
   No. This is an internal change to how matched buffered rows are recorded — 
query results,
   output ordering, and public APIs are unchanged. `PiecewiseMergeJoin` uses 
slightly less
   memory, since the per-buffered-row bitmap is no longer allocated.
   


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