Skip to main content

mito2/read/
seq_scan.rs

1// Copyright 2023 Greptime Team
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
15//! Sequential scan.
16
17use std::fmt;
18use std::sync::Arc;
19use std::time::Instant;
20
21use async_stream::try_stream;
22use common_error::ext::BoxedError;
23use common_recordbatch::util::ChainedRecordBatchStream;
24use common_recordbatch::{RecordBatchStreamWrapper, SendableRecordBatchStream};
25use common_telemetry::tracing;
26use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
27use datafusion::physical_plan::{DisplayAs, DisplayFormatType};
28use datatypes::schema::SchemaRef;
29use futures::{StreamExt, TryStreamExt};
30use snafu::ensure;
31use store_api::metadata::RegionMetadataRef;
32use store_api::region_engine::{
33    PartitionRange, PrepareRequest, QueryScanContext, RegionScanner, ScannerProperties,
34};
35use store_api::storage::TimeSeriesRowSelector;
36use tokio::sync::Semaphore;
37
38use crate::error::{PartitionOutOfRangeSnafu, Result, TooManyFilesToReadSnafu};
39use crate::metrics::SCAN_MAX_CONCURRENT_FILES;
40use crate::read::flat_dedup::{FlatDedupReader, FlatLastNonNull, FlatLastRow};
41use crate::read::flat_merge::FlatMergeReader;
42use crate::read::last_row::FlatLastRowReader;
43use crate::read::pruner::{PartitionPruner, Pruner};
44use crate::read::range::RangeMeta;
45use crate::read::range_cache::{
46    build_range_cache_key, cache_flat_range_stream, cached_flat_range_stream,
47};
48use crate::read::scan_region::{ScanInput, StreamContext};
49use crate::read::scan_util::{
50    BATCH_SIZE_THRESHOLD, MIN_COMPACTION_SPLIT_ROWS, PartitionMetrics, PartitionMetricsList,
51    SplitRecordBatchStream, compute_parallel_channel_size, scan_flat_file_ranges,
52    scan_flat_mem_ranges, should_split_flat_batches_for_merge,
53};
54use crate::read::stream::{ConvertBatchStream, ScanBatch, ScanBatchStream};
55use crate::read::{BoxedRecordBatchStream, ScannerMetrics, scan_util};
56use crate::region::options::MergeMode;
57use crate::sst::parquet::DEFAULT_READ_BATCH_SIZE;
58
59/// Describes where flat source batches will be merged.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub(crate) enum FlatReadMode {
62    /// Each range is consumed independently.
63    Sequential,
64    /// Range outputs feed a later SeriesScan merge.
65    CrossRangeMerge,
66    /// Sources from all ranges feed the compaction merge.
67    Compaction,
68}
69
70/// Scans a region and returns rows in a sorted sequence.
71///
72/// The output order is always `order by primary keys, time index` inside every
73/// [`PartitionRange`]. Each "partition" may contains many [`PartitionRange`]s.
74pub struct SeqScan {
75    /// Properties of the scanner.
76    properties: ScannerProperties,
77    /// Context of streams.
78    stream_ctx: Arc<StreamContext>,
79    /// Shared pruner for file range building.
80    pruner: Arc<Pruner>,
81    /// Metrics for each partition.
82    /// The scanner only sets in query and keeps it empty during compaction.
83    metrics_list: PartitionMetricsList,
84}
85
86impl SeqScan {
87    /// Creates a new [SeqScan] with the given input.
88    /// If `input.compaction` is true, the scanner will not attempt to split ranges.
89    pub(crate) fn new(input: ScanInput) -> Self {
90        let mut properties = ScannerProperties::default()
91            .with_append_mode(input.append_mode)
92            .with_total_rows(input.total_rows())
93            .with_total_rows_is_exact(input.append_mode && input.total_rows_is_exact());
94        if let Some(counters) = input.query_stat_counters.clone() {
95            properties.set_query_stat_counters(counters);
96        }
97        let stream_ctx = Arc::new(StreamContext::seq_scan_ctx(input));
98        properties.partitions = vec![stream_ctx.partition_ranges()];
99
100        // Create the shared pruner with number of workers equal to CPU cores.
101        let num_workers = common_stat::get_total_cpu_cores().max(1);
102        let pruner = Arc::new(Pruner::new(stream_ctx.clone(), num_workers));
103
104        Self {
105            properties,
106            stream_ctx,
107            pruner,
108            metrics_list: PartitionMetricsList::default(),
109        }
110    }
111
112    /// Builds a stream for the query.
113    ///
114    /// The returned stream is not partitioned and will contains all the data. If want
115    /// partitioned scan, use [`RegionScanner::scan_partition`].
116    #[tracing::instrument(
117        skip_all,
118        fields(region_id = %self.stream_ctx.input.mapper.metadata().region_id)
119    )]
120    pub fn build_stream(&self) -> Result<SendableRecordBatchStream, BoxedError> {
121        let metrics_set = ExecutionPlanMetricsSet::new();
122        let ctx = QueryScanContext::default();
123        let streams = (0..self.properties.partitions.len())
124            .map(|partition: usize| self.scan_partition(&ctx, &metrics_set, partition))
125            .collect::<Result<Vec<_>, _>>()?;
126
127        let aggr_stream = ChainedRecordBatchStream::new(streams).map_err(BoxedError::new)?;
128        Ok(Box::pin(aggr_stream))
129    }
130
131    /// Scan [`Batch`] in all partitions one by one.
132    pub(crate) fn scan_all_partitions(&self) -> Result<ScanBatchStream> {
133        let metrics_set = ExecutionPlanMetricsSet::new();
134
135        let streams = (0..self.properties.partitions.len())
136            .map(|partition| {
137                let metrics = self.new_partition_metrics(false, &metrics_set, partition);
138                self.scan_flat_batch_in_partition(partition, metrics)
139            })
140            .collect::<Result<Vec<_>>>()?;
141
142        Ok(Box::pin(futures::stream::iter(streams).flatten()))
143    }
144
145    /// Builds a [BoxedRecordBatchStream] from sequential scan for flat format compaction.
146    ///
147    /// # Panics
148    /// Panics if the compaction flag is not set.
149    pub(crate) async fn build_flat_reader_for_compaction(&self) -> Result<BoxedRecordBatchStream> {
150        assert!(self.stream_ctx.input.compaction);
151
152        // Compaction merges all input files together instead of scanning ranges sequentially.
153        SCAN_MAX_CONCURRENT_FILES
154            .with_label_values(&["compaction"])
155            .observe(self.stream_ctx.input.num_files() as f64);
156
157        let metrics_set = ExecutionPlanMetricsSet::new();
158        let part_metrics = self.new_partition_metrics(false, &metrics_set, 0);
159        debug_assert_eq!(1, self.properties.partitions.len());
160        let partition_ranges = &self.properties.partitions[0];
161
162        let reader = Self::merge_all_flat_ranges_for_compaction(
163            &self.stream_ctx,
164            partition_ranges,
165            &part_metrics,
166            self.pruner.clone(),
167        )
168        .await?;
169        Ok(reader)
170    }
171
172    /// Builds a merge reader that reads all flat ranges.
173    /// Callers MUST not split ranges before calling this method.
174    async fn merge_all_flat_ranges_for_compaction(
175        stream_ctx: &Arc<StreamContext>,
176        partition_ranges: &[PartitionRange],
177        part_metrics: &PartitionMetrics,
178        pruner: Arc<Pruner>,
179    ) -> Result<BoxedRecordBatchStream> {
180        pruner.add_partition_ranges(partition_ranges);
181        let partition_pruner = Arc::new(PartitionPruner::new(pruner, partition_ranges));
182
183        let mut sources = Vec::new();
184        for part_range in partition_ranges {
185            build_flat_sources(
186                stream_ctx,
187                part_range,
188                FlatReadMode::Compaction,
189                part_metrics,
190                partition_pruner.clone(),
191                &mut sources,
192                None,
193            )
194            .await?;
195        }
196
197        common_telemetry::debug!(
198            "Build flat reader to read all parts, region_id: {}, num_part_ranges: {}, num_sources: {}",
199            stream_ctx.input.mapper.metadata().region_id,
200            partition_ranges.len(),
201            sources.len()
202        );
203        Self::build_flat_reader_from_sources(
204            stream_ctx,
205            sources,
206            None,
207            None,
208            false,
209            compute_parallel_channel_size(DEFAULT_READ_BATCH_SIZE),
210        )
211        .await
212    }
213
214    /// Builds a flat reader to read sources that returns RecordBatch.
215    /// If `semaphore` is provided, reads sources in parallel if possible.
216    /// If `skip_dedup` is true, the merged stream is returned without applying flat dedup.
217    #[tracing::instrument(level = tracing::Level::DEBUG, skip_all)]
218    pub(crate) async fn build_flat_reader_from_sources(
219        stream_ctx: &StreamContext,
220        mut sources: Vec<BoxedRecordBatchStream>,
221        semaphore: Option<Arc<Semaphore>>,
222        part_metrics: Option<&PartitionMetrics>,
223        skip_dedup: bool,
224        channel_size: usize,
225    ) -> Result<BoxedRecordBatchStream> {
226        if let Some(semaphore) = semaphore.as_ref() {
227            // Read sources in parallel.
228            if sources.len() > 1 {
229                sources = stream_ctx.input.create_parallel_flat_sources(
230                    sources,
231                    semaphore.clone(),
232                    channel_size,
233                )?;
234            }
235        }
236
237        let mapper = &stream_ctx.input.mapper;
238        let reader: BoxedRecordBatchStream = if sources.len() == 1 {
239            // Currently, we can't skip dedup when there is only one source because
240            // that source may have duplicate rows.
241            sources.pop().unwrap()
242        } else {
243            let schema = mapper.input_arrow_schema(stream_ctx.input.compaction);
244            let metrics_reporter = part_metrics.map(|m| m.merge_metrics_reporter());
245            let reader = FlatMergeReader::new(
246                schema,
247                sources,
248                stream_ctx.input.batch_size(),
249                metrics_reporter,
250            )
251            .await?;
252            Box::pin(reader.into_stream())
253        };
254
255        let dedup = !skip_dedup && !stream_ctx.input.append_mode;
256        let dedup_metrics_reporter = part_metrics.map(|m| m.dedup_metrics_reporter());
257        let reader = if dedup {
258            match stream_ctx.input.merge_mode {
259                MergeMode::LastRow => Box::pin(
260                    FlatDedupReader::new(
261                        reader,
262                        FlatLastRow::new(stream_ctx.input.filter_deleted),
263                        dedup_metrics_reporter,
264                    )
265                    .into_stream(),
266                ) as _,
267                MergeMode::LastNonNull => Box::pin(
268                    FlatDedupReader::new(
269                        reader,
270                        FlatLastNonNull::new(
271                            mapper.field_column_start(),
272                            stream_ctx.input.filter_deleted,
273                        ),
274                        dedup_metrics_reporter,
275                    )
276                    .into_stream(),
277                ) as _,
278            }
279        } else {
280            reader
281        };
282
283        let reader = match &stream_ctx.input.series_row_selector {
284            Some(TimeSeriesRowSelector::LastRow { .. }) => {
285                Box::pin(FlatLastRowReader::new(reader).into_stream()) as _
286            }
287            None => reader,
288        };
289
290        Ok(reader)
291    }
292
293    /// Builds a flat read stream for one partition range.
294    pub(crate) async fn build_flat_partition_range_read(
295        stream_ctx: &Arc<StreamContext>,
296        part_range: &PartitionRange,
297        read_mode: FlatReadMode,
298        part_metrics: &PartitionMetrics,
299        partition_pruner: Arc<PartitionPruner>,
300        file_scan_semaphore: Option<Arc<Semaphore>>,
301        merge_semaphore: Option<Arc<Semaphore>>,
302    ) -> Result<(BoxedRecordBatchStream, usize)> {
303        let cache_key = build_range_cache_key(stream_ctx, part_range).map(|key| {
304            if read_mode == FlatReadMode::CrossRangeMerge {
305                key.for_cross_range_merge()
306            } else {
307                key
308            }
309        });
310
311        if let Some(key) = cache_key.as_ref() {
312            if let Some(value) = stream_ctx.input.cache_strategy.get_range_result(key) {
313                part_metrics.inc_range_cache_hit();
314                return Ok((cached_flat_range_stream(value), DEFAULT_READ_BATCH_SIZE));
315            }
316            part_metrics.inc_range_cache_miss();
317        }
318
319        let mut sources = Vec::new();
320        let split_batch_size = build_flat_sources(
321            stream_ctx,
322            part_range,
323            read_mode,
324            part_metrics,
325            partition_pruner,
326            &mut sources,
327            file_scan_semaphore,
328        )
329        .await?;
330        let estimated_rows_per_batch = split_batch_size.unwrap_or(DEFAULT_READ_BATCH_SIZE);
331        let channel_size = compute_parallel_channel_size(estimated_rows_per_batch);
332        let stream = Self::build_flat_reader_from_sources(
333            stream_ctx,
334            sources,
335            merge_semaphore,
336            Some(part_metrics),
337            false,
338            channel_size,
339        )
340        .await?;
341
342        let stream = match cache_key {
343            Some(key) => cache_flat_range_stream(
344                stream,
345                stream_ctx.input.cache_strategy.clone(),
346                key,
347                part_metrics.clone(),
348            ),
349            None => stream,
350        };
351
352        Ok((stream, estimated_rows_per_batch))
353    }
354
355    /// Scans the given partition when the part list is set properly.
356    /// Otherwise the returned stream might not contains any data.
357    fn scan_partition_impl(
358        &self,
359        ctx: &QueryScanContext,
360        metrics_set: &ExecutionPlanMetricsSet,
361        partition: usize,
362    ) -> Result<SendableRecordBatchStream> {
363        if ctx.explain_verbose {
364            common_telemetry::info!(
365                "SeqScan partition {}, region_id: {}",
366                partition,
367                self.stream_ctx.input.region_metadata().region_id
368            );
369        }
370
371        let metrics = self.new_partition_metrics(ctx.explain_verbose, metrics_set, partition);
372        let input = &self.stream_ctx.input;
373
374        let batch_stream = self.scan_flat_batch_in_partition(partition, metrics.clone())?;
375        let record_batch_stream = ConvertBatchStream::new(
376            batch_stream,
377            input.mapper.clone(),
378            input.cache_strategy.clone(),
379            metrics,
380        );
381
382        Ok(Box::pin(RecordBatchStreamWrapper::new(
383            input.mapper.output_schema(),
384            Box::pin(record_batch_stream),
385        )))
386    }
387
388    #[tracing::instrument(
389        skip_all,
390        fields(
391            region_id = %self.stream_ctx.input.mapper.metadata().region_id,
392            partition = partition
393        )
394    )]
395    fn scan_flat_batch_in_partition(
396        &self,
397        partition: usize,
398        part_metrics: PartitionMetrics,
399    ) -> Result<ScanBatchStream> {
400        ensure!(
401            partition < self.properties.partitions.len(),
402            PartitionOutOfRangeSnafu {
403                given: partition,
404                all: self.properties.partitions.len(),
405            }
406        );
407
408        if self.properties.partitions[partition].is_empty() {
409            return Ok(Box::pin(futures::stream::empty()));
410        }
411
412        let stream_ctx = self.stream_ctx.clone();
413        let semaphore = self.new_semaphore();
414        let partition_ranges = self.properties.partitions[partition].clone();
415        let compaction = self.stream_ctx.input.compaction;
416        let file_scan_semaphore = if compaction { None } else { semaphore.clone() };
417        let pruner = self.pruner.clone();
418        // Initializes ref counts for the pruner.
419        // If we call scan_batch_in_partition() multiple times but don't read all batches from the stream,
420        // then the ref count won't be decremented.
421        // This is a rare case and keeping all remaining entries still uses less memory than a per partition cache.
422        pruner.add_partition_ranges(&partition_ranges);
423        let partition_pruner = Arc::new(PartitionPruner::new(pruner, &partition_ranges));
424
425        let stream = try_stream! {
426            part_metrics.on_first_poll();
427            // Start fetch time before building sources so scan cost contains
428            // build part cost.
429            let mut fetch_start = Instant::now();
430
431            // Scans each part.
432            for part_range in partition_ranges {
433                let (mut reader, _) = Self::build_flat_partition_range_read(
434                    &stream_ctx,
435                    &part_range,
436                    if compaction {
437                        FlatReadMode::Compaction
438                    } else {
439                        FlatReadMode::Sequential
440                    },
441                    &part_metrics,
442                    partition_pruner.clone(),
443                    file_scan_semaphore.clone(),
444                    semaphore.clone(),
445                )
446                .await?;
447
448                let mut metrics = ScannerMetrics {
449                    scan_cost: fetch_start.elapsed(),
450                    ..Default::default()
451                };
452                fetch_start = Instant::now();
453
454                while let Some(record_batch) = reader.try_next().await? {
455                    metrics.scan_cost += fetch_start.elapsed();
456                    metrics.num_batches += 1;
457                    metrics.num_rows += record_batch.num_rows();
458
459                    debug_assert!(record_batch.num_rows() > 0);
460                    if record_batch.num_rows() == 0 {
461                        fetch_start = Instant::now();
462                        continue;
463                    }
464
465                    let yield_start = Instant::now();
466                    yield ScanBatch::RecordBatch(record_batch);
467                    metrics.yield_cost += yield_start.elapsed();
468
469                    fetch_start = Instant::now();
470                }
471
472                metrics.scan_cost += fetch_start.elapsed();
473                fetch_start = Instant::now();
474                part_metrics.merge_metrics(&metrics);
475            }
476
477            part_metrics.on_finish();
478        };
479        Ok(Box::pin(stream))
480    }
481
482    fn new_semaphore(&self) -> Option<Arc<Semaphore>> {
483        if self.properties.target_partitions() > self.properties.num_partitions() {
484            // We can use additional tasks to read the data if we have more target partitions than actual partitions.
485            // This semaphore is partition level.
486            // We don't use a global semaphore to avoid a partition waiting for others. The final concurrency
487            // of tasks usually won't exceed the target partitions a lot as compaction can reduce the number of
488            // files in a part range.
489            Some(Arc::new(Semaphore::new(
490                self.properties.target_partitions() - self.properties.num_partitions() + 1,
491            )))
492        } else {
493            None
494        }
495    }
496
497    /// Creates a new partition metrics instance.
498    /// Sets the partition metrics for the given partition if it is not for compaction.
499    fn new_partition_metrics(
500        &self,
501        explain_verbose: bool,
502        metrics_set: &ExecutionPlanMetricsSet,
503        partition: usize,
504    ) -> PartitionMetrics {
505        let metrics = PartitionMetrics::new(
506            self.stream_ctx.input.mapper.metadata().region_id,
507            partition,
508            get_scanner_type(self.stream_ctx.input.compaction),
509            self.stream_ctx.query_start,
510            explain_verbose,
511            metrics_set,
512        );
513
514        if !self.stream_ctx.input.compaction {
515            self.metrics_list.set(partition, metrics.clone());
516        }
517
518        metrics
519    }
520
521    /// Finds the maximum number of files to read in a single partition range.
522    fn max_files_in_partition(ranges: &[RangeMeta], partition_ranges: &[PartitionRange]) -> usize {
523        partition_ranges
524            .iter()
525            .map(|part_range| {
526                let range_meta = &ranges[part_range.identifier];
527                range_meta.indices.len()
528            })
529            .max()
530            .unwrap_or(0)
531    }
532
533    /// Checks resource limit for the scanner.
534    pub(crate) fn check_scan_limit(&self) -> Result<()> {
535        // Check max file count limit for all partitions since we scan them in parallel.
536        let total_max_files: usize = self
537            .properties
538            .partitions
539            .iter()
540            .map(|partition| Self::max_files_in_partition(&self.stream_ctx.ranges, partition))
541            .sum();
542
543        if !self.stream_ctx.input.compaction {
544            SCAN_MAX_CONCURRENT_FILES
545                .with_label_values(&["query"])
546                .observe(total_max_files as f64);
547        }
548        if self.stream_ctx.input.uses_scan_memory_limit() {
549            return Ok(());
550        }
551
552        let max_concurrent_files = self.stream_ctx.input.max_concurrent_scan_files;
553        if total_max_files > max_concurrent_files {
554            return TooManyFilesToReadSnafu {
555                actual: total_max_files,
556                max: max_concurrent_files,
557            }
558            .fail();
559        }
560
561        Ok(())
562    }
563}
564
565impl RegionScanner for SeqScan {
566    fn name(&self) -> &str {
567        "SeqScan"
568    }
569
570    fn properties(&self) -> &ScannerProperties {
571        &self.properties
572    }
573
574    fn schema(&self) -> SchemaRef {
575        self.stream_ctx.input.mapper.output_schema()
576    }
577
578    fn metadata(&self) -> RegionMetadataRef {
579        self.stream_ctx.input.mapper.metadata().clone()
580    }
581
582    fn scan_partition(
583        &self,
584        ctx: &QueryScanContext,
585        metrics_set: &ExecutionPlanMetricsSet,
586        partition: usize,
587    ) -> Result<SendableRecordBatchStream, BoxedError> {
588        self.scan_partition_impl(ctx, metrics_set, partition)
589            .map_err(BoxedError::new)
590    }
591
592    fn prepare(&mut self, request: PrepareRequest) -> Result<(), BoxedError> {
593        self.properties.prepare(request);
594
595        self.check_scan_limit().map_err(BoxedError::new)?;
596
597        Ok(())
598    }
599
600    fn has_predicate_without_region(&self) -> bool {
601        let predicate = self
602            .stream_ctx
603            .input
604            .predicate_group()
605            .predicate_without_region();
606        predicate.is_some()
607    }
608
609    fn add_dyn_filter_to_predicate(
610        &mut self,
611        filter_exprs: Vec<Arc<dyn datafusion::physical_plan::PhysicalExpr>>,
612    ) -> Vec<bool> {
613        self.stream_ctx.add_dyn_filter_to_predicate(filter_exprs)
614    }
615
616    fn reset_state(&mut self) {
617        self.stream_ctx.input.predicate.clear_dyn_filters();
618        let num_workers = common_stat::get_total_cpu_cores().max(1);
619        self.pruner = Arc::new(Pruner::new(self.stream_ctx.clone(), num_workers));
620        self.metrics_list = PartitionMetricsList::default();
621    }
622
623    fn set_logical_region(&mut self, logical_region: bool) {
624        self.properties.set_logical_region(logical_region);
625    }
626
627    fn set_query_load_region_id(&mut self, region_id: store_api::storage::RegionId) {
628        self.properties.set_query_load_region_id(region_id);
629    }
630
631    fn snapshot_sequence(&self) -> Option<u64> {
632        self.stream_ctx.input.snapshot_sequence
633    }
634}
635
636impl DisplayAs for SeqScan {
637    fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
638        write!(
639            f,
640            "SeqScan: region={}, ",
641            self.stream_ctx.input.mapper.metadata().region_id
642        )?;
643        match t {
644            // TODO(LFC): Implement all the "TreeRender" display format.
645            DisplayFormatType::Default | DisplayFormatType::TreeRender => {
646                self.stream_ctx.format_for_explain(false, f)
647            }
648            DisplayFormatType::Verbose => {
649                self.stream_ctx.format_for_explain(true, f)?;
650                self.metrics_list.format_verbose_metrics(f)
651            }
652        }
653    }
654}
655
656impl fmt::Debug for SeqScan {
657    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
658        f.debug_struct("SeqScan")
659            .field("num_ranges", &self.stream_ctx.ranges.len())
660            .finish()
661    }
662}
663
664/// Builds flat sources for the partition range and push them to the `sources` vector.
665/// Returns the estimated rows per batch after splitting if splitting is applied, or `None`.
666pub(crate) async fn build_flat_sources(
667    stream_ctx: &Arc<StreamContext>,
668    part_range: &PartitionRange,
669    read_mode: FlatReadMode,
670    part_metrics: &PartitionMetrics,
671    partition_pruner: Arc<PartitionPruner>,
672    sources: &mut Vec<BoxedRecordBatchStream>,
673    semaphore: Option<Arc<Semaphore>>,
674) -> Result<Option<usize>> {
675    let compaction = read_mode == FlatReadMode::Compaction;
676    // Gets range meta.
677    let range_meta = &stream_ctx.ranges[part_range.identifier];
678    #[cfg(debug_assertions)]
679    if compaction {
680        // Compaction expects input sources are not been split.
681        debug_assert_eq!(range_meta.indices.len(), range_meta.row_group_indices.len());
682        for (i, row_group_idx) in range_meta.row_group_indices.iter().enumerate() {
683            // It should scan all row groups.
684            debug_assert_eq!(
685                -1, row_group_idx.row_group_index,
686                "Expect {} range scan all row groups, given: {}",
687                i, row_group_idx.row_group_index,
688            );
689        }
690    }
691
692    let read_type = if compaction {
693        "compaction"
694    } else {
695        "seq_scan_files"
696    };
697    let num_indices = range_meta.row_group_indices.len();
698    if num_indices == 0 {
699        return Ok(None);
700    }
701
702    sources.reserve(num_indices);
703    let mut ordered_sources = Vec::with_capacity(num_indices);
704    ordered_sources.resize_with(num_indices, || None);
705    let mut file_scan_tasks = Vec::new();
706    let pre_filter_mode = stream_ctx.range_pre_filter_mode(part_range);
707
708    for (position, index) in range_meta.row_group_indices.iter().enumerate() {
709        if stream_ctx.is_mem_range_index(*index) {
710            let stream = scan_flat_mem_ranges(
711                stream_ctx.clone(),
712                part_metrics.clone(),
713                *index,
714                range_meta.time_range,
715            );
716            ordered_sources[position] = Some(Box::pin(stream) as _);
717        } else if stream_ctx.is_file_range_index(*index) {
718            // Common manifest-level fast-skip shared by SeqScan and UnorderedScan.
719            // Compaction should keep reading its selected input ranges completely.
720            if !compaction
721                && partition_pruner.try_skip_manifest_pruned_file_range(*index, part_metrics)
722            {
723                continue;
724            }
725            if let Some(semaphore_ref) = semaphore.as_ref() {
726                // run in parallel, controlled by semaphore
727                let stream_ctx = stream_ctx.clone();
728                let part_metrics = part_metrics.clone();
729                let partition_pruner = partition_pruner.clone();
730                let semaphore = Arc::clone(semaphore_ref);
731                let row_group_index = *index;
732                file_scan_tasks.push(async move {
733                    let _permit = semaphore.acquire().await.unwrap();
734                    let stream = scan_flat_file_ranges(
735                        stream_ctx,
736                        part_metrics,
737                        row_group_index,
738                        read_type,
739                        partition_pruner,
740                    )
741                    .await?;
742                    Ok((position, Box::pin(stream) as _))
743                });
744            } else {
745                // no semaphore, run sequentially
746                let stream = scan_flat_file_ranges(
747                    stream_ctx.clone(),
748                    part_metrics.clone(),
749                    *index,
750                    read_type,
751                    partition_pruner.clone(),
752                )
753                .await?;
754                ordered_sources[position] = Some(Box::pin(stream) as _);
755            }
756        } else {
757            let stream = scan_util::maybe_scan_flat_other_ranges(
758                stream_ctx,
759                *index,
760                part_metrics,
761                pre_filter_mode,
762            )
763            .await?;
764            ordered_sources[position] = Some(stream);
765        }
766    }
767
768    if !file_scan_tasks.is_empty() {
769        let results = futures::future::try_join_all(file_scan_tasks).await?;
770        for (position, stream) in results {
771            ordered_sources[position] = Some(stream);
772        }
773    }
774
775    // SeriesScan also merges range outputs. Split at the source, before deduplication
776    // and LastRow selection, so their output batching is preserved by the range cache.
777    let needs_merge =
778        ordered_sources.iter().flatten().count() > 1 || (compaction && stream_ctx.ranges.len() > 1);
779    let split_batch_size = if needs_merge || read_mode == FlatReadMode::CrossRangeMerge {
780        should_split_flat_batches_for_merge(stream_ctx, range_meta).filter(|rows_per_series| {
781            // A single-source range only benefits at the downstream merge. Tiny
782            // splits add cache replay, channel and distribution work per batch.
783            if !needs_merge && *rows_per_series < BATCH_SIZE_THRESHOLD as usize {
784                return false;
785            }
786            // Compaction writes every merged batch out, so splitting short series
787            // would hand the SST writer and indexer one tiny batch per series.
788            !compaction || *rows_per_series >= MIN_COMPACTION_SPLIT_ROWS
789        })
790    } else {
791        None
792    };
793    let should_split = split_batch_size.is_some();
794
795    for stream in ordered_sources.into_iter().flatten() {
796        if should_split {
797            sources.push(Box::pin(SplitRecordBatchStream::new(stream)));
798        } else {
799            sources.push(stream);
800        }
801    }
802
803    if should_split {
804        common_telemetry::debug!(
805            "Splitting record batches, region: {}, sources: {}, part_range: {:?}",
806            stream_ctx.input.region_metadata().region_id,
807            sources.len(),
808            part_range,
809        );
810    }
811
812    Ok(split_batch_size)
813}
814
815#[cfg(test)]
816impl SeqScan {
817    /// Returns the input.
818    pub(crate) fn input(&self) -> &ScanInput {
819        &self.stream_ctx.input
820    }
821}
822
823/// Returns the scanner type.
824fn get_scanner_type(compaction: bool) -> &'static str {
825    if compaction {
826        "SeqScan(compaction)"
827    } else {
828        "SeqScan"
829    }
830}