Skip to main content

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}