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