1use std::cmp::Ordering;
22use std::collections::BTreeMap;
23use std::ops::Bound;
24use std::time::Duration;
25
26use either::Either;
27use itertools::Itertools;
28use multimap::MultiMap;
29use risingwave_common::array::Op;
30use risingwave_common::metrics::LabelGuardedHistogram;
31use risingwave_common::row::RowExt;
32use risingwave_common::util::epoch::EpochPair;
33use risingwave_common::util::sort_util::cmp_rows_ascending;
34use tokio::time::Instant;
35
36use super::barrier_align::*;
37use super::join::asof_join::*;
38use super::join::builder::JoinStreamChunkBuilder;
39use super::join::row::JoinRow;
40use super::join::*;
41use super::watermark::*;
42use crate::executor::join::builder::JoinChunkBuilder;
43use crate::executor::prelude::*;
44
45pub struct JoinParams {
46 pub join_key_indices: Vec<usize>,
48 pub deduped_pk_indices: Vec<usize>,
50}
51
52impl JoinParams {
53 pub fn new(join_key_indices: Vec<usize>, deduped_pk_indices: Vec<usize>) -> Self {
54 Self {
55 join_key_indices,
56 deduped_pk_indices,
57 }
58 }
59}
60
61struct JoinSide<S: StateStore, E: AsOfRowEncoding> {
62 ht: AsOfJoinHashMap<S, E>,
64 join_key_indices: Vec<usize>,
66 all_data_types: Vec<DataType>,
68 i2o_mapping: Vec<(usize, usize)>,
70 i2o_mapping_indexed: MultiMap<usize, usize>,
71 inequal_key_idx: usize,
73}
74
75impl<S: StateStore, E: AsOfRowEncoding> std::fmt::Debug for JoinSide<S, E> {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("JoinSide")
78 .field("join_key_indices", &self.join_key_indices)
79 .field("col_types", &self.all_data_types)
80 .field("i2o_mapping", &self.i2o_mapping)
81 .finish()
82 }
83}
84
85impl<S: StateStore, E: AsOfRowEncoding> JoinSide<S, E> {
86 pub async fn init(&mut self, epoch: EpochPair) -> StreamExecutorResult<()> {
87 self.ht.init(epoch).await
88 }
89}
90
91pub struct AsOfJoinExecutor<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> {
97 ctx: ActorContextRef,
98 info: ExecutorInfo,
99
100 input_l: Option<Executor>,
102 input_r: Option<Executor>,
104 actual_output_data_types: Vec<DataType>,
106 side_l: JoinSide<S, E>,
108 side_r: JoinSide<S, E>,
110
111 null_safe: Vec<bool>,
113
114 metrics: Arc<StreamingMetrics>,
115 chunk_size: usize,
117 watermark_buffers: BTreeMap<usize, BufferedWatermarks<SideTypePrimitive>>,
119 asof_desc: AsOfDesc,
121 cnt_rows_received: u32,
123 join_cache_evict_interval_rows: u32,
125 high_join_amplification_threshold: usize,
127}
128
129impl<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> std::fmt::Debug
130 for AsOfJoinExecutor<S, T, E>
131{
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 f.debug_struct("AsOfJoinExecutor")
134 .field("join_type", &T)
135 .field("input_left", &self.input_l.as_ref().unwrap().identity())
136 .field("input_right", &self.input_r.as_ref().unwrap().identity())
137 .field("side_l", &self.side_l)
138 .field("side_r", &self.side_r)
139 .field("stream_key", &self.info.stream_key)
140 .field("schema", &self.info.schema)
141 .field("actual_output_data_types", &self.actual_output_data_types)
142 .finish()
143 }
144}
145
146impl<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> Execute
147 for AsOfJoinExecutor<S, T, E>
148{
149 fn execute(self: Box<Self>) -> BoxedMessageStream {
150 self.into_stream().boxed()
151 }
152}
153
154struct EqJoinArgs<'a, S: StateStore, E: AsOfRowEncoding> {
155 ctx: &'a ActorContextRef,
156 side_l: &'a mut JoinSide<S, E>,
157 side_r: &'a mut JoinSide<S, E>,
158 null_safe: &'a [bool],
159 asof_desc: &'a AsOfDesc,
160 actual_output_data_types: &'a [DataType],
161 chunk: StreamChunk,
162 chunk_size: usize,
163 cnt_rows_received: &'a mut u32,
164 join_cache_evict_interval_rows: u32,
165 high_join_amplification_threshold: usize,
166 join_matched_join_keys: &'a LabelGuardedHistogram,
168}
169
170impl<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> AsOfJoinExecutor<S, T, E> {
171 #[expect(clippy::too_many_arguments)]
172 pub fn new(
173 ctx: ActorContextRef,
174 info: ExecutorInfo,
175 input_l: Executor,
176 input_r: Executor,
177 params_l: JoinParams,
178 params_r: JoinParams,
179 null_safe: Vec<bool>,
180 output_indices: Vec<usize>,
181 state_table_l: StateTable<S>,
182 state_table_r: StateTable<S>,
183 watermark_epoch: AtomicU64Ref,
184 metrics: Arc<StreamingMetrics>,
185 chunk_size: usize,
186 asof_desc: AsOfDesc,
187 use_cache: bool,
188 high_join_amplification_threshold: usize,
189 ) -> Self {
190 let join_cache_evict_interval_rows = ctx
191 .config
192 .developer
193 .join_hash_map_evict_interval_rows
194 .max(1);
195 let cache_epoch = if use_cache {
196 Some(watermark_epoch)
197 } else {
198 None
199 };
200 let schema_fields = [
201 input_l.schema().fields.clone(),
202 input_r.schema().fields.clone(),
203 ]
204 .concat();
205
206 let original_output_data_types = schema_fields
207 .iter()
208 .map(|field| field.data_type())
209 .collect_vec();
210 let actual_output_data_types = output_indices
211 .iter()
212 .map(|&idx| original_output_data_types[idx].clone())
213 .collect_vec();
214
215 let state_all_data_types_l = input_l.schema().data_types();
216 let state_all_data_types_r = input_r.schema().data_types();
217
218 let state_join_key_indices_l = params_l.join_key_indices;
219 let state_join_key_indices_r = params_r.join_key_indices;
220
221 let join_key_data_types_l = state_join_key_indices_l
222 .iter()
223 .map(|idx| state_all_data_types_l[*idx].clone())
224 .collect_vec();
225
226 let join_key_data_types_r = state_join_key_indices_r
227 .iter()
228 .map(|idx| state_all_data_types_r[*idx].clone())
229 .collect_vec();
230
231 assert_eq!(join_key_data_types_l, join_key_data_types_r);
232
233 let (left_to_output, right_to_output) = {
234 let (left_len, right_len) = if is_left_semi_or_anti(T) {
235 (state_all_data_types_l.len(), 0usize)
236 } else if is_right_semi_or_anti(T) {
237 (0usize, state_all_data_types_r.len())
238 } else {
239 (state_all_data_types_l.len(), state_all_data_types_r.len())
240 };
241 JoinStreamChunkBuilder::get_i2o_mapping(&output_indices, left_len, right_len)
242 };
243
244 let l2o_indexed = MultiMap::from_iter(left_to_output.iter().copied());
245 let r2o_indexed = MultiMap::from_iter(right_to_output.iter().copied());
246
247 let watermark_buffers = BTreeMap::new();
248
249 let inequal_key_idx_l = asof_desc.left_idx;
250 let inequal_key_idx_r = asof_desc.right_idx;
251
252 Self {
253 ctx: ctx.clone(),
254 info,
255 input_l: Some(input_l),
256 input_r: Some(input_r),
257 actual_output_data_types,
258 null_safe,
259 side_l: JoinSide {
260 ht: AsOfJoinHashMap::new(
261 state_join_key_indices_l.clone(),
262 state_table_l,
263 params_l.deduped_pk_indices,
264 inequal_key_idx_l,
265 state_all_data_types_l.clone(),
266 cache_epoch.clone(),
267 metrics.clone(),
268 ctx.id,
269 ctx.fragment_id,
270 "left",
271 ),
272 join_key_indices: state_join_key_indices_l,
273 all_data_types: state_all_data_types_l,
274 i2o_mapping: left_to_output,
275 i2o_mapping_indexed: l2o_indexed,
276 inequal_key_idx: inequal_key_idx_l,
277 },
278 side_r: JoinSide {
279 ht: AsOfJoinHashMap::new(
280 state_join_key_indices_r.clone(),
281 state_table_r,
282 params_r.deduped_pk_indices,
283 inequal_key_idx_r,
284 state_all_data_types_r.clone(),
285 cache_epoch,
286 metrics.clone(),
287 ctx.id,
288 ctx.fragment_id,
289 "right",
290 ),
291 join_key_indices: state_join_key_indices_r,
292 all_data_types: state_all_data_types_r,
293 i2o_mapping: right_to_output,
294 i2o_mapping_indexed: r2o_indexed,
295 inequal_key_idx: inequal_key_idx_r,
296 },
297 metrics,
298 chunk_size,
299 watermark_buffers,
300 asof_desc,
301 cnt_rows_received: 0,
302 join_cache_evict_interval_rows,
303 high_join_amplification_threshold,
304 }
305 }
306
307 fn evict_cache(
309 side_l: &mut JoinSide<S, E>,
310 side_r: &mut JoinSide<S, E>,
311 cnt_rows_received: &mut u32,
312 join_cache_evict_interval_rows: u32,
313 ) {
314 *cnt_rows_received += 1;
315 if *cnt_rows_received >= join_cache_evict_interval_rows {
316 side_l.ht.evict_cache();
317 side_r.ht.evict_cache();
318 *cnt_rows_received = 0;
319 }
320 }
321
322 #[try_stream(ok = Message, error = StreamExecutorError)]
323 async fn into_stream(mut self) {
324 let input_l = self.input_l.take().unwrap();
325 let input_r = self.input_r.take().unwrap();
326 let aligned_stream = barrier_align(
327 input_l.execute(),
328 input_r.execute(),
329 self.ctx.id,
330 self.ctx.fragment_id,
331 self.metrics.clone(),
332 "Join",
333 );
334 pin_mut!(aligned_stream);
335 let actor_id = self.ctx.id;
336
337 let barrier = expect_first_barrier_from_aligned_stream(&mut aligned_stream).await?;
338 let first_epoch = barrier.epoch;
339 yield Message::Barrier(barrier);
340 self.side_l.init(first_epoch).await?;
341 self.side_r.init(first_epoch).await?;
342
343 let actor_id_str = self.ctx.id.to_string();
344 let fragment_id_str = self.ctx.fragment_id.to_string();
345
346 let join_actor_input_waiting_duration_ns = self
347 .metrics
348 .join_actor_input_waiting_duration_ns
349 .with_guarded_label_values(&[&actor_id_str, &fragment_id_str]);
350 let left_join_match_duration_ns = self
351 .metrics
352 .join_match_duration_ns
353 .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "left"]);
354 let right_join_match_duration_ns = self
355 .metrics
356 .join_match_duration_ns
357 .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "right"]);
358
359 let barrier_join_match_duration_ns = self
360 .metrics
361 .join_match_duration_ns
362 .with_guarded_label_values(&[
363 actor_id_str.as_str(),
364 fragment_id_str.as_str(),
365 "barrier",
366 ]);
367
368 let left_join_cached_entry_count = self
369 .metrics
370 .join_cached_entry_count
371 .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "left"]);
372
373 let right_join_cached_entry_count = self
374 .metrics
375 .join_cached_entry_count
376 .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "right"]);
377
378 let left_table_id_str = self.side_l.ht.table_id().to_string();
380 let right_table_id_str = self.side_r.ht.table_id().to_string();
381 let left_join_matched_join_keys = self
382 .metrics
383 .join_matched_join_keys
384 .with_guarded_label_values(&[
385 actor_id_str.as_str(),
386 fragment_id_str.as_str(),
387 left_table_id_str.as_str(),
388 ]);
389 let right_join_matched_join_keys = self
390 .metrics
391 .join_matched_join_keys
392 .with_guarded_label_values(&[
393 actor_id_str.as_str(),
394 fragment_id_str.as_str(),
395 right_table_id_str.as_str(),
396 ]);
397
398 let mut start_time = Instant::now();
399
400 while let Some(msg) = aligned_stream
401 .next()
402 .instrument_await("hash_join_barrier_align")
403 .await
404 {
405 join_actor_input_waiting_duration_ns.inc_by(start_time.elapsed().as_nanos() as u64);
406 match msg? {
407 AlignedMessage::WatermarkLeft(watermark) => {
408 for watermark_to_emit in self.handle_watermark(SideType::Left, watermark)? {
409 yield Message::Watermark(watermark_to_emit);
410 }
411 }
412 AlignedMessage::WatermarkRight(watermark) => {
413 for watermark_to_emit in self.handle_watermark(SideType::Right, watermark)? {
414 yield Message::Watermark(watermark_to_emit);
415 }
416 }
417 AlignedMessage::Left(chunk) => {
418 let mut left_time = Duration::from_nanos(0);
419 let mut left_start_time = Instant::now();
420 #[for_await]
421 for chunk in Self::eq_join_left(EqJoinArgs {
422 ctx: &self.ctx,
423 side_l: &mut self.side_l,
424 side_r: &mut self.side_r,
425 null_safe: &self.null_safe,
426 asof_desc: &self.asof_desc,
427 actual_output_data_types: &self.actual_output_data_types,
428 chunk,
429 chunk_size: self.chunk_size,
430 cnt_rows_received: &mut self.cnt_rows_received,
431 join_cache_evict_interval_rows: self.join_cache_evict_interval_rows,
432 high_join_amplification_threshold: self.high_join_amplification_threshold,
433 join_matched_join_keys: &left_join_matched_join_keys,
434 }) {
435 left_time += left_start_time.elapsed();
436 yield Message::Chunk(chunk?);
437 left_start_time = Instant::now();
438 }
439 left_time += left_start_time.elapsed();
440 left_join_match_duration_ns.inc_by(left_time.as_nanos() as u64);
441 self.try_flush_data().await?;
442 }
443 AlignedMessage::Right(chunk) => {
444 let mut right_time = Duration::from_nanos(0);
445 let mut right_start_time = Instant::now();
446 #[for_await]
447 for chunk in Self::eq_join_right(EqJoinArgs {
448 ctx: &self.ctx,
449 side_l: &mut self.side_l,
450 side_r: &mut self.side_r,
451 null_safe: &self.null_safe,
452 asof_desc: &self.asof_desc,
453 actual_output_data_types: &self.actual_output_data_types,
454 chunk,
455 chunk_size: self.chunk_size,
456 cnt_rows_received: &mut self.cnt_rows_received,
457 join_cache_evict_interval_rows: self.join_cache_evict_interval_rows,
458 high_join_amplification_threshold: self.high_join_amplification_threshold,
459 join_matched_join_keys: &right_join_matched_join_keys,
460 }) {
461 right_time += right_start_time.elapsed();
462 yield Message::Chunk(chunk?);
463 right_start_time = Instant::now();
464 }
465 right_time += right_start_time.elapsed();
466 right_join_match_duration_ns.inc_by(right_time.as_nanos() as u64);
467 self.try_flush_data().await?;
468 }
469 AlignedMessage::Barrier(barrier) => {
470 let barrier_start_time = Instant::now();
471 let (left_post_commit, right_post_commit) =
472 self.flush_data(barrier.epoch).await?;
473
474 let update_vnode_bitmap = barrier.as_update_vnode_bitmap(actor_id);
475 yield Message::Barrier(barrier);
476
477 right_post_commit
479 .post_yield_barrier(update_vnode_bitmap.clone())
480 .await?;
481 if left_post_commit
482 .post_yield_barrier(update_vnode_bitmap)
483 .await?
484 .unwrap_or(false)
485 {
486 self.watermark_buffers
487 .values_mut()
488 .for_each(|buffers| buffers.clear());
489 }
490
491 for (join_cached_entry_count, ht) in [
493 (&left_join_cached_entry_count, &self.side_l.ht),
494 (&right_join_cached_entry_count, &self.side_r.ht),
495 ] {
496 join_cached_entry_count.set(ht.entry_count() as i64);
497 }
498
499 barrier_join_match_duration_ns
500 .inc_by(barrier_start_time.elapsed().as_nanos() as u64);
501 }
502 }
503 start_time = Instant::now();
504 }
505 }
506
507 async fn flush_data(
508 &mut self,
509 epoch: EpochPair,
510 ) -> StreamExecutorResult<(
511 AsOfJoinHashMapPostCommit<'_, S, E>,
512 AsOfJoinHashMapPostCommit<'_, S, E>,
513 )> {
514 let left = self.side_l.ht.flush(epoch).await?;
515 let right = self.side_r.ht.flush(epoch).await?;
516 Ok((left, right))
517 }
518
519 async fn try_flush_data(&mut self) -> StreamExecutorResult<()> {
520 self.side_l.ht.try_flush().await?;
521 self.side_r.ht.try_flush().await?;
522 Ok(())
523 }
524
525 fn handle_watermark(
526 &mut self,
527 side: SideTypePrimitive,
528 watermark: Watermark,
529 ) -> StreamExecutorResult<Vec<Watermark>> {
530 let (side_update, side_match) = if side == SideType::Left {
531 (&mut self.side_l, &mut self.side_r)
532 } else {
533 (&mut self.side_r, &mut self.side_l)
534 };
535
536 if side_update.join_key_indices[0] == watermark.col_idx {
538 side_match.ht.update_watermark(watermark.val.clone());
539 }
540
541 let wm_in_jk = side_update
543 .join_key_indices
544 .iter()
545 .positions(|idx| *idx == watermark.col_idx);
546 let mut watermarks_to_emit = vec![];
547 for idx in wm_in_jk {
548 let buffers = self
549 .watermark_buffers
550 .entry(idx)
551 .or_insert_with(|| BufferedWatermarks::with_ids([SideType::Left, SideType::Right]));
552 if let Some(selected_watermark) = buffers.handle_watermark(side, watermark.clone()) {
553 let empty_indices = vec![];
554 let output_indices = side_update
555 .i2o_mapping_indexed
556 .get_vec(&side_update.join_key_indices[idx])
557 .unwrap_or(&empty_indices)
558 .iter()
559 .chain(
560 side_match
561 .i2o_mapping_indexed
562 .get_vec(&side_match.join_key_indices[idx])
563 .unwrap_or(&empty_indices),
564 );
565 for output_idx in output_indices {
566 watermarks_to_emit.push(selected_watermark.clone().with_idx(*output_idx));
567 }
568 };
569 }
570 Ok(watermarks_to_emit)
571 }
572
573 #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
574 async fn eq_join_left(args: EqJoinArgs<'_, S, E>) {
575 let EqJoinArgs {
576 ctx,
577 side_l,
578 side_r,
579 null_safe,
580 asof_desc,
581 actual_output_data_types,
582 chunk,
583 chunk_size,
584 cnt_rows_received,
585 join_cache_evict_interval_rows,
586 high_join_amplification_threshold,
587 join_matched_join_keys,
588 } = args;
589
590 let (side_update, side_match) = (side_l, side_r);
591
592 let mut join_chunk_builder =
593 JoinChunkBuilder::<T, { SideType::Left }>::new(JoinStreamChunkBuilder::new(
594 chunk_size,
595 actual_output_data_types.to_vec(),
596 side_update.i2o_mapping.clone(),
597 side_match.i2o_mapping.clone(),
598 ));
599
600 let inequal_key_idx_update = [side_update.inequal_key_idx];
602
603 for r in chunk.rows_with_holes() {
604 let Some((op, row)) = r else {
605 continue;
606 };
607 Self::evict_cache(
608 side_update,
609 side_match,
610 cnt_rows_received,
611 join_cache_evict_interval_rows,
612 );
613
614 let join_key_null_not_safe = side_update
617 .join_key_indices
618 .iter()
619 .zip_eq(null_safe.iter())
620 .any(|(idx, ns)| !ns && row.datum_at(*idx).is_none());
621 if join_key_null_not_safe {
622 match op {
623 Op::Insert | Op::UpdateInsert => {
624 if let Some(chunk) =
625 join_chunk_builder.forward_if_not_matched(Op::Insert, row)
626 {
627 yield chunk;
628 }
629 }
630 Op::Delete | Op::UpdateDelete => {
631 if let Some(chunk) =
632 join_chunk_builder.forward_if_not_matched(Op::Delete, row)
633 {
634 yield chunk;
635 }
636 }
637 }
638 join_matched_join_keys.observe(0.0);
639 continue;
640 }
641
642 let join_key = row.project(&side_update.join_key_indices);
643
644 let inequal_key_is_null = side_update.ht.check_inequal_key_null(&row);
645 let inequal_key = row.project(&inequal_key_idx_update);
646
647 let mut join_matched_rows_cnt = 0;
648
649 if !inequal_key_is_null {
650 let matched_row_by_inequality = match asof_desc.inequality_type {
651 AsOfInequalityType::Lt => {
652 side_match
653 .ht
654 .lower_bound_by_inequality_with_jk_prefix(
655 &join_key,
656 Bound::Excluded(&inequal_key),
657 )
658 .await
659 }
660 AsOfInequalityType::Le => {
661 side_match
662 .ht
663 .lower_bound_by_inequality_with_jk_prefix(
664 &join_key,
665 Bound::Included(&inequal_key),
666 )
667 .await
668 }
669 AsOfInequalityType::Gt => {
670 side_match
671 .ht
672 .upper_bound_by_inequality_with_jk_prefix(
673 &join_key,
674 Bound::Excluded(&inequal_key),
675 )
676 .await
677 }
678 AsOfInequalityType::Ge => {
679 side_match
680 .ht
681 .upper_bound_by_inequality_with_jk_prefix(
682 &join_key,
683 Bound::Included(&inequal_key),
684 )
685 .await
686 }
687 }?
688 .map(|row| JoinRow::new(row, 0));
689 match op {
690 Op::Insert | Op::UpdateInsert => {
691 if let Some(matched_row) = matched_row_by_inequality {
692 join_matched_rows_cnt += 1;
693 if let Some(chunk) =
694 join_chunk_builder.with_match_on_insert(&row, &matched_row)
695 {
696 yield chunk;
697 }
698 } else if let Some(chunk) =
699 join_chunk_builder.forward_if_not_matched(Op::Insert, row)
700 {
701 yield chunk;
702 }
703 side_update.ht.insert(row)?;
704 }
705 Op::Delete | Op::UpdateDelete => {
706 if let Some(matched_row) = matched_row_by_inequality {
707 join_matched_rows_cnt += 1;
708 if let Some(chunk) =
709 join_chunk_builder.with_match_on_delete(&row, &matched_row)
710 {
711 yield chunk;
712 }
713 } else if let Some(chunk) =
714 join_chunk_builder.forward_if_not_matched(Op::Delete, row)
715 {
716 yield chunk;
717 }
718 side_update.ht.delete(row)?;
719 }
720 }
721 } else {
722 match op {
725 Op::Insert | Op::UpdateInsert => {
726 if let Some(chunk) =
727 join_chunk_builder.forward_if_not_matched(Op::Insert, row)
728 {
729 yield chunk;
730 }
731 }
732 Op::Delete | Op::UpdateDelete => {
733 if let Some(chunk) =
734 join_chunk_builder.forward_if_not_matched(Op::Delete, row)
735 {
736 yield chunk;
737 }
738 }
739 }
740 }
741 join_matched_join_keys.observe(join_matched_rows_cnt as _);
742 if join_matched_rows_cnt > high_join_amplification_threshold {
743 tracing::warn!(target: "high_join_amplification",
744 matched_rows_len = join_matched_rows_cnt,
745 update_table_id = %side_update.ht.table_id(),
746 match_table_id = %side_match.ht.table_id(),
747 join_key = ?join_key,
748 actor_id = %ctx.id,
749 fragment_id = %ctx.fragment_id,
750 "large rows matched for join key when AsOf join updating left side",
751 );
752 }
753 }
754 if let Some(chunk) = join_chunk_builder.take() {
755 yield chunk;
756 }
757 }
758
759 fn cmp_pk_rows(pk1: &impl Row, pk2: &impl Row) -> Ordering {
760 cmp_rows_ascending(pk1, pk2)
761 }
762
763 #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
764 async fn eq_join_right(args: EqJoinArgs<'_, S, E>) {
765 let EqJoinArgs {
766 ctx,
767 side_l,
768 side_r,
769 null_safe,
770 asof_desc,
771 actual_output_data_types,
772 chunk,
773 chunk_size,
774 cnt_rows_received,
775 join_cache_evict_interval_rows,
776 high_join_amplification_threshold,
777 join_matched_join_keys,
778 } = args;
779
780 let (side_update, side_match) = (side_r, side_l);
781
782 let mut join_chunk_builder = JoinStreamChunkBuilder::new(
783 chunk_size,
784 actual_output_data_types.to_vec(),
785 side_update.i2o_mapping.clone(),
786 side_match.i2o_mapping.clone(),
787 );
788
789 let inequal_key_idx_update = [side_update.inequal_key_idx];
791
792 for r in chunk.rows_with_holes() {
793 let Some((op, row)) = r else {
794 continue;
795 };
796 Self::evict_cache(
797 side_update,
798 side_match,
799 cnt_rows_received,
800 join_cache_evict_interval_rows,
801 );
802
803 let join_key_null_not_safe = side_update
806 .join_key_indices
807 .iter()
808 .zip_eq(null_safe.iter())
809 .any(|(idx, ns)| !ns && row.datum_at(*idx).is_none());
810 if join_key_null_not_safe {
811 join_matched_join_keys.observe(0.0);
812 continue;
813 }
814
815 let join_key = row.project(&side_update.join_key_indices);
816
817 let inequal_key_is_null = side_update.ht.check_inequal_key_null(&row);
818 let inequal_key = row.project(&inequal_key_idx_update);
819
820 let mut join_matched_rows_cnt = 0;
821
822 if !inequal_key_is_null {
823 let (row_to_delete_r, row_to_insert_r) = {
824 let (first_row, second_row) = side_update
825 .ht
826 .first_two_by_inequality_with_jk_prefix(&join_key, &inequal_key)
827 .await?;
828 if let Some(first_row) = first_row {
829 let row_pk = side_update.ht.get_pk_from_row(row);
830 let first_pk = side_update.ht.get_pk_from_row(&first_row).to_owned_row();
831 match op {
832 Op::Insert | Op::UpdateInsert => {
833 if Self::cmp_pk_rows(&first_pk, &row_pk) == Ordering::Greater {
835 (Some(Either::Left(first_row)), Some(Either::Right(row)))
836 } else {
837 (None, None)
839 }
840 }
841 Op::Delete | Op::UpdateDelete => {
842 if Self::cmp_pk_rows(&first_pk, &row_pk) == Ordering::Equal {
843 if let Some(second_row) = second_row {
844 (Some(Either::Right(row)), Some(Either::Left(second_row)))
845 } else {
846 (Some(Either::Right(row)), None)
847 }
848 } else {
849 (None, None)
851 }
852 }
853 }
854 } else {
855 match op {
856 Op::Insert | Op::UpdateInsert => (None, Some(Either::Right(row))),
858 Op::Delete | Op::UpdateDelete => (Some(Either::Right(row)), None),
860 }
861 }
862 };
863 if row_to_delete_r.is_none() && row_to_insert_r.is_none() {
869 } else {
871 let prev_inequality_key = side_update
872 .ht
873 .upper_bound_by_inequality_with_jk_prefix(
874 &join_key,
875 Bound::Excluded(&inequal_key),
876 )
877 .await?
878 .map(|r| r.project(&inequal_key_idx_update));
879 let next_inequality_key = side_update
880 .ht
881 .lower_bound_by_inequality_with_jk_prefix(
882 &join_key,
883 Bound::Excluded(&inequal_key),
884 )
885 .await?
886 .map(|r| r.project(&inequal_key_idx_update));
887
888 let affected_inequality_key_r = match asof_desc.inequality_type {
889 AsOfInequalityType::Lt | AsOfInequalityType::Le => &next_inequality_key,
890 AsOfInequalityType::Gt | AsOfInequalityType::Ge => &prev_inequality_key,
891 };
892 let affected_row_r =
893 if let Some(affected_inequality_key_r) = affected_inequality_key_r {
894 side_update
895 .ht
896 .first_by_inequality_with_jk_prefix(
897 &join_key,
898 &affected_inequality_key_r,
899 )
900 .await?
901 } else {
902 None
903 }
904 .map(Either::Left);
905
906 let (row_to_delete_r, row_to_insert_r) =
907 match (&row_to_delete_r, &row_to_insert_r) {
908 (Some(_), Some(_)) => (row_to_delete_r, row_to_insert_r),
909 (None, Some(_)) => (affected_row_r, row_to_insert_r),
910 (Some(_), None) => (row_to_delete_r, affected_row_r),
911 (None, None) => unreachable!(),
912 };
913 let range = match asof_desc.inequality_type {
914 AsOfInequalityType::Lt => (
915 prev_inequality_key
916 .map(Either::Left)
917 .map_or_else(|| Bound::Unbounded, Bound::Included),
918 Bound::Excluded(Either::Right(&inequal_key)),
919 ),
920 AsOfInequalityType::Le => (
921 prev_inequality_key
922 .map(Either::Left)
923 .map_or_else(|| Bound::Unbounded, Bound::Excluded),
924 Bound::Included(Either::Right(&inequal_key)),
925 ),
926 AsOfInequalityType::Gt => (
927 Bound::Excluded(Either::Right(&inequal_key)),
928 next_inequality_key
929 .map(Either::Left)
930 .map_or_else(|| Bound::Unbounded, Bound::Included),
931 ),
932 AsOfInequalityType::Ge => (
933 Bound::Included(Either::Right(&inequal_key)),
934 next_inequality_key
935 .map(Either::Left)
936 .map_or_else(|| Bound::Unbounded, Bound::Excluded),
937 ),
938 };
939
940 let rows_l_stream = side_match
941 .ht
942 .range_by_inequality_with_jk_prefix(&join_key, &range)
943 .await?;
944 #[for_await]
945 for row_l in rows_l_stream {
946 let row_l = row_l?;
947 join_matched_rows_cnt += 1;
948 if let Some(row_to_delete_r) = &row_to_delete_r {
949 if let Some(chunk) =
950 join_chunk_builder.append_row(Op::Delete, row_to_delete_r, &row_l)
951 {
952 yield chunk;
953 }
954 } else if is_as_of_left_outer(T)
955 && let Some(chunk) =
956 join_chunk_builder.append_row_matched(Op::Delete, &row_l)
957 {
958 yield chunk;
959 }
960 if let Some(row_to_insert_r) = &row_to_insert_r {
961 if let Some(chunk) =
962 join_chunk_builder.append_row(Op::Insert, row_to_insert_r, &row_l)
963 {
964 yield chunk;
965 }
966 } else if is_as_of_left_outer(T)
967 && let Some(chunk) =
968 join_chunk_builder.append_row_matched(Op::Insert, &row_l)
969 {
970 yield chunk;
971 }
972 }
973 }
974
975 match op {
976 Op::Insert | Op::UpdateInsert => {
977 side_update.ht.insert(row)?;
978 }
979 Op::Delete | Op::UpdateDelete => {
980 side_update.ht.delete(row)?;
981 }
982 }
983 } else {
984 }
987 join_matched_join_keys.observe(join_matched_rows_cnt as _);
988 if join_matched_rows_cnt > high_join_amplification_threshold {
989 let join_key = row.project(&side_update.join_key_indices);
990 tracing::warn!(target: "high_join_amplification",
991 matched_rows_len = join_matched_rows_cnt,
992 update_table_id = %side_update.ht.table_id(),
993 match_table_id = %side_match.ht.table_id(),
994 join_key = ?join_key,
995 actor_id = %ctx.id,
996 fragment_id = %ctx.fragment_id,
997 "large rows matched for join key when AsOf join updating right side",
998 );
999 }
1000 }
1001 if let Some(chunk) = join_chunk_builder.take() {
1002 yield chunk;
1003 }
1004 }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009 use std::sync::atomic::AtomicU64;
1010
1011 use prometheus::Registry;
1012 use risingwave_common::array::*;
1013 use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, TableId};
1014 use risingwave_common::config::MetricLevel;
1015 use risingwave_common::metrics::get_label;
1016 use risingwave_common::util::epoch::test_epoch;
1017 use risingwave_common::util::sort_util::OrderType;
1018 use risingwave_storage::memory::MemoryStateStore;
1019
1020 use super::*;
1021 use crate::common::table::test_utils::gen_pbtable;
1022 use crate::executor::test_utils::{MessageSender, MockSource, StreamExecutorTestExt};
1023
1024 async fn create_in_memory_state_table(
1025 mem_state: MemoryStateStore,
1026 data_types: &[DataType],
1027 order_types: &[OrderType],
1028 pk_indices: &[usize],
1029 table_id: u32,
1030 ) -> StateTable<MemoryStateStore> {
1031 let column_descs = data_types
1032 .iter()
1033 .enumerate()
1034 .map(|(id, data_type)| ColumnDesc::unnamed(ColumnId::new(id as i32), data_type.clone()))
1035 .collect_vec();
1036 StateTable::from_table_catalog(
1037 &gen_pbtable(
1038 TableId::new(table_id),
1039 column_descs,
1040 order_types.to_vec(),
1041 pk_indices.to_vec(),
1042 0,
1043 ),
1044 mem_state.clone(),
1045 None,
1046 )
1047 .await
1048 }
1049
1050 async fn create_executor<const T: AsOfJoinTypePrimitive>(
1051 asof_desc: AsOfDesc,
1052 use_cache: bool,
1053 ) -> (MessageSender, MessageSender, BoxedMessageStream) {
1054 create_executor_with_metrics::<T>(
1055 asof_desc,
1056 use_cache,
1057 Arc::new(StreamingMetrics::unused()),
1058 )
1059 .await
1060 }
1061
1062 async fn create_executor_with_metrics<const T: AsOfJoinTypePrimitive>(
1063 asof_desc: AsOfDesc,
1064 use_cache: bool,
1065 metrics: Arc<StreamingMetrics>,
1066 ) -> (MessageSender, MessageSender, BoxedMessageStream) {
1067 let schema = Schema {
1068 fields: vec![
1069 Field::unnamed(DataType::Int64), Field::unnamed(DataType::Int64),
1071 Field::unnamed(DataType::Int64),
1072 ],
1073 };
1074 let (tx_l, source_l) = MockSource::channel();
1075 let source_l = source_l.into_executor(schema.clone(), vec![1]);
1076 let (tx_r, source_r) = MockSource::channel();
1077 let source_r = source_r.into_executor(schema, vec![1]);
1078 let params_l = JoinParams::new(vec![0], vec![1]);
1079 let params_r = JoinParams::new(vec![0], vec![1]);
1080
1081 let mem_state = MemoryStateStore::new();
1082
1083 let state_l = create_in_memory_state_table(
1084 mem_state.clone(),
1085 &[DataType::Int64, DataType::Int64, DataType::Int64],
1086 &[
1087 OrderType::ascending(),
1088 OrderType::ascending(),
1089 OrderType::ascending(),
1090 ],
1091 &[0, asof_desc.left_idx, 1],
1092 0,
1093 )
1094 .await;
1095
1096 let state_r = create_in_memory_state_table(
1097 mem_state,
1098 &[DataType::Int64, DataType::Int64, DataType::Int64],
1099 &[
1100 OrderType::ascending(),
1101 OrderType::ascending(),
1102 OrderType::ascending(),
1103 ],
1104 &[0, asof_desc.right_idx, 1],
1105 1,
1106 )
1107 .await;
1108
1109 let schema: Schema = [source_l.schema().fields(), source_r.schema().fields()]
1110 .concat()
1111 .into_iter()
1112 .collect();
1113 let schema_len = schema.len();
1114 let info = ExecutorInfo::for_test(schema, vec![1], "AsOfJoinExecutor".to_owned(), 0);
1115
1116 let executor = AsOfJoinExecutor::<MemoryStateStore, T, AsOfCpuEncoding>::new(
1117 ActorContext::for_test(123),
1118 info,
1119 source_l,
1120 source_r,
1121 params_l,
1122 params_r,
1123 vec![false],
1124 (0..schema_len).collect_vec(),
1125 state_l,
1126 state_r,
1127 Arc::new(AtomicU64::new(0)),
1128 metrics,
1129 1024,
1130 asof_desc,
1131 use_cache,
1132 2048, );
1134 (tx_l, tx_r, executor.boxed().execute())
1135 }
1136
1137 fn join_matched_sample_count(registry: &Registry, table_id: &str) -> u64 {
1138 registry
1139 .gather()
1140 .iter()
1141 .find(|metric_family| metric_family.name() == "stream_join_matched_join_keys")
1142 .and_then(|metric_family| {
1143 metric_family.get_metric().iter().find(|metric| {
1144 get_label::<String>(metric, "table_id").as_deref() == Some(table_id)
1145 })
1146 })
1147 .map(|metric| metric.get_histogram().get_sample_count())
1148 .unwrap_or(0)
1149 }
1150
1151 #[tokio::test]
1152 async fn test_asof_join_records_null_equi_key_probe_metrics() -> StreamExecutorResult<()> {
1153 let asof_desc = AsOfDesc {
1154 left_idx: 1,
1155 right_idx: 1,
1156 inequality_type: AsOfInequalityType::Lt,
1157 };
1158 let registry = Registry::new();
1159 let metrics = Arc::new(StreamingMetrics::new(®istry, MetricLevel::Debug));
1160 let (mut tx_l, mut tx_r, mut hash_join) =
1161 create_executor_with_metrics::<{ AsOfJoinType::Inner }>(asof_desc, false, metrics)
1162 .await;
1163
1164 tx_l.push_barrier(test_epoch(1), false);
1165 tx_r.push_barrier(test_epoch(1), false);
1166 hash_join.next_unwrap_ready_barrier()?;
1167
1168 tx_l.push_chunk(StreamChunk::from_pretty(
1169 " I I I
1170 + . 10 1",
1171 ));
1172 hash_join.next_unwrap_pending();
1173
1174 tx_r.push_chunk(StreamChunk::from_pretty(
1175 " I I I
1176 + . 20 2",
1177 ));
1178 hash_join.next_unwrap_pending();
1179
1180 assert_eq!(join_matched_sample_count(®istry, "0"), 1);
1181 assert_eq!(join_matched_sample_count(®istry, "1"), 1);
1182
1183 Ok(())
1184 }
1185
1186 #[tokio::test]
1187 async fn test_asof_inner_join() -> StreamExecutorResult<()> {
1188 test_asof_inner_join_impl(false).await
1189 }
1190
1191 #[tokio::test]
1192 async fn test_asof_inner_join_with_cache() -> StreamExecutorResult<()> {
1193 test_asof_inner_join_impl(true).await
1194 }
1195
1196 async fn test_asof_inner_join_impl(use_cache: bool) -> StreamExecutorResult<()> {
1197 let asof_desc = AsOfDesc {
1198 left_idx: 0,
1199 right_idx: 2,
1200 inequality_type: AsOfInequalityType::Lt,
1201 };
1202
1203 let chunk_l1 = StreamChunk::from_pretty(
1204 " I I I
1205 + 1 4 7
1206 + 2 5 8
1207 + 3 6 9",
1208 );
1209 let chunk_l2 = StreamChunk::from_pretty(
1210 " I I I
1211 + 3 8 1
1212 - 3 8 1",
1213 );
1214 let chunk_r1 = StreamChunk::from_pretty(
1215 " I I I
1216 + 2 1 7
1217 + 2 2 1
1218 + 2 3 4
1219 + 2 4 2
1220 + 6 1 9
1221 + 6 2 9",
1222 );
1223 let chunk_r2 = StreamChunk::from_pretty(
1224 " I I I
1225 - 2 3 4",
1226 );
1227 let chunk_r3 = StreamChunk::from_pretty(
1228 " I I I
1229 + 2 3 3",
1230 );
1231 let chunk_l3 = StreamChunk::from_pretty(
1232 " I I I
1233 - 2 5 8",
1234 );
1235 let chunk_l4 = StreamChunk::from_pretty(
1236 " I I I
1237 + 6 3 1
1238 + 6 4 1",
1239 );
1240 let chunk_r4 = StreamChunk::from_pretty(
1241 " I I I
1242 - 6 1 9",
1243 );
1244
1245 let (mut tx_l, mut tx_r, mut hash_join) =
1246 create_executor::<{ AsOfJoinType::Inner }>(asof_desc, use_cache).await;
1247
1248 tx_l.push_barrier(test_epoch(1), false);
1250 tx_r.push_barrier(test_epoch(1), false);
1251 hash_join.next_unwrap_ready_barrier()?;
1252
1253 tx_l.push_chunk(chunk_l1);
1255 hash_join.next_unwrap_pending();
1256
1257 tx_l.push_barrier(test_epoch(2), false);
1259 tx_r.push_barrier(test_epoch(2), false);
1260 hash_join.next_unwrap_ready_barrier()?;
1261
1262 tx_l.push_chunk(chunk_l2);
1264 hash_join.next_unwrap_pending();
1265
1266 tx_r.push_chunk(chunk_r1);
1268 let chunk = hash_join.next_unwrap_ready_chunk()?;
1269 assert_eq!(
1270 chunk,
1271 StreamChunk::from_pretty(
1272 " I I I I I I
1273 + 2 5 8 2 1 7
1274 - 2 5 8 2 1 7
1275 + 2 5 8 2 3 4"
1276 )
1277 );
1278
1279 tx_r.push_chunk(chunk_r2);
1281 let chunk = hash_join.next_unwrap_ready_chunk()?;
1282 assert_eq!(
1283 chunk,
1284 StreamChunk::from_pretty(
1285 " I I I I I I
1286 - 2 5 8 2 3 4
1287 + 2 5 8 2 1 7"
1288 )
1289 );
1290
1291 tx_r.push_chunk(chunk_r3);
1293 let chunk = hash_join.next_unwrap_ready_chunk()?;
1294 assert_eq!(
1295 chunk,
1296 StreamChunk::from_pretty(
1297 " I I I I I I
1298 - 2 5 8 2 1 7
1299 + 2 5 8 2 3 3"
1300 )
1301 );
1302
1303 tx_l.push_chunk(chunk_l3);
1305 let chunk = hash_join.next_unwrap_ready_chunk()?;
1306 assert_eq!(
1307 chunk,
1308 StreamChunk::from_pretty(
1309 " I I I I I I
1310 - 2 5 8 2 3 3"
1311 )
1312 );
1313
1314 tx_l.push_chunk(chunk_l4);
1316 let chunk = hash_join.next_unwrap_ready_chunk()?;
1317 assert_eq!(
1318 chunk,
1319 StreamChunk::from_pretty(
1320 " I I I I I I
1321 + 6 3 1 6 1 9
1322 + 6 4 1 6 1 9"
1323 )
1324 );
1325
1326 tx_r.push_chunk(chunk_r4);
1328 let chunk = hash_join.next_unwrap_ready_chunk()?;
1329 assert_eq!(
1330 chunk,
1331 StreamChunk::from_pretty(
1332 " I I I I I I
1333 - 6 3 1 6 1 9
1334 + 6 3 1 6 2 9
1335 - 6 4 1 6 1 9
1336 + 6 4 1 6 2 9"
1337 )
1338 );
1339
1340 Ok(())
1341 }
1342
1343 #[tokio::test]
1344 async fn test_asof_left_outer_join() -> StreamExecutorResult<()> {
1345 test_asof_left_outer_join_impl(false).await
1346 }
1347
1348 #[tokio::test]
1349 async fn test_asof_left_outer_join_with_cache() -> StreamExecutorResult<()> {
1350 test_asof_left_outer_join_impl(true).await
1351 }
1352
1353 async fn test_asof_left_outer_join_impl(use_cache: bool) -> StreamExecutorResult<()> {
1354 let asof_desc = AsOfDesc {
1355 left_idx: 1,
1356 right_idx: 2,
1357 inequality_type: AsOfInequalityType::Ge,
1358 };
1359
1360 let chunk_l1 = StreamChunk::from_pretty(
1361 " I I I
1362 + 1 4 7
1363 + 2 5 8
1364 + 3 6 9",
1365 );
1366 let chunk_l2 = StreamChunk::from_pretty(
1367 " I I I
1368 + 3 8 1
1369 - 3 8 1",
1370 );
1371 let chunk_r1 = StreamChunk::from_pretty(
1372 " I I I
1373 + 2 3 4
1374 + 2 2 5
1375 + 2 1 5
1376 + 6 1 8
1377 + 6 2 9",
1378 );
1379 let chunk_r2 = StreamChunk::from_pretty(
1380 " I I I
1381 - 2 3 4
1382 - 2 1 5
1383 - 2 2 5",
1384 );
1385 let chunk_l3 = StreamChunk::from_pretty(
1386 " I I I
1387 + 6 8 9",
1388 );
1389 let chunk_r3 = StreamChunk::from_pretty(
1390 " I I I
1391 - 6 1 8",
1392 );
1393
1394 let (mut tx_l, mut tx_r, mut hash_join) =
1395 create_executor::<{ AsOfJoinType::LeftOuter }>(asof_desc, use_cache).await;
1396
1397 tx_l.push_barrier(test_epoch(1), false);
1399 tx_r.push_barrier(test_epoch(1), false);
1400 hash_join.next_unwrap_ready_barrier()?;
1401
1402 tx_l.push_chunk(chunk_l1);
1404 let chunk = hash_join.next_unwrap_ready_chunk()?;
1405 assert_eq!(
1406 chunk,
1407 StreamChunk::from_pretty(
1408 " I I I I I I
1409 + 1 4 7 . . .
1410 + 2 5 8 . . .
1411 + 3 6 9 . . ."
1412 )
1413 );
1414
1415 tx_l.push_barrier(test_epoch(2), false);
1417 tx_r.push_barrier(test_epoch(2), false);
1418 hash_join.next_unwrap_ready_barrier()?;
1419
1420 tx_l.push_chunk(chunk_l2);
1422 let chunk = hash_join.next_unwrap_ready_chunk()?;
1423 assert_eq!(
1424 chunk,
1425 StreamChunk::from_pretty(
1426 " I I I I I I
1427 + 3 8 1 . . . D
1428 - 3 8 1 . . . D"
1429 )
1430 );
1431
1432 tx_r.push_chunk(chunk_r1);
1434 let chunk = hash_join.next_unwrap_ready_chunk()?;
1435 assert_eq!(
1436 chunk,
1437 StreamChunk::from_pretty(
1438 " I I I I I I
1439 - 2 5 8 . . .
1440 + 2 5 8 2 3 4
1441 - 2 5 8 2 3 4
1442 + 2 5 8 2 2 5
1443 - 2 5 8 2 2 5
1444 + 2 5 8 2 1 5"
1445 )
1446 );
1447
1448 tx_r.push_chunk(chunk_r2);
1450 let chunk = hash_join.next_unwrap_ready_chunk()?;
1451 assert_eq!(
1452 chunk,
1453 StreamChunk::from_pretty(
1454 " I I I I I I
1455 - 2 5 8 2 1 5
1456 + 2 5 8 2 2 5
1457 - 2 5 8 2 2 5
1458 + 2 5 8 . . ."
1459 )
1460 );
1461
1462 tx_l.push_chunk(chunk_l3);
1464 let chunk = hash_join.next_unwrap_ready_chunk()?;
1465 assert_eq!(
1466 chunk,
1467 StreamChunk::from_pretty(
1468 " I I I I I I
1469 + 6 8 9 6 1 8"
1470 )
1471 );
1472
1473 tx_r.push_chunk(chunk_r3);
1475 let chunk = hash_join.next_unwrap_ready_chunk()?;
1476 assert_eq!(
1477 chunk,
1478 StreamChunk::from_pretty(
1479 " I I I I I I
1480 - 6 8 9 6 1 8
1481 + 6 8 9 . . ."
1482 )
1483 );
1484 Ok(())
1485 }
1486}