datafusion_optimizer/simplify_expressions/simplify_predicates.rs
1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Simplifies predicates by reducing redundant or overlapping conditions.
19//!
20//! This module provides functionality to optimize logical predicates used in query planning
21//! by eliminating redundant conditions, thus reducing the number of predicates to evaluate.
22//! Unlike the simplifier in `simplify_expressions/simplify_exprs.rs`, which focuses on
23//! general expression simplification (e.g., constant folding and algebraic simplifications),
24//! this module specifically targets predicate optimization by handling containment relationships.
25//! For example, it can simplify `x > 5 AND x > 6` to just `x > 6`, as the latter condition
26//! encompasses the former, resulting in fewer checks during query execution.
27
28use datafusion_common::{Column, Result, ScalarValue};
29use datafusion_expr::{BinaryExpr, Expr, Operator};
30use std::collections::BTreeMap;
31
32/// Simplifies a list of predicates by removing redundancies.
33///
34/// This function takes a vector of predicate expressions and groups them by the column they reference.
35/// Predicates that reference a single column and are comparison operations (e.g., >, >=, <, <=, =)
36/// are analyzed to remove redundant conditions. For instance, `x > 5 AND x > 6` is simplified to
37/// `x > 6`. Other predicates that do not fit this pattern are retained as-is.
38///
39/// # Arguments
40/// * `predicates` - A vector of `Expr` representing the predicates to simplify.
41///
42/// # Returns
43/// A `Result` containing a vector of simplified `Expr` predicates.
44pub fn simplify_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
45 // Early return for simple cases
46 if predicates.len() <= 1 {
47 return Ok(predicates);
48 }
49
50 // Group predicates by their column reference
51 let mut column_predicates: BTreeMap<Column, Vec<Expr>> = BTreeMap::new();
52 let mut other_predicates = Vec::new();
53
54 for pred in predicates {
55 match &pred {
56 Expr::BinaryExpr(BinaryExpr {
57 left,
58 op:
59 Operator::Gt
60 | Operator::GtEq
61 | Operator::Lt
62 | Operator::LtEq
63 | Operator::Eq,
64 right,
65 }) => {
66 if let (Some(col), Some(_)) =
67 (extract_column_from_expr(left), right.as_literal())
68 {
69 column_predicates.entry(col).or_default().push(pred);
70 } else if let (Some(_), Some(col)) =
71 (left.as_literal(), extract_column_from_expr(right))
72 {
73 column_predicates.entry(col).or_default().push(pred);
74 } else {
75 other_predicates.push(pred);
76 }
77 }
78 _ => other_predicates.push(pred),
79 }
80 }
81
82 // Process each column's predicates to remove redundancies
83 let mut result = other_predicates;
84 for (_, preds) in column_predicates {
85 let simplified = simplify_column_predicates(preds)?;
86 result.extend(simplified);
87 }
88
89 Ok(result)
90}
91
92/// Simplifies predicates related to a single column.
93///
94/// This function processes a list of predicates that all reference the same column and
95/// simplifies them based on their operators. It groups predicates into greater-than (>, >=),
96/// less-than (<, <=), and equality (=) categories, then selects the most restrictive condition
97/// in each category to reduce redundancy. For example, among `x > 5` and `x > 6`, only `x > 6`
98/// is retained as it is more restrictive.
99///
100/// # Arguments
101/// * `predicates` - A vector of `Expr` representing predicates for a single column.
102///
103/// # Returns
104/// A `Result` containing a vector of simplified `Expr` predicates for the column.
105fn simplify_column_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
106 if predicates.len() <= 1 {
107 return Ok(predicates);
108 }
109
110 // Group by operator type, but combining similar operators
111 let mut greater_predicates = Vec::new(); // Combines > and >=
112 let mut less_predicates = Vec::new(); // Combines < and <=
113 let mut eq_predicates = Vec::new();
114
115 for pred in predicates {
116 match &pred {
117 Expr::BinaryExpr(BinaryExpr { left: _, op, right }) => {
118 match (op, right.as_literal().is_some()) {
119 (Operator::Gt, true)
120 | (Operator::Lt, false)
121 | (Operator::GtEq, true)
122 | (Operator::LtEq, false) => greater_predicates.push(pred),
123 (Operator::Lt, true)
124 | (Operator::Gt, false)
125 | (Operator::LtEq, true)
126 | (Operator::GtEq, false) => less_predicates.push(pred),
127 (Operator::Eq, _) => eq_predicates.push(pred),
128 _ => unreachable!("Unexpected operator: {}", op),
129 }
130 }
131 _ => unreachable!("Unexpected predicate {}", pred.to_string()),
132 }
133 }
134
135 let mut result = Vec::new();
136
137 if !eq_predicates.is_empty() {
138 // If there are many equality predicates, we can only keep one if they are all the same
139 if eq_predicates.len() == 1
140 || eq_predicates.iter().all(|e| e == &eq_predicates[0])
141 {
142 result.push(eq_predicates.pop().unwrap());
143 } else {
144 // If they are not the same, add a false predicate
145 result.push(Expr::Literal(ScalarValue::Boolean(Some(false)), None));
146 }
147 }
148
149 // Handle all greater-than-style predicates (keep the most restrictive - highest value)
150 if !greater_predicates.is_empty() {
151 if let Some(most_restrictive) =
152 find_most_restrictive_predicate(&greater_predicates, true)?
153 {
154 result.push(most_restrictive);
155 } else {
156 result.extend(greater_predicates);
157 }
158 }
159
160 // Handle all less-than-style predicates (keep the most restrictive - lowest value)
161 if !less_predicates.is_empty() {
162 if let Some(most_restrictive) =
163 find_most_restrictive_predicate(&less_predicates, false)?
164 {
165 result.push(most_restrictive);
166 } else {
167 result.extend(less_predicates);
168 }
169 }
170
171 Ok(result)
172}
173
174/// Finds the most restrictive predicate from a list based on literal values.
175///
176/// This function iterates through a list of predicates to identify the most restrictive one
177/// by comparing their literal values. For greater-than predicates, the highest value is most
178/// restrictive, while for less-than predicates, the lowest value is most restrictive.
179///
180/// # Arguments
181/// * `predicates` - A slice of `Expr` representing predicates to compare.
182/// * `find_greater` - A boolean indicating whether to find the highest value (true for >, >=)
183/// or the lowest value (false for <, <=).
184///
185/// # Returns
186/// A `Result` containing an `Option<Expr>` with the most restrictive predicate, if any.
187fn find_most_restrictive_predicate(
188 predicates: &[Expr],
189 find_greater: bool,
190) -> Result<Option<Expr>> {
191 if predicates.is_empty() {
192 return Ok(None);
193 }
194
195 let mut most_restrictive_idx = 0;
196 let mut best_value: Option<&ScalarValue> = None;
197
198 for (idx, pred) in predicates.iter().enumerate() {
199 if let Expr::BinaryExpr(BinaryExpr { left, op, right }) = pred {
200 // Extract the literal value based on which side has it
201 let scalar_value = match (right.as_literal(), left.as_literal()) {
202 (Some(scalar), _) => Some(scalar),
203 (_, Some(scalar)) => Some(scalar),
204 _ => None,
205 };
206
207 if let Some(scalar) = scalar_value {
208 if let Some(current_best) = best_value {
209 let comparison = scalar.try_cmp(current_best)?;
210 let is_better = if find_greater {
211 comparison == std::cmp::Ordering::Greater
212 || (comparison == std::cmp::Ordering::Equal
213 && op == &Operator::Gt)
214 } else {
215 comparison == std::cmp::Ordering::Less
216 || (comparison == std::cmp::Ordering::Equal
217 && op == &Operator::Lt)
218 };
219
220 if is_better {
221 best_value = Some(scalar);
222 most_restrictive_idx = idx;
223 }
224 } else {
225 best_value = Some(scalar);
226 most_restrictive_idx = idx;
227 }
228 }
229 }
230 }
231
232 Ok(Some(predicates[most_restrictive_idx].clone()))
233}
234
235/// Extracts a column reference from an expression, if present.
236///
237/// This function checks if the given expression is a column reference or contains one,
238/// such as within a cast operation. It returns the `Column` if found.
239///
240/// # Arguments
241/// * `expr` - A reference to an `Expr` to inspect for a column reference.
242///
243/// # Returns
244/// An `Option<Column>` containing the column reference if found, otherwise `None`.
245fn extract_column_from_expr(expr: &Expr) -> Option<Column> {
246 match expr {
247 Expr::Column(col) => Some(col.clone()),
248 _ => None,
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use arrow::datatypes::DataType;
256 use datafusion_expr::{cast, col, lit};
257
258 #[test]
259 fn test_simplify_predicates_with_cast() {
260 // Test that predicates on cast expressions are not grouped with predicates on the raw column
261 // a < 5 AND CAST(a AS varchar) < 'abc' AND a < 6
262 // Should simplify to:
263 // a < 5 AND CAST(a AS varchar) < 'abc'
264
265 let predicates = vec![
266 col("a").lt(lit(5i32)),
267 cast(col("a"), DataType::Utf8).lt(lit("abc")),
268 col("a").lt(lit(6i32)),
269 ];
270
271 let result = simplify_predicates(predicates).unwrap();
272
273 // Should have 2 predicates: a < 5 and CAST(a AS varchar) < 'abc'
274 assert_eq!(result.len(), 2);
275
276 // Check that the cast predicate is preserved
277 let has_cast_predicate = result.iter().any(|p| {
278 matches!(p, Expr::BinaryExpr(BinaryExpr {
279 left,
280 op: Operator::Lt,
281 right
282 }) if matches!(left.as_ref(), Expr::Cast(_)) && right == &Box::new(lit("abc")))
283 });
284 assert!(has_cast_predicate, "Cast predicate should be preserved");
285
286 // Check that we have the more restrictive column predicate (a < 5)
287 let has_column_predicate = result.iter().any(|p| {
288 matches!(p, Expr::BinaryExpr(BinaryExpr {
289 left,
290 op: Operator::Lt,
291 right
292 }) if left == &Box::new(col("a")) && right == &Box::new(lit(5i32)))
293 });
294 assert!(has_column_predicate, "Should have a < 5 predicate");
295 }
296
297 #[test]
298 fn test_extract_column_ignores_cast() {
299 // Test that extract_column_from_expr does not extract columns from cast expressions
300 let cast_expr = cast(col("a"), DataType::Utf8);
301 assert_eq!(extract_column_from_expr(&cast_expr), None);
302
303 // Test that it still extracts from direct column references
304 let col_expr = col("a");
305 assert_eq!(extract_column_from_expr(&col_expr), Some(Column::from("a")));
306 }
307
308 #[test]
309 fn test_simplify_predicates_direct_columns_only() {
310 // Test that only predicates on direct columns are simplified together
311 let predicates = vec![
312 col("a").lt(lit(5i32)),
313 col("a").lt(lit(3i32)),
314 col("b").gt(lit(10i32)),
315 col("b").gt(lit(20i32)),
316 ];
317
318 let result = simplify_predicates(predicates).unwrap();
319
320 // Should have 2 predicates: a < 3 and b > 20 (most restrictive for each column)
321 assert_eq!(result.len(), 2);
322
323 // Check for a < 3
324 let has_a_predicate = result.iter().any(|p| {
325 matches!(p, Expr::BinaryExpr(BinaryExpr {
326 left,
327 op: Operator::Lt,
328 right
329 }) if left == &Box::new(col("a")) && right == &Box::new(lit(3i32)))
330 });
331 assert!(has_a_predicate, "Should have a < 3 predicate");
332
333 // Check for b > 20
334 let has_b_predicate = result.iter().any(|p| {
335 matches!(p, Expr::BinaryExpr(BinaryExpr {
336 left,
337 op: Operator::Gt,
338 right
339 }) if left == &Box::new(col("b")) && right == &Box::new(lit(20i32)))
340 });
341 assert!(has_b_predicate, "Should have b > 20 predicate");
342 }
343}