datafusion_physical_optimizer/
topk_aggregation.rs1use std::sync::Arc;
21
22use crate::PhysicalOptimizerRule;
23use datafusion_common::Result;
24use datafusion_common::config::ConfigOptions;
25use datafusion_common::tree_node::{Transformed, TransformedResult, TreeNode};
26use datafusion_physical_expr::expressions::Column;
27use datafusion_physical_plan::ExecutionPlan;
28use datafusion_physical_plan::aggregates::LimitOptions;
29use datafusion_physical_plan::aggregates::{AggregateExec, topk_types_supported};
30use datafusion_physical_plan::execution_plan::CardinalityEffect;
31use datafusion_physical_plan::projection::ProjectionExec;
32use datafusion_physical_plan::sorts::sort::SortExec;
33use itertools::Itertools;
34
35#[derive(Debug)]
37pub struct TopKAggregation {}
38
39impl TopKAggregation {
40 pub fn new() -> Self {
42 Self {}
43 }
44
45 fn transform_agg(
46 aggr: &AggregateExec,
47 order_by: &str,
48 order_desc: bool,
49 nulls_first: bool,
50 limit: usize,
51 ) -> Option<Arc<dyn ExecutionPlan>> {
52 let (group_key, group_key_alias) =
54 aggr.group_expr().expr().iter().exactly_one().ok()?;
55 let kt = group_key.data_type(&aggr.input().schema()).ok()?;
56 let vt = if let Some((field, _)) = aggr.get_minmax_desc() {
57 field.data_type().clone()
58 } else {
59 kt.clone()
60 };
61 if !topk_types_supported(&kt, &vt) {
62 return None;
63 }
64 if aggr.filter_expr().iter().any(|e| e.is_some()) {
65 return None;
66 }
67
68 if let Some((field, desc)) = aggr.get_minmax_desc() {
70 let input_nullable = aggr
77 .aggr_expr()
78 .iter()
79 .exactly_one()
80 .ok()?
81 .expressions()
82 .into_iter()
83 .exactly_one()
84 .ok()?
85 .nullable(aggr.input_schema.as_ref())
86 .ok()?;
87 if nulls_first && input_nullable {
88 return None;
89 }
90 if desc != order_desc {
92 return None;
93 }
94 if order_by != field.name() {
96 return None;
97 }
98 } else if aggr.aggr_expr().is_empty() {
99 if order_by != group_key_alias {
101 return None;
102 }
103 } else {
104 return None;
106 }
107
108 let new_aggr = AggregateExec::with_new_limit_options(
110 aggr,
111 Some(LimitOptions::new_with_order(limit, order_desc)),
112 );
113 Some(Arc::new(new_aggr))
114 }
115
116 fn transform_sort(plan: &Arc<dyn ExecutionPlan>) -> Option<Arc<dyn ExecutionPlan>> {
117 let sort = plan.downcast_ref::<SortExec>()?;
118
119 let children = sort.children();
120 let child = children.into_iter().exactly_one().ok()?;
121 let order = sort.properties().output_ordering()?;
122 let order = order.iter().exactly_one().ok()?;
123 let order_desc = order.options.descending;
124 let nulls_first = order.options.nulls_first;
125 let order = order.expr.downcast_ref::<Column>()?;
126 let mut cur_col_name = order.name().to_string();
127 let limit = sort.fetch()?;
128
129 let mut cardinality_preserved = true;
130 let closure = |plan: Arc<dyn ExecutionPlan>| {
131 if !cardinality_preserved {
132 return Ok(Transformed::no(plan));
133 }
134 if let Some(aggr) = plan.downcast_ref::<AggregateExec>() {
135 match Self::transform_agg(
137 aggr,
138 &cur_col_name,
139 order_desc,
140 nulls_first,
141 limit,
142 ) {
143 None => cardinality_preserved = false,
144 Some(plan) => return Ok(Transformed::yes(plan)),
145 }
146 } else if let Some(proj) = plan.downcast_ref::<ProjectionExec>() {
147 for proj_expr in proj.expr() {
149 let Some(src_col) = proj_expr.expr.downcast_ref::<Column>() else {
150 continue;
151 };
152 if proj_expr.alias == cur_col_name {
153 cur_col_name = src_col.name().to_string();
154 }
155 }
156 } else {
157 match plan.cardinality_effect() {
159 CardinalityEffect::Equal | CardinalityEffect::GreaterEqual => {}
160 CardinalityEffect::Unknown | CardinalityEffect::LowerEqual => {
161 cardinality_preserved = false;
162 }
163 }
164 }
165 Ok(Transformed::no(plan))
166 };
167 let child = Arc::clone(child).transform_down(closure).data().ok()?;
168 let sort = SortExec::new(sort.expr().clone(), child)
169 .with_fetch(sort.fetch())
170 .with_preserve_partitioning(sort.preserve_partitioning());
171 Some(Arc::new(sort))
172 }
173}
174
175impl Default for TopKAggregation {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181impl PhysicalOptimizerRule for TopKAggregation {
182 fn optimize(
183 &self,
184 plan: Arc<dyn ExecutionPlan>,
185 config: &ConfigOptions,
186 ) -> Result<Arc<dyn ExecutionPlan>> {
187 if config.optimizer.enable_topk_aggregation {
188 plan.transform_down(|plan| {
189 Ok(if let Some(plan) = TopKAggregation::transform_sort(&plan) {
190 Transformed::yes(plan)
191 } else {
192 Transformed::no(plan)
193 })
194 })
195 .data()
196 } else {
197 Ok(plan)
198 }
199 }
200
201 fn name(&self) -> &str {
202 "LimitAggregation"
203 }
204
205 fn schema_check(&self) -> bool {
206 true
207 }
208}
209
210