Skip to main content

risingwave_stream/executor/top_n/
group_top_n.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::ops::{Deref, DerefMut};
17
18use risingwave_common::array::Op;
19use risingwave_common::hash::HashKey;
20use risingwave_common::row::{RowDeserializer, RowExt};
21use risingwave_common::util::epoch::EpochPair;
22use risingwave_common::util::iter_util::ZipEqDebug;
23use risingwave_common::util::sort_util::{ColumnOrder, topn_watermark_forwardable_order_key};
24
25use super::top_n_cache::TopNCacheTrait;
26use super::utils::*;
27use super::{ManagedTopNState, TopNCache};
28use crate::cache::ManagedLruCache;
29use crate::common::metrics::MetricsInfo;
30use crate::common::table::state_table::StateTablePostCommit;
31use crate::executor::monitor::GroupTopNMetrics;
32use crate::executor::prelude::*;
33use crate::executor::top_n::top_n_cache::TopNStaging;
34
35pub type GroupTopNExecutor<K, S, const WITH_TIES: bool> =
36    TopNExecutorWrapper<InnerGroupTopNExecutor<K, S, WITH_TIES>>;
37
38impl<K: HashKey, S: StateStore, const WITH_TIES: bool> GroupTopNExecutor<K, S, WITH_TIES> {
39    #[expect(clippy::too_many_arguments)]
40    pub fn new(
41        input: Executor,
42        ctx: ActorContextRef,
43        schema: Schema,
44        storage_key: Vec<ColumnOrder>,
45        offset_and_limit: (usize, usize),
46        order_by: Vec<ColumnOrder>,
47        group_by: Vec<usize>,
48        state_table: StateTable<S>,
49        watermark_epoch: AtomicU64Ref,
50    ) -> StreamResult<Self> {
51        let inner = InnerGroupTopNExecutor::new(
52            schema,
53            storage_key,
54            offset_and_limit,
55            order_by,
56            group_by,
57            state_table,
58            watermark_epoch,
59            &ctx,
60        )?;
61        Ok(TopNExecutorWrapper { input, ctx, inner })
62    }
63}
64
65pub struct InnerGroupTopNExecutor<K: HashKey, S: StateStore, const WITH_TIES: bool> {
66    schema: Schema,
67
68    /// `LIMIT XXX`. None means no limit.
69    limit: usize,
70
71    /// `OFFSET XXX`. `0` means no offset.
72    offset: usize,
73
74    /// The storage key indices of the `GroupTopNExecutor`
75    storage_key_indices: Vec<usize>,
76
77    managed_state: ManagedTopNState<S>,
78
79    /// which column we used to group the data.
80    group_by: Vec<usize>,
81
82    /// The `ORDER BY` column whose watermarks can be forwarded, if any.
83    watermark_order_key: Option<usize>,
84
85    /// group key -> cache for this group
86    caches: GroupTopNCache<K, WITH_TIES>,
87
88    /// Used for serializing pk into `CacheKey`.
89    cache_key_serde: CacheKeySerde,
90
91    /// Minimum cache capacity per group from config
92    topn_cache_min_capacity: usize,
93
94    metrics: GroupTopNMetrics,
95}
96
97impl<K: HashKey, S: StateStore, const WITH_TIES: bool> InnerGroupTopNExecutor<K, S, WITH_TIES> {
98    #[expect(clippy::too_many_arguments)]
99    pub fn new(
100        schema: Schema,
101        storage_key: Vec<ColumnOrder>,
102        offset_and_limit: (usize, usize),
103        order_by: Vec<ColumnOrder>,
104        group_by: Vec<usize>,
105        state_table: StateTable<S>,
106        watermark_epoch: AtomicU64Ref,
107        ctx: &ActorContext,
108    ) -> StreamResult<Self> {
109        let metrics_info = MetricsInfo::new(
110            ctx.streaming_metrics.clone(),
111            state_table.table_id(),
112            ctx.id,
113            "GroupTopN",
114        );
115        let metrics = ctx.streaming_metrics.new_group_top_n_metrics(
116            state_table.table_id(),
117            ctx.id,
118            ctx.fragment_id,
119        );
120
121        let cache_key_serde = create_cache_key_serde(&storage_key, &schema, &order_by, &group_by);
122        let managed_state = ManagedTopNState::<S>::new(state_table, cache_key_serde.clone());
123
124        Ok(Self {
125            schema,
126            offset: offset_and_limit.0,
127            limit: offset_and_limit.1,
128            managed_state,
129            storage_key_indices: storage_key.into_iter().map(|op| op.column_index).collect(),
130            watermark_order_key: topn_watermark_forwardable_order_key(&order_by),
131            group_by,
132            caches: GroupTopNCache::new(watermark_epoch, metrics_info),
133            cache_key_serde,
134            topn_cache_min_capacity: ctx.config.developer.topn_cache_min_capacity,
135            metrics,
136        })
137    }
138}
139
140pub struct GroupTopNCache<K: HashKey, const WITH_TIES: bool> {
141    data: ManagedLruCache<K, TopNCache<WITH_TIES>>,
142}
143
144impl<K: HashKey, const WITH_TIES: bool> GroupTopNCache<K, WITH_TIES> {
145    pub fn new(watermark_sequence: AtomicU64Ref, metrics_info: MetricsInfo) -> Self {
146        let cache = ManagedLruCache::unbounded(watermark_sequence, metrics_info);
147        Self { data: cache }
148    }
149}
150
151impl<K: HashKey, const WITH_TIES: bool> Deref for GroupTopNCache<K, WITH_TIES> {
152    type Target = ManagedLruCache<K, TopNCache<WITH_TIES>>;
153
154    fn deref(&self) -> &Self::Target {
155        &self.data
156    }
157}
158
159impl<K: HashKey, const WITH_TIES: bool> DerefMut for GroupTopNCache<K, WITH_TIES> {
160    fn deref_mut(&mut self) -> &mut Self::Target {
161        &mut self.data
162    }
163}
164
165impl<K: HashKey, S: StateStore, const WITH_TIES: bool> TopNExecutorBase
166    for InnerGroupTopNExecutor<K, S, WITH_TIES>
167where
168    TopNCache<WITH_TIES>: TopNCacheTrait,
169{
170    type State = S;
171
172    async fn apply_chunk(
173        &mut self,
174        chunk: StreamChunk,
175    ) -> StreamExecutorResult<Option<StreamChunk>> {
176        let keys = K::build_many(&self.group_by, chunk.data_chunk());
177        let mut stagings: HashMap<K, TopNStaging> = HashMap::new();
178
179        for (r, group_cache_key) in chunk.rows_with_holes().zip_eq_debug(keys.iter()) {
180            let Some((op, row_ref)) = r else {
181                continue;
182            };
183
184            // The pk without group by
185            let pk_row = row_ref.project(&self.storage_key_indices[self.group_by.len()..]);
186            let cache_key = serialize_pk_to_cache_key(pk_row, &self.cache_key_serde);
187
188            let group_key = row_ref.project(&self.group_by);
189            self.metrics.group_top_n_total_query_cache_count.inc();
190            // If 'self.caches' does not already have a cache for the current group, create a new
191            // cache for it and insert it into `self.caches`
192            if !self.caches.contains(group_cache_key) {
193                self.metrics.group_top_n_cache_miss_count.inc();
194                let mut topn_cache = TopNCache::with_min_capacity(
195                    self.offset,
196                    self.limit,
197                    self.schema.data_types(),
198                    self.topn_cache_min_capacity,
199                );
200                self.managed_state
201                    .init_topn_cache(Some(group_key), &mut topn_cache)
202                    .await?;
203                self.caches.put(group_cache_key.clone(), topn_cache);
204            }
205
206            let mut cache = self.caches.get_mut(group_cache_key).unwrap();
207            let staging = stagings.entry(group_cache_key.clone()).or_default();
208
209            // apply the chunk to state table
210            match op {
211                Op::Insert | Op::UpdateInsert => {
212                    self.managed_state.insert(row_ref);
213                    cache.insert(cache_key, row_ref, staging);
214                }
215
216                Op::Delete | Op::UpdateDelete => {
217                    self.managed_state.delete(row_ref);
218                    cache
219                        .delete(
220                            Some(group_key),
221                            &mut self.managed_state,
222                            cache_key,
223                            row_ref,
224                            staging,
225                        )
226                        .await?;
227                }
228            }
229        }
230
231        self.metrics
232            .group_top_n_cached_entry_count
233            .set(self.caches.len() as i64);
234
235        let data_types = self.schema.data_types();
236        let deserializer = RowDeserializer::new(data_types.clone());
237        let mut chunk_builder = StreamChunkBuilder::unlimited(data_types, Some(chunk.capacity()));
238        for staging in stagings.into_values() {
239            for res in staging.into_deserialized_changes(&deserializer) {
240                let record = res?;
241                let _none = chunk_builder.append_record(record);
242            }
243        }
244        Ok(chunk_builder.take())
245    }
246
247    async fn flush_data(
248        &mut self,
249        epoch: EpochPair,
250    ) -> StreamExecutorResult<StateTablePostCommit<'_, S>> {
251        self.managed_state.flush(epoch).await
252    }
253
254    async fn try_flush_data(&mut self) -> StreamExecutorResult<()> {
255        self.managed_state.try_flush().await
256    }
257
258    fn clear_cache(&mut self) {
259        self.caches.clear();
260    }
261
262    fn evict(&mut self) {
263        self.caches.evict()
264    }
265
266    async fn init(&mut self, epoch: EpochPair) -> StreamExecutorResult<()> {
267        self.managed_state.init_epoch(epoch).await
268    }
269
270    async fn handle_watermark(&mut self, watermark: Watermark) -> Option<Watermark> {
271        if watermark.col_idx == self.group_by[0] {
272            // The state table is ordered by the first group key column, so the watermark on it can
273            // also be used to clean up the states of the groups below it.
274            self.managed_state.update_watermark(watermark.val.clone());
275        }
276        // A row can only change the output of its own group, so watermarks on group key columns
277        // can always be forwarded. Watermarks on the first `ORDER BY` column can be forwarded if
278        // it's ordered `ASC NULLS LAST`. See `topn_watermark_forwardable_order_key` for the
279        // reasoning.
280        (self.group_by.contains(&watermark.col_idx)
281            || Some(watermark.col_idx) == self.watermark_order_key)
282            .then_some(watermark)
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use std::sync::atomic::AtomicU64;
289
290    use risingwave_common::array::stream_chunk::StreamChunkTestExt;
291    use risingwave_common::catalog::Field;
292    use risingwave_common::hash::SerializedKey;
293    use risingwave_common::util::epoch::test_epoch;
294    use risingwave_common::util::sort_util::OrderType;
295    use risingwave_storage::memory::MemoryStateStore;
296
297    use super::*;
298    use crate::executor::test_utils::top_n_executor::create_in_memory_state_table;
299    use crate::executor::test_utils::{MockSource, StreamExecutorTestExt};
300
301    fn create_schema() -> Schema {
302        Schema {
303            fields: vec![
304                Field::unnamed(DataType::Int64),
305                Field::unnamed(DataType::Int64),
306                Field::unnamed(DataType::Int64),
307            ],
308        }
309    }
310
311    fn storage_key() -> Vec<ColumnOrder> {
312        vec![
313            ColumnOrder::new(1, OrderType::ascending()),
314            ColumnOrder::new(2, OrderType::ascending()),
315            ColumnOrder::new(0, OrderType::ascending()),
316        ]
317    }
318
319    /// group by 1, order by 2
320    fn order_by_1() -> Vec<ColumnOrder> {
321        vec![ColumnOrder::new(2, OrderType::ascending())]
322    }
323
324    /// group by 1,2, order by 0
325    fn order_by_2() -> Vec<ColumnOrder> {
326        vec![ColumnOrder::new(0, OrderType::ascending())]
327    }
328
329    fn stream_key() -> StreamKey {
330        vec![1, 2, 0]
331    }
332
333    fn create_stream_chunks() -> Vec<StreamChunk> {
334        let chunk0 = StreamChunk::from_pretty(
335            "  I I I
336            + 10 9 1
337            +  8 8 2
338            +  7 8 2
339            +  9 1 1
340            + 10 1 1
341            +  8 1 3",
342        );
343        let chunk1 = StreamChunk::from_pretty(
344            "  I I I
345            - 10 9 1
346            -  8 8 2
347            - 10 1 1",
348        );
349        let chunk2 = StreamChunk::from_pretty(
350            " I I I
351            - 7 8 2
352            - 8 1 3
353            - 9 1 1",
354        );
355        let chunk3 = StreamChunk::from_pretty(
356            "  I I I
357            +  5 1 1
358            +  2 1 1
359            +  3 1 2
360            +  4 1 3",
361        );
362        vec![chunk0, chunk1, chunk2, chunk3]
363    }
364
365    fn create_source() -> Executor {
366        let mut chunks = create_stream_chunks();
367        let schema = create_schema();
368        MockSource::with_messages(vec![
369            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
370            Message::Chunk(std::mem::take(&mut chunks[0])),
371            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
372            Message::Chunk(std::mem::take(&mut chunks[1])),
373            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
374            Message::Chunk(std::mem::take(&mut chunks[2])),
375            Message::Barrier(Barrier::new_test_barrier(test_epoch(4))),
376            Message::Chunk(std::mem::take(&mut chunks[3])),
377            Message::Barrier(Barrier::new_test_barrier(test_epoch(5))),
378        ])
379        .into_executor(schema, stream_key())
380    }
381
382    #[tokio::test]
383    async fn test_without_offset_and_with_limits() {
384        let source = create_source();
385        let state_table = create_in_memory_state_table(
386            &[DataType::Int64, DataType::Int64, DataType::Int64],
387            &[
388                OrderType::ascending(),
389                OrderType::ascending(),
390                OrderType::ascending(),
391            ],
392            &stream_key(),
393        )
394        .await;
395        let schema = source.schema().clone();
396        let top_n = GroupTopNExecutor::<SerializedKey, MemoryStateStore, false>::new(
397            source,
398            ActorContext::for_test(0),
399            schema,
400            storage_key(),
401            (0, 2),
402            order_by_1(),
403            vec![1],
404            state_table,
405            Arc::new(AtomicU64::new(0)),
406        )
407        .unwrap();
408        let mut top_n = top_n.boxed().execute();
409
410        // consume the init barrier
411        top_n.expect_barrier().await;
412        assert_eq!(
413            top_n.expect_chunk().await.sort_rows(),
414            StreamChunk::from_pretty(
415                "  I I I
416                + 10 9 1
417                +  8 8 2
418                +  7 8 2
419                +  9 1 1
420                + 10 1 1
421                ",
422            )
423            .sort_rows(),
424        );
425
426        // barrier
427        top_n.expect_barrier().await;
428        assert_eq!(
429            top_n.expect_chunk().await.sort_rows(),
430            StreamChunk::from_pretty(
431                "  I I I
432                - 10 9 1
433                -  8 8 2
434                - 10 1 1
435                +  8 1 3
436                ",
437            )
438            .sort_rows(),
439        );
440
441        // barrier
442        top_n.expect_barrier().await;
443        assert_eq!(
444            top_n.expect_chunk().await.sort_rows(),
445            StreamChunk::from_pretty(
446                " I I I
447                - 7 8 2
448                - 8 1 3
449                - 9 1 1
450                ",
451            )
452            .sort_rows(),
453        );
454
455        // barrier
456        top_n.expect_barrier().await;
457        assert_eq!(
458            top_n.expect_chunk().await.sort_rows(),
459            StreamChunk::from_pretty(
460                " I I I
461                + 5 1 1
462                + 2 1 1
463                ",
464            )
465            .sort_rows(),
466        );
467    }
468
469    #[tokio::test]
470    async fn test_with_offset_and_with_limits() {
471        let source = create_source();
472        let state_table = create_in_memory_state_table(
473            &[DataType::Int64, DataType::Int64, DataType::Int64],
474            &[
475                OrderType::ascending(),
476                OrderType::ascending(),
477                OrderType::ascending(),
478            ],
479            &stream_key(),
480        )
481        .await;
482        let schema = source.schema().clone();
483        let top_n = GroupTopNExecutor::<SerializedKey, MemoryStateStore, false>::new(
484            source,
485            ActorContext::for_test(0),
486            schema,
487            storage_key(),
488            (1, 2),
489            order_by_1(),
490            vec![1],
491            state_table,
492            Arc::new(AtomicU64::new(0)),
493        )
494        .unwrap();
495        let mut top_n = top_n.boxed().execute();
496
497        // consume the init barrier
498        top_n.expect_barrier().await;
499        assert_eq!(
500            top_n.expect_chunk().await.sort_rows(),
501            StreamChunk::from_pretty(
502                "  I I I
503                +  8 8 2
504                + 10 1 1
505                +  8 1 3
506                ",
507            )
508            .sort_rows(),
509        );
510
511        // barrier
512        top_n.expect_barrier().await;
513        assert_eq!(
514            top_n.expect_chunk().await.sort_rows(),
515            StreamChunk::from_pretty(
516                "  I I I
517                -  8 8 2
518                - 10 1 1
519                ",
520            )
521            .sort_rows(),
522        );
523
524        // barrier
525        top_n.expect_barrier().await;
526        assert_eq!(
527            top_n.expect_chunk().await.sort_rows(),
528            StreamChunk::from_pretty(
529                " I I I
530                - 8 1 3",
531            )
532            .sort_rows(),
533        );
534
535        // barrier
536        top_n.expect_barrier().await;
537        assert_eq!(
538            top_n.expect_chunk().await.sort_rows(),
539            StreamChunk::from_pretty(
540                " I I I
541                + 5 1 1
542                + 3 1 2
543                ",
544            )
545            .sort_rows(),
546        );
547    }
548
549    #[tokio::test]
550    async fn test_multi_group_key() {
551        let source = create_source();
552        let state_table = create_in_memory_state_table(
553            &[DataType::Int64, DataType::Int64, DataType::Int64],
554            &[
555                OrderType::ascending(),
556                OrderType::ascending(),
557                OrderType::ascending(),
558            ],
559            &stream_key(),
560        )
561        .await;
562        let schema = source.schema().clone();
563        let top_n = GroupTopNExecutor::<SerializedKey, MemoryStateStore, false>::new(
564            source,
565            ActorContext::for_test(0),
566            schema,
567            storage_key(),
568            (0, 2),
569            order_by_2(),
570            vec![1, 2],
571            state_table,
572            Arc::new(AtomicU64::new(0)),
573        )
574        .unwrap();
575        let mut top_n = top_n.boxed().execute();
576
577        // consume the init barrier
578        top_n.expect_barrier().await;
579        assert_eq!(
580            top_n.expect_chunk().await.sort_rows(),
581            StreamChunk::from_pretty(
582                "  I I I
583                + 10 9 1
584                +  8 8 2
585                +  7 8 2
586                +  9 1 1
587                + 10 1 1
588                +  8 1 3",
589            )
590            .sort_rows(),
591        );
592
593        // barrier
594        top_n.expect_barrier().await;
595        assert_eq!(
596            top_n.expect_chunk().await.sort_rows(),
597            StreamChunk::from_pretty(
598                "  I I I
599                - 10 9 1
600                -  8 8 2
601                - 10 1 1",
602            )
603            .sort_rows(),
604        );
605
606        // barrier
607        top_n.expect_barrier().await;
608        assert_eq!(
609            top_n.expect_chunk().await.sort_rows(),
610            StreamChunk::from_pretty(
611                "  I I I
612                - 7 8 2
613                - 8 1 3
614                - 9 1 1",
615            )
616            .sort_rows(),
617        );
618
619        // barrier
620        top_n.expect_barrier().await;
621        assert_eq!(
622            top_n.expect_chunk().await.sort_rows(),
623            StreamChunk::from_pretty(
624                "  I I I
625                +  5 1 1
626                +  2 1 1
627                +  3 1 2
628                +  4 1 3",
629            )
630            .sort_rows(),
631        );
632    }
633
634    #[tokio::test]
635    async fn test_compact_changes() {
636        let schema = create_schema();
637        let source = MockSource::with_messages(vec![
638            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
639            Message::Chunk(StreamChunk::from_pretty(
640                "  I I I
641                +  0 0 9
642                +  0 0 8
643                +  0 0 7
644                +  0 0 6
645                +  0 1 15
646                +  0 1 14",
647            )),
648            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
649            Message::Chunk(StreamChunk::from_pretty(
650                "  I I I
651                -  0 0 6
652                -  0 0 8
653                +  0 0 4
654                +  0 0 3
655                +  0 1 12
656                +  0 2 26
657                -  0 1 12
658                +  0 1 11",
659            )),
660            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
661            Message::Chunk(StreamChunk::from_pretty(
662                "  I I I
663                +  0 0 11", // this should result in no chunk output
664            )),
665            Message::Barrier(Barrier::new_test_barrier(test_epoch(4))),
666        ])
667        .into_executor(schema.clone(), vec![2]);
668
669        let state_table = create_in_memory_state_table(
670            &schema.data_types(),
671            &[
672                OrderType::ascending(),
673                OrderType::ascending(),
674                OrderType::ascending(),
675            ],
676            &[0, 1, 2], // table pk = group key (0, 1) + order key (2) + additional pk (empty)
677        )
678        .await;
679
680        let top_n = GroupTopNExecutor::<SerializedKey, MemoryStateStore, false>::new(
681            source,
682            ActorContext::for_test(0),
683            schema,
684            vec![
685                ColumnOrder::new(0, OrderType::ascending()),
686                ColumnOrder::new(1, OrderType::ascending()),
687                ColumnOrder::new(2, OrderType::ascending()),
688            ],
689            (0, 2), // (offset, limit)
690            vec![ColumnOrder::new(2, OrderType::ascending())],
691            vec![0, 1],
692            state_table,
693            Arc::new(AtomicU64::new(0)),
694        )
695        .unwrap();
696        let mut top_n = top_n.boxed().execute();
697
698        // initial barrier
699        top_n.expect_barrier().await;
700
701        assert_eq!(
702            top_n.expect_chunk().await.sort_rows(),
703            StreamChunk::from_pretty(
704                "  I I I
705                +  0 0 7
706                +  0 0 6
707                +  0 1 15
708                +  0 1 14",
709            )
710            .sort_rows(),
711        );
712        top_n.expect_barrier().await;
713
714        assert_eq!(
715            top_n.expect_chunk().await.sort_rows(),
716            StreamChunk::from_pretty(
717                "  I I I
718                -  0 0 6
719                -  0 0 7
720                +  0 0 4
721                +  0 0 3
722                -  0 1 15
723                +  0 1 11
724                +  0 2 26",
725            )
726            .sort_rows(),
727        );
728        top_n.expect_barrier().await;
729
730        // no output chunk for the last input chunk
731        top_n.expect_barrier().await;
732    }
733}