Summary
conditional_join uses a flat matches array as a survivor mask while applying multiple predicates over candidate ranges. This avoids materializing all (left, right) pairs, but the mask can still be as large as the sum of all candidate range widths and is rescanned/reallocated across conditions.
Relevant implementation:
janitor/functions/_conditional_join/_helpers.py: _get_positive_matches and build_indices_matches
- Rust comparison kernels receiving
matches and counts_array
Problem
For a left row with candidate range [start, end), the current representation reserves one mask entry for every candidate, including candidates eliminated by earlier predicates. With broad ranges and several conditions, this can create substantial memory traffic even when the final result is selective.
The same intermediate state is also unnecessary for keep="first" and keep="last", where only one surviving right position per left row is required.
Possible directions
Investigate and benchmark the following alternatives:
- Fused predicate evaluation in Rust: evaluate all conditions in one candidate scan without an intermediate
matches tape.
- Direct first/last selection: scan candidates in the requested direction and stop at the first complete match.
- Compact survivor representation: use CSR-style row offsets plus surviving right positions after selective predicates.
- Bit-packed masks: replace the
int8 mask with a uint64 bitset if the tape remains necessary.
- Interval/segment survivors for predicates whose matches remain contiguous under sorted right-side data.
Suggested scope
Start with a benchmark and a fused/direct-selection implementation for keep="first" and keep="last"; retain the current path as a fallback for keep="all", irregular predicates, and aggregation paths until equivalent behavior is established.
Acceptance criteria
- Preserve output ordering and null semantics, including nullable dtypes and
!=.
- Preserve
join_agg, include_join_positions, and return_building_blocks behavior.
- Compare peak memory and runtime against the current implementation for broad ranges, selective predicates, multiple conditions, and high-cardinality inputs.
- Add regression tests covering empty ranges, duplicate values, nulls, sorted/unsorted right inputs, and all keep modes.
- Document when the existing
matches representation remains the preferred path.
Summary
conditional_joinuses a flatmatchesarray as a survivor mask while applying multiple predicates over candidate ranges. This avoids materializing all(left, right)pairs, but the mask can still be as large as the sum of all candidate range widths and is rescanned/reallocated across conditions.Relevant implementation:
janitor/functions/_conditional_join/_helpers.py:_get_positive_matchesandbuild_indices_matchesmatchesandcounts_arrayProblem
For a left row with candidate range
[start, end), the current representation reserves one mask entry for every candidate, including candidates eliminated by earlier predicates. With broad ranges and several conditions, this can create substantial memory traffic even when the final result is selective.The same intermediate state is also unnecessary for
keep="first"andkeep="last", where only one surviving right position per left row is required.Possible directions
Investigate and benchmark the following alternatives:
matchestape.int8mask with auint64bitset if the tape remains necessary.Suggested scope
Start with a benchmark and a fused/direct-selection implementation for
keep="first"andkeep="last"; retain the current path as a fallback forkeep="all", irregular predicates, and aggregation paths until equivalent behavior is established.Acceptance criteria
!=.join_agg,include_join_positions, andreturn_building_blocksbehavior.matchesrepresentation remains the preferred path.