@@ -22,6 +22,8 @@ use vortex_array::expr::forms::conjuncts;
2222use vortex_array:: expr:: root;
2323use vortex_array:: iter:: ArrayIterator ;
2424use vortex_array:: iter:: ArrayIteratorAdapter ;
25+ use vortex_array:: scalar_fn:: fns:: pack:: Pack ;
26+ use vortex_array:: scalar_fn:: fns:: stat:: StatFn ;
2527use vortex_array:: stats:: StatsSet ;
2628use vortex_array:: stream:: ArrayStream ;
2729use vortex_array:: stream:: ArrayStreamAdapter ;
@@ -44,11 +46,31 @@ use crate::layouts::row_idx::RowIdxLayoutReader;
4446use crate :: scan:: plan_v2:: LayoutReaderScanPlanV2 ;
4547use crate :: scan:: plan_v2:: ScanPlanRef ;
4648use crate :: scan:: plan_v2:: plan_v2_enabled;
49+ use crate :: scan:: IDEAL_SPLIT_SIZE ;
4750use crate :: scan:: repeated_scan:: RepeatedScan ;
4851use crate :: scan:: split_by:: SplitBy ;
4952use crate :: scan:: splits:: Splits ;
5053use crate :: scan:: splits:: attempt_split_ranges;
5154
55+ /// Minimum split size for an aggregate scan where every aggregate reads from
56+ /// zone maps only, e.g. SELECT min(col), max(col) from 'file.vortex'.
57+ /// IDEAL_SPLIT_SIZE is good for a decoded split, but aggregate scans which read
58+ /// from zone maps only don't decode columns, so per-row work is cheaper, and
59+ /// small splits create unnecessary overhead.
60+ const PURE_AGGREGATE_MIN_SPLIT_SIZE : u64 = IDEAL_SPLIT_SIZE * 16 ;
61+
62+ /// True if "projection" consists only of aggregate stat expressions
63+ fn is_pure_aggregate_projection ( projection : & Expression ) -> bool {
64+ if projection. is :: < StatFn > ( ) {
65+ return true ;
66+ }
67+ if projection. is :: < Pack > ( ) {
68+ let field_count = projection. as_ :: < Pack > ( ) . names . len ( ) ;
69+ return field_count > 0 && ( 0 ..field_count) . all ( |i| projection. child ( i) . is :: < StatFn > ( ) ) ;
70+ }
71+ false
72+ }
73+
5274/// Builder for scanning a [`LayoutReader`] into arrays, streams, iterators, or mapped outputs.
5375///
5476/// A scan has three independent row restriction mechanisms:
@@ -298,20 +320,33 @@ impl<A: 'static + Send> ScanBuilder<A> {
298320 let field_mask =
299321 referenced_field_masks ( & projection, filter. as_ref ( ) , layout_reader. dtype ( ) ) ?;
300322
301- let splits =
302- if let Some ( ranges) = attempt_split_ranges ( & self . selection , self . row_range . as_ref ( ) ) {
303- Splits :: Ranges ( ranges)
304- } else {
305- let split_range = self
306- . row_range
307- . clone ( )
308- . unwrap_or_else ( || 0 ..layout_reader. row_count ( ) ) ;
309- Splits :: Natural ( self . split_by . splits (
310- layout_reader. as_ref ( ) ,
311- & split_range,
312- & field_mask,
313- ) ?)
314- } ;
323+ // Projection of aggregate-only projections doesn't decode columns so
324+ // small splits are creating a lot of overhead.
325+ let split_by = if is_pure_aggregate_projection ( & projection) {
326+ let target_splits = get_available_parallelism ( ) . unwrap_or ( 1 ) . max ( 1 ) * 4 ;
327+ let scan_rows = self
328+ . row_range
329+ . clone ( )
330+ . map_or_else ( || layout_reader. row_count ( ) , |r| r. end - r. start ) ;
331+ let coarse = scan_rows
332+ . div_ceil ( target_splits as u64 )
333+ . max ( PURE_AGGREGATE_MIN_SPLIT_SIZE ) ;
334+ SplitBy :: RowCount ( usize:: try_from ( coarse) . unwrap_or ( usize:: MAX ) )
335+ } else {
336+ self . split_by
337+ } ;
338+
339+ let splits = if let Some ( ranges) =
340+ attempt_split_ranges ( & self . selection , self . row_range . as_ref ( ) )
341+ {
342+ Splits :: Ranges ( ranges)
343+ } else {
344+ let split_range = self
345+ . row_range
346+ . clone ( )
347+ . unwrap_or_else ( || 0 ..layout_reader. row_count ( ) ) ;
348+ Splits :: Natural ( split_by. splits ( layout_reader. as_ref ( ) , & split_range, & field_mask) ?)
349+ } ;
315350
316351 if plan_v2_enabled ( ) ? {
317352 let source: ScanPlanRef =
0 commit comments