Skip to main content

radixdb_executor/aggregation/
mod.rs

1// Copyright 2026 RadixDB Contributors
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//! Aggregation and GROUP BY Execution
16//!
17//! This module implements aggregation with GROUP BY and HAVING clauses:
18//!
19//! - Global aggregation (without GROUP BY): `SELECT COUNT(*) FROM table`
20//! - Grouped aggregation: `SELECT category, SUM(amount) FROM sales GROUP BY category`
21//! - HAVING clause: `SELECT category, SUM(amount) FROM sales GROUP BY category HAVING SUM(amount) > 100`
22
23use std::hash::{BuildHasherDefault, Hash, Hasher};
24use std::sync::{Arc, Mutex, RwLock};
25
26use ahash::AHasher;
27use hashbrown::hash_map::RawEntryMut;
28#[cfg(feature = "parallel")]
29use rayon::prelude::*;
30use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
31// SmallVec removed - Vec is faster due to spilled() check overhead in hot loops
32
33use radixdb_core::{CompactArc, CompactVec, I64Map, StringMap};
34use radixdb_core::{Error, Result, Row, RowVec, Value, ValueMap, ValueSet};
35use radixdb_functions::aggregate::{numeric::NumericAccumulator, CompiledAggregate};
36use radixdb_functions::{AggregateFunction, AggregateOrderBySpec, FunctionRegistry};
37use radixdb_sql::ast::*;
38use radixdb_storage::mvcc::engine::MVCCEngine;
39use radixdb_storage::traits::{Engine, QueryResult};
40
41use super::compiled_plan::{CompiledCountDistinct, CompiledExecution};
42use super::context::ExecutionContext;
43#[allow(deprecated)]
44use super::expression::CompiledEvaluator;
45use super::expression::{ExpressionEval, RowFilter};
46use super::mutation::host::ActiveTransaction;
47use super::query_classification::QueryClassification;
48use super::result::ExecutorResult;
49use super::utils::build_column_index_map;
50
51// Re-export for backward compatibility
52pub use super::utils::{expression_contains_aggregate, is_aggregate_function};
53
54mod execute;
55mod finalize;
56mod global;
57mod grouped;
58mod planning;
59mod rollup;
60mod storage;
61mod streaming;
62#[cfg(test)]
63mod tests;
64
65/// Narrow composition contract for the remaining correlated-subquery hooks.
66/// Storage and function dependencies stay explicit and downward-only.
67pub trait AggregationHost: Sync {
68    fn aggregation_engine(&self) -> &Arc<MVCCEngine>;
69    fn aggregation_function_registry(&self) -> &FunctionRegistry;
70    fn aggregation_active_transaction(&self) -> &Mutex<Option<ActiveTransaction>>;
71    fn aggregation_process_where_subqueries(
72        &self,
73        expression: &Expression,
74        context: &ExecutionContext,
75    ) -> Result<Expression>;
76    fn aggregation_try_process_select_subqueries(
77        &self,
78        columns: &[Expression],
79        context: &ExecutionContext,
80    ) -> Result<Option<Vec<Expression>>>;
81    fn aggregation_has_correlated_subqueries(&self, expression: &Expression) -> bool;
82    fn aggregation_process_correlated_expression(
83        &self,
84        expression: &Expression,
85        context: &ExecutionContext,
86    ) -> Result<Expression>;
87    fn aggregation_output_column_names(
88        &self,
89        select_expressions: &[Expression],
90        source_columns: &[String],
91        table_alias: Option<&str>,
92    ) -> Vec<String>;
93}
94
95/// Single owner for aggregate planning, state, execution and finalization.
96pub struct AggregationExecutor<'a, H: AggregationHost + ?Sized> {
97    host: &'a H,
98}
99
100impl<'a, H: AggregationHost + ?Sized> AggregationExecutor<'a, H> {
101    fn new(host: &'a H) -> Self {
102        Self { host }
103    }
104}
105
106/// Internal call surface between aggregation phases and the SELECT owner.
107pub trait AggregationExecutorExt: AggregationHost {
108    fn execute_select_with_aggregation(
109        &self,
110        statement: &SelectStatement,
111        context: &ExecutionContext,
112        rows: RowVec,
113        columns: &[String],
114    ) -> Result<Box<dyn QueryResult>> {
115        AggregationExecutor::new(self)
116            .execute_select_with_aggregation(statement, context, rows, columns)
117    }
118
119    fn execute_aggregation_for_window(
120        &self,
121        statement: &SelectStatement,
122        context: &ExecutionContext,
123        rows: &[(i64, Row)],
124        columns: &[String],
125    ) -> Result<(Vec<String>, RowVec)> {
126        AggregationExecutor::new(self)
127            .execute_aggregation_for_window(statement, context, rows, columns)
128    }
129
130    fn try_aggregation_pushdown(
131        &self,
132        table: &dyn radixdb_storage::traits::Table,
133        statement: &SelectStatement,
134        context: &ExecutionContext,
135        classification: &Arc<QueryClassification>,
136    ) -> Result<Option<Box<dyn QueryResult>>> {
137        AggregationExecutor::new(self).try_aggregation_pushdown(
138            table,
139            statement,
140            context,
141            classification,
142        )
143    }
144
145    fn try_filtered_aggregation_pushdown(
146        &self,
147        table: &dyn radixdb_storage::traits::Table,
148        statement: &SelectStatement,
149        context: &ExecutionContext,
150        classification: &Arc<QueryClassification>,
151        columns: &[String],
152    ) -> Result<Option<Box<dyn QueryResult>>> {
153        AggregationExecutor::new(self).try_filtered_aggregation_pushdown(
154            table,
155            statement,
156            context,
157            classification,
158            columns,
159        )
160    }
161
162    fn try_streaming_global_aggregation(
163        &self,
164        table: &dyn radixdb_storage::traits::Table,
165        statement: &SelectStatement,
166        context: &ExecutionContext,
167        classification: &Arc<QueryClassification>,
168    ) -> Result<Option<Box<dyn QueryResult>>> {
169        AggregationExecutor::new(self).try_streaming_global_aggregation(
170            table,
171            statement,
172            context,
173            classification,
174        )
175    }
176
177    fn try_streaming_derived_table_aggregation(
178        &self,
179        source: Box<dyn QueryResult>,
180        statement: &SelectStatement,
181        classification: &Arc<QueryClassification>,
182        context: &ExecutionContext,
183    ) -> Result<DerivedAggregationAttempt> {
184        AggregationExecutor::new(self).try_streaming_derived_table_aggregation(
185            source,
186            statement,
187            classification,
188            context,
189        )
190    }
191
192    fn try_storage_aggregation(
193        &self,
194        table: &dyn radixdb_storage::traits::Table,
195        statement: &SelectStatement,
196        context: &ExecutionContext,
197        columns: &[String],
198        classification: &QueryClassification,
199    ) -> Option<Box<dyn QueryResult>> {
200        AggregationExecutor::new(self).try_storage_aggregation(
201            table,
202            statement,
203            context,
204            columns,
205            classification,
206        )
207    }
208
209    fn try_fast_count_distinct_compiled(
210        &self,
211        statement: &SelectStatement,
212        compiled: &RwLock<CompiledExecution>,
213    ) -> Option<Result<Box<dyn QueryResult>>> {
214        AggregationExecutor::new(self).try_fast_count_distinct_compiled(statement, compiled)
215    }
216
217    fn try_fast_count_star_compiled(
218        &self,
219        statement: &SelectStatement,
220        compiled: &RwLock<CompiledExecution>,
221    ) -> Option<Result<Box<dyn QueryResult>>> {
222        AggregationExecutor::new(self).try_fast_count_star_compiled(statement, compiled)
223    }
224}
225
226impl<T: AggregationHost + ?Sized> AggregationExecutorExt for T {}
227
228/// Single condition in a HAVING clause
229#[derive(Clone, Debug)]
230struct HavingCondition {
231    /// Index of the aggregate in the aggregations array
232    agg_index: usize,
233    /// Comparison operator
234    op: ComparisonOp,
235    /// Threshold value
236    threshold: f64,
237}
238
239/// Simple HAVING filter for inline application during fast aggregation
240/// Supports: SUM(col) op value, COUNT(*) op value, COUNT(col) op value
241/// Also supports AND combinations: COUNT(*) > 10 AND SUM(x) > 100
242#[derive(Clone, Debug)]
243struct SimpleHavingFilter {
244    /// All conditions that must pass (AND semantics)
245    conditions: Vec<HavingCondition>,
246}
247
248#[derive(Clone, Copy, Debug)]
249enum ComparisonOp {
250    Gt,
251    Gte,
252    Lt,
253    Lte,
254    Eq,
255    Neq,
256}
257
258impl HavingCondition {
259    /// Check if a value passes this condition
260    fn matches(&self, value: f64) -> bool {
261        match self.op {
262            ComparisonOp::Gt => value > self.threshold,
263            ComparisonOp::Gte => value >= self.threshold,
264            ComparisonOp::Lt => value < self.threshold,
265            ComparisonOp::Lte => value <= self.threshold,
266            ComparisonOp::Eq => (value - self.threshold).abs() < f64::EPSILON,
267            ComparisonOp::Neq => (value - self.threshold).abs() >= f64::EPSILON,
268        }
269    }
270}
271
272impl SimpleHavingFilter {
273    /// Create a filter with a single condition
274    fn single(agg_index: usize, op: ComparisonOp, threshold: f64) -> Self {
275        Self {
276            conditions: vec![HavingCondition {
277                agg_index,
278                op,
279                threshold,
280            }],
281        }
282    }
283
284    /// Combine two filters with AND semantics
285    fn and(mut self, other: Self) -> Self {
286        self.conditions.extend(other.conditions);
287        self
288    }
289}
290
291/// Simple aggregate type for fast aggregation path
292/// Supports COUNT, SUM, AVG, MIN, MAX (no DISTINCT, FILTER, ORDER BY, or expressions)
293#[derive(Clone)]
294enum SimpleAgg {
295    Count(Option<usize>), // COUNT(*) or COUNT(col) - stores column index for COUNT(col)
296    Sum(usize),           // SUM(col) - stores column index
297    Avg(usize),           // AVG(col) - stores column index
298    Min(usize),           // MIN(col) - stores column index
299    Max(usize),           // MAX(col) - stores column index
300}
301
302impl SimpleAgg {
303    #[inline]
304    fn count_includes_row(&self, row: &Row) -> bool {
305        match self {
306            Self::Count(None) => true,
307            Self::Count(Some(column_index)) => {
308                row.get(*column_index).is_some_and(|value| !value.is_null())
309            }
310            _ => false,
311        }
312    }
313}
314
315/// Outcome of trying the streaming aggregate path for a derived source.
316///
317/// A rejected optimization returns the original result source. This is
318/// essential: the caller must materialize that source once, rather than issue
319/// the same subquery a second time after an optimizer probe consumed a row.
320pub enum DerivedAggregationAttempt {
321    Applied(Box<dyn QueryResult>),
322    Rejected(Box<dyn QueryResult>),
323}
324
325struct DerivedAggregationPlan {
326    group_col_name: String,
327    group_col_idx: usize,
328    aggregations: Vec<SqlAggregateFunction>,
329    simple_aggs: Vec<SimpleAgg>,
330}
331
332/// Try to parse a simple HAVING clause for inline filtering
333/// Returns None if the HAVING is too complex for inline optimization
334/// Supports: single conditions and AND combinations
335fn try_parse_simple_having(
336    having: &Expression,
337    aggregations: &[SqlAggregateFunction],
338) -> Option<SimpleHavingFilter> {
339    // Handle AND expressions: parse both sides and combine
340    if let Expression::Infix(binop) = having {
341        if binop.operator.eq_ignore_ascii_case("AND") {
342            let left = try_parse_simple_having(&binop.left, aggregations)?;
343            let right = try_parse_simple_having(&binop.right, aggregations)?;
344            return Some(left.and(right));
345        }
346    }
347
348    // Handle single comparison: AGG(col) op value
349    try_parse_single_having_condition(having, aggregations)
350        .map(|(agg_index, op, threshold)| SimpleHavingFilter::single(agg_index, op, threshold))
351}
352
353/// Parse a single HAVING condition (not AND/OR)
354fn try_parse_single_having_condition(
355    having: &Expression,
356    aggregations: &[SqlAggregateFunction],
357) -> Option<(usize, ComparisonOp, f64)> {
358    // Handle comparison: AGG(col) op value
359    if let Expression::Infix(binop) = having {
360        let (op, threshold) = match binop.operator.as_str() {
361            ">" => (ComparisonOp::Gt, extract_numeric_value(&binop.right)?),
362            ">=" => (ComparisonOp::Gte, extract_numeric_value(&binop.right)?),
363            "<" => (ComparisonOp::Lt, extract_numeric_value(&binop.right)?),
364            "<=" => (ComparisonOp::Lte, extract_numeric_value(&binop.right)?),
365            "=" => (ComparisonOp::Eq, extract_numeric_value(&binop.right)?),
366            "!=" | "<>" => (ComparisonOp::Neq, extract_numeric_value(&binop.right)?),
367            _ => return None,
368        };
369
370        // Left side should be an aggregate function
371        if let Expression::FunctionCall(func) = &*binop.left {
372            let func_upper = func.function.to_uppercase();
373            if matches!(func_upper.as_str(), "SUM" | "COUNT" | "AVG" | "MIN" | "MAX") {
374                // Find matching aggregate
375                for (i, agg) in aggregations.iter().enumerate() {
376                    if agg.name.to_uppercase() == func_upper && !agg.distinct {
377                        // Check if column matches (for non-COUNT(*))
378                        let col_matches = if func_upper == "COUNT" {
379                            // COUNT(*) or COUNT(col)
380                            func.arguments.first().is_none_or(|arg| {
381                                matches!(arg, Expression::Star(_))
382                                    || match arg {
383                                        Expression::Identifier(id) => {
384                                            id.value_lower == agg.column_lower
385                                        }
386                                        _ => false,
387                                    }
388                            })
389                        } else {
390                            // SUM, AVG, etc. - check column
391                            func.arguments.first().is_some_and(|arg| match arg {
392                                Expression::Identifier(id) => id.value_lower == agg.column_lower,
393                                _ => false,
394                            })
395                        };
396
397                        if col_matches {
398                            return Some((i, op, threshold));
399                        }
400                    }
401                }
402            }
403        }
404    }
405
406    None
407}
408
409/// Extract numeric value from expression
410fn extract_numeric_value(expr: &Expression) -> Option<f64> {
411    match expr {
412        Expression::IntegerLiteral(lit) => Some(lit.value as f64),
413        Expression::FloatLiteral(lit) => Some(lit.value),
414        Expression::Prefix(unary) if unary.operator == "-" => {
415            extract_numeric_value(&unary.right).map(|v| -v)
416        }
417        _ => None,
418    }
419}
420
421/// Represents a grouping set for ROLLUP/CUBE operations
422/// Each grouping set specifies which columns are active (included in grouping)
423/// For ROLLUP(a, b), we get: [true, true], [true, false], [false, false]
424#[derive(Clone, Debug)]
425struct GroupingSet {
426    /// For each GROUP BY column, whether it's included in this grouping level
427    /// If false, the column value will be NULL in the output (rolled up)
428    active_columns: Vec<bool>,
429}
430
431/// Generate a canonical key for an expression for semantic matching.
432/// This ensures consistent matching regardless of token positions or formatting.
433/// All string-based keys are lowercased for case-insensitive matching.
434fn expression_canonical_key(expr: &Expression) -> String {
435    match expr {
436        Expression::Identifier(id) => id.value_lower.to_string(),
437        Expression::QualifiedIdentifier(qid) => {
438            format!("{}.{}", qid.qualifier.value_lower, qid.name.value_lower)
439        }
440        Expression::IntegerLiteral(lit) => format!("$pos:{}", lit.value),
441        Expression::FloatLiteral(lit) => format!("$float:{}", lit.value),
442        Expression::StringLiteral(lit) => format!("$str:{}", lit.value.to_lowercase()),
443        Expression::BooleanLiteral(lit) => format!("$bool:{}", lit.value),
444        Expression::FunctionCall(func) => {
445            // For function calls, build a canonical form
446            let args: Vec<String> = func
447                .arguments
448                .iter()
449                .map(expression_canonical_key)
450                .collect();
451            format!("{}({})", func.function.to_lowercase(), args.join(","))
452        }
453        Expression::Infix(bin) => {
454            // For infix/binary operations, build a canonical form
455            format!(
456                "({} {} {})",
457                expression_canonical_key(&bin.left),
458                bin.operator.to_lowercase(),
459                expression_canonical_key(&bin.right)
460            )
461        }
462        Expression::Prefix(un) => {
463            // For prefix/unary operations
464            format!(
465                "({}{})",
466                un.operator.to_lowercase(),
467                expression_canonical_key(&un.right)
468            )
469        }
470        Expression::Aliased(aliased) => {
471            // For aliased expressions, use the underlying expression
472            expression_canonical_key(&aliased.expression)
473        }
474        // For other complex expressions, use Display but lowercase for consistency
475        _ => format!("{}", expr).to_lowercase(),
476    }
477}
478
479/// Generate a canonical key for a GroupByItem for semantic matching.
480fn group_by_item_canonical_key(item: &GroupByItem) -> String {
481    match item {
482        GroupByItem::Column(name) => name.to_lowercase(),
483        GroupByItem::Position(pos) => format!("$pos:{}", pos),
484        GroupByItem::Expression { expr, .. } => expression_canonical_key(expr),
485    }
486}
487
488/// Represents a GROUP BY item - either a column reference or an expression
489#[derive(Clone, Debug)]
490#[allow(clippy::large_enum_variant)]
491pub enum GroupByItem {
492    /// Simple column reference by name
493    Column(String),
494    /// Positional reference like GROUP BY 1
495    Position(usize),
496    /// Complex expression that needs to be evaluated
497    Expression {
498        /// The expression to evaluate
499        expr: Expression,
500        /// Display name for the result column (from alias if available)
501        display_name: String,
502    },
503}
504
505/// Represents the source of a column in post-aggregation processing
506#[derive(Clone, Debug)]
507enum ColumnSource {
508    /// Column comes directly from aggregation result
509    AggColumn(String),
510    /// Column needs to be evaluated from an expression (boxed to reduce enum size)
511    Expression(Box<Expression>),
512    /// Correlated subquery expression that needs per-row evaluation with outer row context
513    CorrelatedExpression(Box<Expression>),
514    /// GROUPING() function - index is the GROUP BY column position (0-based)
515    GroupingFlag(usize),
516}
517
518/// Compute a hash for a group key (slice of Values)
519/// This avoids allocating Vec<Value> for each row
520/// OPTIMIZATION: Use AHasher for optimal hashing of Value types (strings, floats, JSON, etc.)
521/// Empirically tested to perform better than FxHasher for GROUP BY workloads
522/// Called on every row in GROUP BY, so performance is critical
523#[inline]
524fn hash_group_key(values: &[Value]) -> u64 {
525    let mut hasher = AHasher::default();
526    for v in values {
527        v.hash(&mut hasher);
528    }
529    hasher.finish()
530}
531
532#[inline]
533fn track_distinct_value(seen: &mut ValueSet, value: &Value) -> bool {
534    seen.insert(value.clone())
535}
536
537/// Group entry storing the key values and row indices
538struct GroupEntry {
539    /// The actual key values (stored once per group)
540    key_values: Vec<Value>,
541    /// Indices of rows belonging to this group
542    row_indices: Vec<usize>,
543}
544
545/// Represents an aggregate function call in a SELECT list
546#[derive(Clone, Debug)]
547pub struct SqlAggregateFunction {
548    /// Function name (COUNT, SUM, AVG, MIN, MAX, etc.)
549    pub name: String,
550    /// Column name the function operates on (* for COUNT(*))
551    pub column: String,
552    /// Pre-computed lowercase column name for index lookups
553    pub column_lower: String,
554    /// Alias for the result column
555    pub alias: Option<String>,
556    /// Whether DISTINCT is specified
557    pub distinct: bool,
558    /// Extra arguments (e.g., separator for STRING_AGG)
559    pub extra_args: Vec<Value>,
560    /// The expression to evaluate for each row (for SUM(val * 2), AVG(a + b), etc.)
561    /// If None, use column directly; if Some, evaluate expression first
562    pub expression: Option<Expression>,
563    /// ORDER BY clause for ordered-set aggregates like STRING_AGG
564    pub order_by: Vec<radixdb_sql::ast::OrderByExpression>,
565    /// FILTER clause condition - only accumulate rows where this is true
566    pub filter: Option<Expression>,
567    /// Whether this aggregate is hidden (only used for ORDER BY, not in SELECT)
568    pub hidden: bool,
569}
570
571impl SqlAggregateFunction {
572    /// Get the result column name
573    pub fn get_column_name(&self) -> String {
574        if let Some(ref alias) = self.alias {
575            alias.clone()
576        } else if self.column == "*" {
577            format!("{}(*)", self.name)
578        } else if self.extra_args.is_empty() {
579            format!("{}({})", self.name, self.column)
580        } else {
581            // Include extra arguments in column name (e.g., STRING_AGG(name, ' | '))
582            let args_str: Vec<String> = std::iter::once(self.column.clone())
583                .chain(self.extra_args.iter().map(|v| match v {
584                    Value::Text(s) => format!("'{}'", s),
585                    other => other.to_string(),
586                }))
587                .collect();
588            format!("{}({})", self.name, args_str.join(", "))
589        }
590    }
591
592    /// Get the expression name (without alias) for HAVING clause matching
593    /// This returns `SUM(price)` even if there's an alias like `AS total`
594    pub fn get_expression_name(&self) -> String {
595        if self.column == "*" {
596            format!("{}(*)", self.name)
597        } else {
598            format!("{}({})", self.name, self.column)
599        }
600    }
601}