Skip to content

Commit a7fa061

Browse files
committed
perf: bulk-append contiguous buffered runs in sort merge join
The materializing sort-merge-join stream advanced one output row at a time in join_partial. When a streamed row matches a run of buffered rows (high fan-out), each matched row cost a VecDeque index lookup in scanning_idx plus two more in scanning_advance, along with two individual builder appends. The matched buffered rows within a batch form a contiguous index range, so append the whole run at once via append_output_range (append_value_n + append_slice) and advance the scan cursor by the run length. A len == 1 fast path keeps the common one-row-per-key case free of scratch-buffer overhead. Benchmarks (sort_merge_join): inner join with 1:10 fan-out improves ~23% (13.28ms -> 10.18ms); 1:1 cases are neutral.
1 parent 1e58928 commit a7fa061

1 file changed

Lines changed: 88 additions & 8 deletions

File tree

datafusion/physical-plan/src/joins/sort_merge_join/materializing_stream.rs

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ pub(super) struct StreamedBatch {
129129
pub num_output_rows: usize,
130130
/// Index of currently scanned batch from buffered data
131131
pub buffered_batch_idx: Option<usize>,
132+
/// Reusable scratch buffer for materializing contiguous buffered index
133+
/// ranges in [`Self::append_output_range`]
134+
buffered_index_scratch: Vec<u64>,
132135
}
133136

134137
impl StreamedBatch {
@@ -141,6 +144,7 @@ impl StreamedBatch {
141144
output_indices: vec![],
142145
num_output_rows: 0,
143146
buffered_batch_idx: None,
147+
buffered_index_scratch: vec![],
144148
}
145149
}
146150

@@ -152,6 +156,7 @@ impl StreamedBatch {
152156
output_indices: vec![],
153157
num_output_rows: 0,
154158
buffered_batch_idx: None,
159+
buffered_index_scratch: vec![],
155160
}
156161
}
157162

@@ -199,6 +204,61 @@ impl StreamedBatch {
199204
}
200205
self.num_output_rows += 1;
201206
}
207+
208+
/// Appends `len` output pairs joining the current streamed index against the
209+
/// contiguous buffered index range `buffered_start..buffered_start + len`.
210+
///
211+
/// Equivalent to calling [`Self::append_output_pair`] once per index in the
212+
/// range, but appends into the builders in bulk.
213+
fn append_output_range(
214+
&mut self,
215+
buffered_batch_idx: Option<usize>,
216+
buffered_start: usize,
217+
len: usize,
218+
batch_size: usize,
219+
) {
220+
if len == 0 {
221+
return;
222+
}
223+
224+
if self.output_indices.is_empty() || self.buffered_batch_idx != buffered_batch_idx
225+
{
226+
debug_assert!(
227+
batch_size > self.num_output_rows,
228+
"batch_size ({batch_size}) must be > num_output_rows ({})",
229+
self.num_output_rows
230+
);
231+
let capacity = batch_size - self.num_output_rows;
232+
self.output_indices.push(StreamedJoinedChunk {
233+
buffered_batch_idx,
234+
streamed_indices: UInt64Builder::with_capacity(capacity),
235+
buffered_indices: UInt64Builder::with_capacity(capacity),
236+
});
237+
self.buffered_batch_idx = buffered_batch_idx;
238+
};
239+
240+
let current_chunk = self.output_indices.last_mut().unwrap();
241+
if len == 1 {
242+
// Fast path avoids touching the scratch buffer for the common
243+
// one-row-per-key case.
244+
current_chunk.streamed_indices.append_value(self.idx as u64);
245+
current_chunk
246+
.buffered_indices
247+
.append_value(buffered_start as u64);
248+
} else {
249+
self.buffered_index_scratch.clear();
250+
self.buffered_index_scratch
251+
.extend((buffered_start..buffered_start + len).map(|idx| idx as u64));
252+
current_chunk
253+
.streamed_indices
254+
.append_value_n(self.idx as u64, len);
255+
current_chunk
256+
.buffered_indices
257+
.append_slice(&self.buffered_index_scratch);
258+
}
259+
260+
self.num_output_rows += len;
261+
}
202262
}
203263

204264
/// Per-row filter outcome tracking for full outer joins.
@@ -1344,22 +1404,32 @@ impl MaterializingSortMergeJoinStream {
13441404
while !self.buffered_data.scanning_finished()
13451405
&& self.num_unfrozen_pairs() < self.batch_size
13461406
{
1407+
// The buffered rows remaining in the batch being scanned form a
1408+
// contiguous index range, so append them in one go rather than
1409+
// one row at a time, capped by the remaining output batch space.
1410+
let remaining_capacity = self.batch_size - self.num_unfrozen_pairs();
1411+
let run_len = self
1412+
.buffered_data
1413+
.scanning_batch_remaining()
1414+
.min(remaining_capacity);
13471415
let scanning_idx = self.buffered_data.scanning_idx();
1416+
13481417
if join_streamed {
1349-
// Join streamed row and buffered row
1350-
self.streamed_batch.append_output_pair(
1418+
// Join streamed row and buffered rows
1419+
self.streamed_batch.append_output_range(
13511420
Some(self.buffered_data.scanning_batch_idx),
1352-
Some(scanning_idx),
1421+
scanning_idx,
1422+
run_len,
13531423
self.batch_size,
13541424
);
13551425
} else {
1356-
// Join nulls and buffered row for FULL join
1426+
// Join nulls and buffered rows for FULL join
13571427
self.buffered_data
13581428
.scanning_batch_mut()
13591429
.null_joined
1360-
.push(scanning_idx);
1430+
.extend(scanning_idx..scanning_idx + run_len);
13611431
}
1362-
self.buffered_data.scanning_advance();
1432+
self.buffered_data.scanning_advance_by(run_len);
13631433

13641434
if self.buffered_data.scanning_finished() {
13651435
self.streamed_joined = join_streamed;
@@ -1996,14 +2066,24 @@ impl BufferedData {
19962066
self.scanning_offset = 0;
19972067
}
19982068

1999-
pub fn scanning_advance(&mut self) {
2000-
self.scanning_offset += 1;
2069+
/// Advances the scanning position by `n` rows.
2070+
///
2071+
/// `n` must not exceed [`Self::scanning_batch_remaining`], so that the
2072+
/// position never runs past the end of the batch being scanned.
2073+
pub fn scanning_advance_by(&mut self, n: usize) {
2074+
debug_assert!(n <= self.scanning_batch_remaining());
2075+
self.scanning_offset += n;
20012076
while !self.scanning_finished() && self.scanning_batch_finished() {
20022077
self.scanning_batch_idx += 1;
20032078
self.scanning_offset = 0;
20042079
}
20052080
}
20062081

2082+
/// Number of unscanned rows remaining in the batch currently being scanned
2083+
pub fn scanning_batch_remaining(&self) -> usize {
2084+
self.scanning_batch().range.len() - self.scanning_offset
2085+
}
2086+
20072087
pub fn scanning_batch(&self) -> &BufferedBatch {
20082088
&self.batches[self.scanning_batch_idx]
20092089
}

0 commit comments

Comments
 (0)