@@ -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
134137impl 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