@@ -21,6 +21,8 @@ use vortex_array::expr::analysis::referenced_field_paths;
2121use vortex_array:: expr:: root;
2222use vortex_array:: iter:: ArrayIterator ;
2323use vortex_array:: iter:: ArrayIteratorAdapter ;
24+ use vortex_array:: scalar_fn:: fns:: pack:: Pack ;
25+ use vortex_array:: scalar_fn:: fns:: stat:: StatFn ;
2426use vortex_array:: stats:: StatsSet ;
2527use vortex_array:: stream:: ArrayStream ;
2628use vortex_array:: stream:: ArrayStreamAdapter ;
@@ -40,11 +42,31 @@ use vortex_utils::parallelism::get_available_parallelism;
4042use crate :: LayoutReader ;
4143use crate :: LayoutReaderRef ;
4244use crate :: layouts:: row_idx:: RowIdxLayoutReader ;
45+ use crate :: scan:: IDEAL_SPLIT_SIZE ;
4346use crate :: scan:: repeated_scan:: RepeatedScan ;
4447use crate :: scan:: split_by:: SplitBy ;
4548use crate :: scan:: splits:: Splits ;
4649use crate :: scan:: splits:: attempt_split_ranges;
4750
51+ /// Minimum split size for an aggregate scan where every aggregate reads from
52+ /// zone maps only, e.g. SELECT min(col), max(col) from 'file.vortex'.
53+ /// IDEAL_SPLIT_SIZE is good for a decoded split, but aggregate scans which read
54+ /// from zone maps only don't decode columns, so per-row work is cheaper, and
55+ /// small splits create unnecessary overhead.
56+ const PURE_AGGREGATE_MIN_SPLIT_SIZE : u64 = IDEAL_SPLIT_SIZE * 16 ;
57+
58+ /// True if "projection" consists only of aggregate stat expressions
59+ fn is_pure_aggregate_projection ( projection : & Expression ) -> bool {
60+ if projection. is :: < StatFn > ( ) {
61+ return true ;
62+ }
63+ if projection. is :: < Pack > ( ) {
64+ let field_count = projection. as_ :: < Pack > ( ) . names . len ( ) ;
65+ return field_count > 0 && ( 0 ..field_count) . all ( |i| projection. child ( i) . is :: < StatFn > ( ) ) ;
66+ }
67+ false
68+ }
69+
4870/// Builder for scanning a [`LayoutReader`] into arrays, streams, iterators, or mapped outputs.
4971///
5072/// A scan has three independent row restriction mechanisms:
@@ -294,20 +316,33 @@ impl<A: 'static + Send> ScanBuilder<A> {
294316 let field_mask =
295317 referenced_field_masks ( & projection, filter. as_ref ( ) , layout_reader. dtype ( ) ) ?;
296318
297- let splits =
298- if let Some ( ranges) = attempt_split_ranges ( & self . selection , self . row_range . as_ref ( ) ) {
299- Splits :: Ranges ( ranges)
300- } else {
301- let split_range = self
302- . row_range
303- . clone ( )
304- . unwrap_or_else ( || 0 ..layout_reader. row_count ( ) ) ;
305- Splits :: Natural ( self . split_by . splits (
306- layout_reader. as_ref ( ) ,
307- & split_range,
308- & field_mask,
309- ) ?)
310- } ;
319+ // Projection of aggregate-only projections doesn't decode columns so
320+ // small splits are creating a lot of overhead.
321+ let split_by = if is_pure_aggregate_projection ( & projection) {
322+ let target_splits = get_available_parallelism ( ) . unwrap_or ( 1 ) . max ( 1 ) * 4 ;
323+ let scan_rows = self
324+ . row_range
325+ . clone ( )
326+ . map_or_else ( || layout_reader. row_count ( ) , |r| r. end - r. start ) ;
327+ let coarse = scan_rows
328+ . div_ceil ( target_splits as u64 )
329+ . max ( PURE_AGGREGATE_MIN_SPLIT_SIZE ) ;
330+ SplitBy :: RowCount ( usize:: try_from ( coarse) . unwrap_or ( usize:: MAX ) )
331+ } else {
332+ self . split_by
333+ } ;
334+
335+ let splits = if let Some ( ranges) =
336+ attempt_split_ranges ( & self . selection , self . row_range . as_ref ( ) )
337+ {
338+ Splits :: Ranges ( ranges)
339+ } else {
340+ let split_range = self
341+ . row_range
342+ . clone ( )
343+ . unwrap_or_else ( || 0 ..layout_reader. row_count ( ) ) ;
344+ Splits :: Natural ( split_by. splits ( layout_reader. as_ref ( ) , & split_range, & field_mask) ?)
345+ } ;
311346
312347 Ok ( RepeatedScan :: new (
313348 self . session . clone ( ) ,
0 commit comments