1use crate::optimizer::ApplyOrder;
21use crate::{OptimizerConfig, OptimizerRule};
22
23use std::collections::HashSet;
24
25use datafusion_common::Result;
26use datafusion_common::tree_node::Transformed;
27use datafusion_expr::{Aggregate, Expr, LogicalPlan, LogicalPlanBuilder, Volatility};
28
29#[derive(Default, Debug)]
33pub struct EliminateGroupByConstant {}
34
35impl EliminateGroupByConstant {
36 pub fn new() -> Self {
37 Self {}
38 }
39}
40
41impl OptimizerRule for EliminateGroupByConstant {
42 fn supports_rewrite(&self) -> bool {
43 true
44 }
45
46 fn rewrite(
47 &self,
48 plan: LogicalPlan,
49 _config: &dyn OptimizerConfig,
50 ) -> Result<Transformed<LogicalPlan>> {
51 match plan {
52 LogicalPlan::Aggregate(aggregate) => {
53 let group_by_columns: HashSet<&datafusion_common::Column> = aggregate
55 .group_expr
56 .iter()
57 .filter_map(|expr| match expr {
58 Expr::Column(c) => Some(c),
59 _ => None,
60 })
61 .collect();
62
63 let (redundant, required): (Vec<_>, Vec<_>) = aggregate
64 .group_expr
65 .iter()
66 .partition(|expr| is_redundant_group_expr(expr, &group_by_columns));
67 if redundant.is_empty() || required.is_empty() {
75 return Ok(Transformed::no(LogicalPlan::Aggregate(aggregate)));
76 }
77
78 let simplified_aggregate = LogicalPlan::Aggregate(Aggregate::try_new(
79 aggregate.input,
80 required.into_iter().cloned().collect(),
81 aggregate.aggr_expr.clone(),
82 )?);
83
84 let projection_expr =
85 aggregate.group_expr.into_iter().chain(aggregate.aggr_expr);
86
87 let projection = LogicalPlanBuilder::from(simplified_aggregate)
88 .project(projection_expr)?
89 .build()?;
90
91 Ok(Transformed::yes(projection))
92 }
93 _ => Ok(Transformed::no(plan)),
94 }
95 }
96
97 fn name(&self) -> &str {
98 "eliminate_group_by_constant"
99 }
100
101 fn apply_order(&self) -> Option<ApplyOrder> {
102 Some(ApplyOrder::BottomUp)
103 }
104}
105
106fn is_redundant_group_expr(
111 expr: &Expr,
112 group_by_columns: &HashSet<&datafusion_common::Column>,
113) -> bool {
114 if matches!(expr, Expr::Column(_)) {
116 return false;
117 }
118 is_deterministic_of(expr, group_by_columns)
119}
120
121fn is_deterministic_of(
124 expr: &Expr,
125 known_columns: &HashSet<&datafusion_common::Column>,
126) -> bool {
127 match expr {
128 Expr::Alias(e) => is_deterministic_of(&e.expr, known_columns),
129 Expr::Column(c) => known_columns.contains(c),
130 Expr::Literal(_, _) => true,
131 Expr::BinaryExpr(e) => {
132 is_deterministic_of(&e.left, known_columns)
133 && is_deterministic_of(&e.right, known_columns)
134 }
135 Expr::ScalarFunction(e) => {
136 matches!(
137 e.func.signature().volatility,
138 Volatility::Immutable | Volatility::Stable
139 ) && e
140 .args
141 .iter()
142 .all(|arg| is_deterministic_of(arg, known_columns))
143 }
144 Expr::Cast(e) => is_deterministic_of(&e.expr, known_columns),
145 Expr::TryCast(e) => is_deterministic_of(&e.expr, known_columns),
146 Expr::Negative(e) => is_deterministic_of(e, known_columns),
147 _ => false,
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::OptimizerContext;
155 use crate::assert_optimized_plan_eq_snapshot;
156 use crate::test::*;
157
158 use arrow::datatypes::DataType;
159 use datafusion_expr::expr::ScalarFunction;
160 use datafusion_expr::{
161 ColumnarValue, LogicalPlanBuilder, ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl,
162 Signature, TypeSignature, col, lit,
163 };
164
165 use datafusion_functions_aggregate::expr_fn::count;
166
167 use std::sync::Arc;
168
169 macro_rules! assert_optimized_plan_equal {
170 (
171 $plan:expr,
172 @ $expected:literal $(,)?
173 ) => {{
174 let optimizer_ctx = OptimizerContext::new().with_max_passes(1);
175 let rules: Vec<Arc<dyn crate::OptimizerRule + Send + Sync>> = vec![Arc::new(EliminateGroupByConstant::new())];
176 assert_optimized_plan_eq_snapshot!(
177 optimizer_ctx,
178 rules,
179 $plan,
180 @ $expected,
181 )
182 }};
183 }
184
185 #[derive(Debug, PartialEq, Eq, Hash)]
186 struct ScalarUDFMock {
187 signature: Signature,
188 }
189
190 impl ScalarUDFMock {
191 fn new_with_volatility(volatility: Volatility) -> Self {
192 Self {
193 signature: Signature::new(TypeSignature::Any(1), volatility),
194 }
195 }
196 }
197
198 impl ScalarUDFImpl for ScalarUDFMock {
199 fn name(&self) -> &str {
200 "scalar_fn_mock"
201 }
202 fn signature(&self) -> &Signature {
203 &self.signature
204 }
205 fn return_type(&self, _args: &[DataType]) -> Result<DataType> {
206 Ok(DataType::Int32)
207 }
208 fn invoke_with_args(&self, _args: ScalarFunctionArgs) -> Result<ColumnarValue> {
209 unimplemented!()
210 }
211 }
212
213 #[test]
214 fn test_eliminate_gby_literal() -> Result<()> {
215 let scan = test_table_scan()?;
216 let plan = LogicalPlanBuilder::from(scan)
217 .aggregate(vec![col("a"), lit(1u32)], vec![count(col("c"))])?
218 .build()?;
219
220 assert_optimized_plan_equal!(plan, @r"
221 Projection: test.a, UInt32(1), count(test.c)
222 Aggregate: groupBy=[[test.a]], aggr=[[count(test.c)]]
223 TableScan: test
224 ")
225 }
226
227 #[test]
228 fn test_no_op_only_constant_with_aggregate() -> Result<()> {
229 let scan = test_table_scan()?;
230 let plan = LogicalPlanBuilder::from(scan)
231 .aggregate(vec![lit("test"), lit(123u32)], vec![count(col("c"))])?
232 .build()?;
233
234 assert_optimized_plan_equal!(plan, @r#"
235 Aggregate: groupBy=[[Utf8("test"), UInt32(123)]], aggr=[[count(test.c)]]
236 TableScan: test
237 "#)
238 }
239
240 #[test]
241 fn test_no_op_no_constants() -> Result<()> {
242 let scan = test_table_scan()?;
243 let plan = LogicalPlanBuilder::from(scan)
244 .aggregate(vec![col("a"), col("b")], vec![count(col("c"))])?
245 .build()?;
246
247 assert_optimized_plan_equal!(plan, @r"
248 Aggregate: groupBy=[[test.a, test.b]], aggr=[[count(test.c)]]
249 TableScan: test
250 ")
251 }
252
253 #[test]
254 fn test_no_op_only_constant() -> Result<()> {
255 let scan = test_table_scan()?;
256 let plan = LogicalPlanBuilder::from(scan)
257 .aggregate(vec![lit(123u32)], Vec::<Expr>::new())?
258 .build()?;
259
260 assert_optimized_plan_equal!(plan, @r"
261 Aggregate: groupBy=[[UInt32(123)]], aggr=[[]]
262 TableScan: test
263 ")
264 }
265
266 #[test]
267 fn test_eliminate_constant_with_alias() -> Result<()> {
268 let scan = test_table_scan()?;
269 let plan = LogicalPlanBuilder::from(scan)
270 .aggregate(
271 vec![lit(123u32).alias("const"), col("a")],
272 vec![count(col("c"))],
273 )?
274 .build()?;
275
276 assert_optimized_plan_equal!(plan, @r"
277 Projection: UInt32(123) AS const, test.a, count(test.c)
278 Aggregate: groupBy=[[test.a]], aggr=[[count(test.c)]]
279 TableScan: test
280 ")
281 }
282
283 #[test]
284 fn test_eliminate_scalar_fn_with_constant_arg() -> Result<()> {
285 let udf = ScalarUDF::new_from_impl(ScalarUDFMock::new_with_volatility(
286 Volatility::Immutable,
287 ));
288 let udf_expr =
289 Expr::ScalarFunction(ScalarFunction::new_udf(udf.into(), vec![lit(123u32)]));
290 let scan = test_table_scan()?;
291 let plan = LogicalPlanBuilder::from(scan)
292 .aggregate(vec![udf_expr, col("a")], vec![count(col("c"))])?
293 .build()?;
294
295 assert_optimized_plan_equal!(plan, @r"
296 Projection: scalar_fn_mock(UInt32(123)), test.a, count(test.c)
297 Aggregate: groupBy=[[test.a]], aggr=[[count(test.c)]]
298 TableScan: test
299 ")
300 }
301
302 #[test]
303 fn test_eliminate_deterministic_expr_of_group_by_column() -> Result<()> {
304 let scan = test_table_scan()?;
305 let plan = LogicalPlanBuilder::from(scan)
307 .aggregate(
308 vec![
309 col("a"),
310 col("a") - lit(1u32),
311 col("a") - lit(2u32),
312 col("a") - lit(3u32),
313 ],
314 vec![count(col("c"))],
315 )?
316 .build()?;
317
318 assert_optimized_plan_equal!(plan, @r"
319 Projection: test.a, test.a - UInt32(1), test.a - UInt32(2), test.a - UInt32(3), count(test.c)
320 Aggregate: groupBy=[[test.a]], aggr=[[count(test.c)]]
321 TableScan: test
322 ")
323 }
324
325 #[test]
326 fn test_no_eliminate_independent_columns() -> Result<()> {
327 let scan = test_table_scan()?;
329 let plan = LogicalPlanBuilder::from(scan)
330 .aggregate(vec![col("a"), col("b") - lit(1u32)], vec![count(col("c"))])?
331 .build()?;
332
333 assert_optimized_plan_equal!(plan, @r"
334 Aggregate: groupBy=[[test.a, test.b - UInt32(1)]], aggr=[[count(test.c)]]
335 TableScan: test
336 ")
337 }
338
339 #[test]
340 fn test_no_op_volatile_scalar_fn_with_constant_arg() -> Result<()> {
341 let udf = ScalarUDF::new_from_impl(ScalarUDFMock::new_with_volatility(
342 Volatility::Volatile,
343 ));
344 let udf_expr =
345 Expr::ScalarFunction(ScalarFunction::new_udf(udf.into(), vec![lit(123u32)]));
346 let scan = test_table_scan()?;
347 let plan = LogicalPlanBuilder::from(scan)
348 .aggregate(vec![udf_expr, col("a")], vec![count(col("c"))])?
349 .build()?;
350
351 assert_optimized_plan_equal!(plan, @r"
352 Aggregate: groupBy=[[scalar_fn_mock(UInt32(123)), test.a]], aggr=[[count(test.c)]]
353 TableScan: test
354 ")
355 }
356}