datafusion_physical_optimizer/
aggregate_statistics.rs1use datafusion_common::Result;
20use datafusion_common::config::ConfigOptions;
21use datafusion_common::scalar::ScalarValue;
22use datafusion_common::tree_node::{Transformed, TransformedResult, TreeNode};
23use datafusion_physical_plan::aggregates::{
24 AggregateExec, AggregateInputMode, AggregateMode,
25};
26use datafusion_physical_plan::placeholder_row::PlaceholderRowExec;
27use datafusion_physical_plan::projection::{ProjectionExec, ProjectionExpr};
28use datafusion_physical_plan::statistics::{StatisticsArgs, StatisticsContext};
29use datafusion_physical_plan::udaf::{
30 AggregateFunctionExpr, StatisticsArgs as PlanStatisticsArgs,
31};
32use datafusion_physical_plan::{ExecutionPlan, expressions};
33use std::sync::Arc;
34
35use crate::PhysicalOptimizerRule;
36
37#[derive(Default, Debug)]
39pub struct AggregateStatistics {}
40
41impl AggregateStatistics {
42 #[expect(missing_docs)]
43 pub fn new() -> Self {
44 Self {}
45 }
46}
47
48impl PhysicalOptimizerRule for AggregateStatistics {
49 #[cfg_attr(feature = "recursive_protection", recursive::recursive)]
50 #[expect(clippy::allow_attributes)] #[allow(clippy::only_used_in_recursion)] fn optimize(
53 &self,
54 plan: Arc<dyn ExecutionPlan>,
55 config: &ConfigOptions,
56 ) -> Result<Arc<dyn ExecutionPlan>> {
57 if let Some(partial_agg_exec) = take_optimizable(&plan) {
58 let partial_agg_exec = partial_agg_exec
59 .downcast_ref::<AggregateExec>()
60 .expect("take_optimizable() ensures that this is a AggregateExec");
61 let stats = StatisticsContext::new()
62 .compute(partial_agg_exec.input().as_ref(), &StatisticsArgs::new())?;
63 let mut projections = vec![];
64 for expr in partial_agg_exec.aggr_expr() {
65 let field = expr.field();
66 let args = expr.expressions();
67 let statistics_args = PlanStatisticsArgs {
68 statistics: &stats,
69 return_type: field.data_type(),
70 is_distinct: expr.is_distinct(),
71 exprs: args.as_slice(),
72 };
73 if let Some((optimizable_statistic, name)) =
74 take_optimizable_value_from_statistics(&statistics_args, expr)
75 {
76 projections.push(ProjectionExpr {
77 expr: expressions::lit(optimizable_statistic),
78 alias: name.to_owned(),
79 });
80 } else {
81 break;
83 }
84 }
85
86 if projections.len() == partial_agg_exec.aggr_expr().len() {
88 Ok(Arc::new(ProjectionExec::try_new(
90 projections,
91 Arc::new(PlaceholderRowExec::new(plan.schema())),
92 )?))
93 } else {
94 plan.map_children(|child| {
95 self.optimize(child, config).map(Transformed::yes)
96 })
97 .data()
98 }
99 } else {
100 plan.map_children(|child| self.optimize(child, config).map(Transformed::yes))
101 .data()
102 }
103 }
104
105 fn name(&self) -> &str {
106 "aggregate_statistics"
107 }
108
109 fn schema_check(&self) -> bool {
111 false
112 }
113}
114
115fn take_optimizable(plan: &Arc<dyn ExecutionPlan>) -> Option<Arc<dyn ExecutionPlan>> {
120 let agg_exec = plan.downcast_ref::<AggregateExec>()?;
121
122 if matches!(
123 agg_exec.mode(),
124 AggregateMode::Single | AggregateMode::SinglePartitioned
125 ) && agg_exec.group_expr().is_empty()
126 && agg_exec.filter_expr().iter().all(|e| e.is_none())
127 {
128 return Some(Arc::clone(plan));
129 }
130
131 if agg_exec.mode().input_mode() == AggregateInputMode::Partial
132 && agg_exec.group_expr().is_empty()
133 {
134 let mut child = Arc::clone(agg_exec.input());
135 loop {
136 if let Some(partial_agg_exec) = child.downcast_ref::<AggregateExec>()
137 && partial_agg_exec.mode().input_mode() == AggregateInputMode::Raw
138 && partial_agg_exec.group_expr().is_empty()
139 && partial_agg_exec.filter_expr().iter().all(|e| e.is_none())
140 {
141 return Some(child);
142 }
143 if let [childrens_child] = child.children().as_slice() {
144 child = Arc::clone(childrens_child);
145 } else {
146 break;
147 }
148 }
149 }
150 None
151}
152
153fn take_optimizable_value_from_statistics(
155 statistics_args: &PlanStatisticsArgs,
156 agg_expr: &AggregateFunctionExpr,
157) -> Option<(ScalarValue, String)> {
158 let value = agg_expr.fun().value_from_stats(statistics_args);
159 value.map(|val| (val, agg_expr.name().to_string()))
160}
161
162