datafusion_optimizer/
replace_distinct_aggregate.rs1use crate::optimizer::{ApplyOrder, ApplyOrder::BottomUp};
21use crate::{OptimizerConfig, OptimizerRule};
22use std::sync::Arc;
23
24use datafusion_common::tree_node::Transformed;
25use datafusion_common::{Column, Dependency, Result};
26use datafusion_expr::expr_rewriter::normalize_cols;
27use datafusion_expr::utils::expand_wildcard;
28use datafusion_expr::{Aggregate, Distinct, DistinctOn, Expr, LogicalPlan};
29use datafusion_expr::{ExprFunctionExt, Limit, LogicalPlanBuilder, col, lit};
30
31#[derive(Default, Debug)]
69pub struct ReplaceDistinctWithAggregate {}
70
71impl ReplaceDistinctWithAggregate {
72 #[expect(missing_docs)]
73 pub fn new() -> Self {
74 Self {}
75 }
76}
77
78impl OptimizerRule for ReplaceDistinctWithAggregate {
79 fn supports_rewrite(&self) -> bool {
80 true
81 }
82
83 fn rewrite(
84 &self,
85 plan: LogicalPlan,
86 config: &dyn OptimizerConfig,
87 ) -> Result<Transformed<LogicalPlan>> {
88 match plan {
89 LogicalPlan::Distinct(Distinct::All(input)) => {
90 let group_expr = expand_wildcard(input.schema(), &input, None)?;
91
92 if group_expr.is_empty() {
93 return Ok(Transformed::yes(LogicalPlan::Limit(Limit {
96 skip: None,
97 fetch: Some(Box::new(lit(1i64))),
98 input,
99 })));
100 }
101
102 let field_count = input.schema().fields().len();
103 for dep in input.schema().functional_dependencies().iter() {
104 if dep.mode == Dependency::Single
111 && dep.source_indices.len() >= field_count
112 && dep.source_indices[..field_count]
113 .iter()
114 .enumerate()
115 .all(|(idx, f_idx)| idx == *f_idx)
116 {
117 return Ok(Transformed::yes(Arc::unwrap_or_clone(input)));
118 }
119 }
120
121 let aggr_plan = LogicalPlan::Aggregate(Aggregate::try_new(
123 input,
124 group_expr,
125 vec![],
126 )?);
127 Ok(Transformed::yes(aggr_plan))
128 }
129 LogicalPlan::Distinct(Distinct::On(DistinctOn {
130 select_expr,
131 on_expr,
132 sort_expr,
133 input,
134 schema,
135 })) => {
136 let expr_cnt = on_expr.len();
137
138 let first_value_udaf: Arc<datafusion_expr::AggregateUDF> =
140 config.function_registry().unwrap().udaf("first_value")?;
141 let aggr_expr = select_expr.into_iter().map(|e| {
142 if let Some(order_by) = &sort_expr {
143 first_value_udaf
144 .call(vec![e])
145 .order_by(order_by.clone())
146 .build()
147 .unwrap()
149 } else {
150 first_value_udaf.call(vec![e])
151 }
152 });
153
154 let aggr_expr = normalize_cols(aggr_expr, input.as_ref())?;
155 let group_expr = normalize_cols(on_expr, input.as_ref())?;
156
157 let plan = LogicalPlan::Aggregate(Aggregate::try_new(
159 input, group_expr, aggr_expr,
160 )?);
161 let lpb = LogicalPlanBuilder::from(plan);
164
165 let plan = if let Some(mut sort_expr) = sort_expr {
166 sort_expr.truncate(expr_cnt);
172
173 lpb.sort(sort_expr)?.build()?
174 } else {
175 lpb.build()?
176 };
177
178 let project_exprs = plan
182 .schema()
183 .iter()
184 .skip(expr_cnt)
185 .zip(schema.iter())
186 .map(|((new_qualifier, new_field), (old_qualifier, old_field))| {
187 col(Column::from((new_qualifier, new_field)))
188 .alias_qualified(old_qualifier.cloned(), old_field.name())
189 })
190 .collect::<Vec<Expr>>();
191
192 let plan = LogicalPlanBuilder::from(plan)
193 .project(project_exprs)?
194 .build()?;
195
196 Ok(Transformed::yes(plan))
197 }
198 _ => Ok(Transformed::no(plan)),
199 }
200 }
201
202 fn name(&self) -> &str {
203 "replace_distinct_aggregate"
204 }
205
206 fn apply_order(&self) -> Option<ApplyOrder> {
207 Some(BottomUp)
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use crate::assert_optimized_plan_eq_snapshot;
214 use crate::replace_distinct_aggregate::ReplaceDistinctWithAggregate;
215 use crate::test::*;
216 use arrow::datatypes::{Fields, Schema};
217 use std::sync::Arc;
218
219 use crate::OptimizerContext;
220 use datafusion_common::Result;
221 use datafusion_expr::{
222 Expr, col, logical_plan::builder::LogicalPlanBuilder, table_scan,
223 };
224 use datafusion_functions_aggregate::sum::sum;
225
226 macro_rules! assert_optimized_plan_equal {
227 (
228 $plan:expr,
229 @ $expected:literal $(,)?
230 ) => {{
231 let optimizer_ctx = OptimizerContext::new().with_max_passes(1);
232 let rules: Vec<Arc<dyn crate::OptimizerRule + Send + Sync>> = vec![Arc::new(ReplaceDistinctWithAggregate::new())];
233 assert_optimized_plan_eq_snapshot!(
234 optimizer_ctx,
235 rules,
236 $plan,
237 @ $expected,
238 )
239 }};
240 }
241
242 #[test]
243 fn eliminate_redundant_distinct_simple() -> Result<()> {
244 let table_scan = test_table_scan().unwrap();
245 let plan = LogicalPlanBuilder::from(table_scan)
246 .aggregate(vec![col("c")], Vec::<Expr>::new())?
247 .project(vec![col("c")])?
248 .distinct()?
249 .build()?;
250
251 assert_optimized_plan_equal!(plan, @r"
252 Projection: test.c
253 Aggregate: groupBy=[[test.c]], aggr=[[]]
254 TableScan: test
255 ")
256 }
257
258 #[test]
259 fn eliminate_redundant_distinct_pair() -> Result<()> {
260 let table_scan = test_table_scan().unwrap();
261 let plan = LogicalPlanBuilder::from(table_scan)
262 .aggregate(vec![col("a"), col("b")], Vec::<Expr>::new())?
263 .project(vec![col("a"), col("b")])?
264 .distinct()?
265 .build()?;
266
267 assert_optimized_plan_equal!(plan, @r"
268 Projection: test.a, test.b
269 Aggregate: groupBy=[[test.a, test.b]], aggr=[[]]
270 TableScan: test
271 ")
272 }
273
274 #[test]
275 fn do_not_eliminate_distinct() -> Result<()> {
276 let table_scan = test_table_scan().unwrap();
277 let plan = LogicalPlanBuilder::from(table_scan)
278 .project(vec![col("a"), col("b")])?
279 .distinct()?
280 .build()?;
281
282 assert_optimized_plan_equal!(plan, @r"
283 Aggregate: groupBy=[[test.a, test.b]], aggr=[[]]
284 Projection: test.a, test.b
285 TableScan: test
286 ")
287 }
288
289 #[test]
290 fn do_not_eliminate_distinct_with_aggr() -> Result<()> {
291 let table_scan = test_table_scan().unwrap();
292 let plan = LogicalPlanBuilder::from(table_scan)
293 .aggregate(vec![col("a"), col("b"), col("c")], vec![sum(col("c"))])?
294 .project(vec![col("a"), col("b")])?
295 .distinct()?
296 .build()?;
297
298 assert_optimized_plan_equal!(plan, @r"
299 Aggregate: groupBy=[[test.a, test.b]], aggr=[[]]
300 Projection: test.a, test.b
301 Aggregate: groupBy=[[test.a, test.b, test.c]], aggr=[[sum(test.c)]]
302 TableScan: test
303 ")
304 }
305
306 #[test]
307 fn use_limit_1_when_no_columns() -> Result<()> {
308 let plan = table_scan(Some("test"), &Schema::new(Fields::empty()), None)?
309 .distinct()?
310 .build()?;
311
312 assert_optimized_plan_equal!(plan, @r"
313 Limit: skip=0, fetch=1
314 TableScan: test
315 ")
316 }
317}