blob: e811ce731310206ad12aacdf2506510126e9b4da [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Simplifies predicates by reducing redundant or overlapping conditions.
//!
//! This module provides functionality to optimize logical predicates used in query planning
//! by eliminating redundant conditions, thus reducing the number of predicates to evaluate.
//! Unlike the simplifier in `simplify_expressions/simplify_exprs.rs`, which focuses on
//! general expression simplification (e.g., constant folding and algebraic simplifications),
//! this module specifically targets predicate optimization by handling containment relationships.
//! For example, it can simplify `x > 5 AND x > 6` to just `x > 6`, as the latter condition
//! encompasses the former, resulting in fewer checks during query execution.
use datafusion_common::{Column, Result, ScalarValue};
use datafusion_expr::{BinaryExpr, Expr, Operator};
use std::collections::BTreeMap;
/// Simplifies a list of predicates by removing redundancies.
///
/// This function takes a vector of predicate expressions and groups them by the column they reference.
/// Predicates that reference a single column and are comparison operations (e.g., >, >=, <, <=, =)
/// are analyzed to remove redundant conditions. For instance, `x > 5 AND x > 6` is simplified to
/// `x > 6`. Other predicates that do not fit this pattern are retained as-is.
///
/// # Arguments
/// * `predicates` - A vector of `Expr` representing the predicates to simplify.
///
/// # Returns
/// A `Result` containing a vector of simplified `Expr` predicates.
pub fn simplify_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
// Early return for simple cases
if predicates.len() <= 1 {
return Ok(predicates);
}
// Group predicates by their column reference
let mut column_predicates: BTreeMap<Column, Vec<Expr>> = BTreeMap::new();
let mut other_predicates = Vec::new();
for pred in predicates {
match &pred {
Expr::BinaryExpr(BinaryExpr {
left,
op:
Operator::Gt
| Operator::GtEq
| Operator::Lt
| Operator::LtEq
| Operator::Eq,
right,
}) => {
let left_col = extract_column_from_expr(left);
let right_col = extract_column_from_expr(right);
if let (Some(col), Some(_)) = (&left_col, right.as_literal()) {
column_predicates.entry(col.clone()).or_default().push(pred);
} else if let (Some(_), Some(col)) = (left.as_literal(), &right_col) {
column_predicates.entry(col.clone()).or_default().push(pred);
} else {
other_predicates.push(pred);
}
}
_ => other_predicates.push(pred),
}
}
// Process each column's predicates to remove redundancies
let mut result = other_predicates;
for (_, preds) in column_predicates {
let simplified = simplify_column_predicates(preds)?;
result.extend(simplified);
}
Ok(result)
}
/// Simplifies predicates related to a single column.
///
/// This function processes a list of predicates that all reference the same column and
/// simplifies them based on their operators. It groups predicates into greater-than (>, >=),
/// less-than (<, <=), and equality (=) categories, then selects the most restrictive condition
/// in each category to reduce redundancy. For example, among `x > 5` and `x > 6`, only `x > 6`
/// is retained as it is more restrictive.
///
/// # Arguments
/// * `predicates` - A vector of `Expr` representing predicates for a single column.
///
/// # Returns
/// A `Result` containing a vector of simplified `Expr` predicates for the column.
fn simplify_column_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
if predicates.len() <= 1 {
return Ok(predicates);
}
// Group by operator type, but combining similar operators
let mut greater_predicates = Vec::new(); // Combines > and >=
let mut less_predicates = Vec::new(); // Combines < and <=
let mut eq_predicates = Vec::new();
for pred in predicates {
match &pred {
Expr::BinaryExpr(BinaryExpr { left: _, op, right }) => {
match (op, right.as_literal().is_some()) {
(Operator::Gt, true)
| (Operator::Lt, false)
| (Operator::GtEq, true)
| (Operator::LtEq, false) => greater_predicates.push(pred),
(Operator::Lt, true)
| (Operator::Gt, false)
| (Operator::LtEq, true)
| (Operator::GtEq, false) => less_predicates.push(pred),
(Operator::Eq, _) => eq_predicates.push(pred),
_ => unreachable!("Unexpected operator: {}", op),
}
}
_ => unreachable!("Unexpected predicate {}", pred.to_string()),
}
}
let mut result = Vec::new();
if !eq_predicates.is_empty() {
// If there are many equality predicates, we can only keep one if they are all the same
if eq_predicates.len() == 1
|| eq_predicates.iter().all(|e| e == &eq_predicates[0])
{
result.push(eq_predicates.pop().unwrap());
} else {
// If they are not the same, add a false predicate
result.push(Expr::Literal(ScalarValue::Boolean(Some(false)), None));
}
}
// Handle all greater-than-style predicates (keep the most restrictive - highest value)
if !greater_predicates.is_empty() {
if let Some(most_restrictive) =
find_most_restrictive_predicate(&greater_predicates, true)?
{
result.push(most_restrictive);
} else {
result.extend(greater_predicates);
}
}
// Handle all less-than-style predicates (keep the most restrictive - lowest value)
if !less_predicates.is_empty() {
if let Some(most_restrictive) =
find_most_restrictive_predicate(&less_predicates, false)?
{
result.push(most_restrictive);
} else {
result.extend(less_predicates);
}
}
Ok(result)
}
/// Finds the most restrictive predicate from a list based on literal values.
///
/// This function iterates through a list of predicates to identify the most restrictive one
/// by comparing their literal values. For greater-than predicates, the highest value is most
/// restrictive, while for less-than predicates, the lowest value is most restrictive.
///
/// # Arguments
/// * `predicates` - A slice of `Expr` representing predicates to compare.
/// * `find_greater` - A boolean indicating whether to find the highest value (true for >, >=)
/// or the lowest value (false for <, <=).
///
/// # Returns
/// A `Result` containing an `Option<Expr>` with the most restrictive predicate, if any.
fn find_most_restrictive_predicate(
predicates: &[Expr],
find_greater: bool,
) -> Result<Option<Expr>> {
if predicates.is_empty() {
return Ok(None);
}
let mut most_restrictive_idx = 0;
let mut best_value: Option<&ScalarValue> = None;
for (idx, pred) in predicates.iter().enumerate() {
if let Expr::BinaryExpr(BinaryExpr { left, op, right }) = pred {
// Extract the literal value based on which side has it
let scalar_value = match (right.as_literal(), left.as_literal()) {
(Some(scalar), _) => Some(scalar),
(_, Some(scalar)) => Some(scalar),
_ => None,
};
if let Some(scalar) = scalar_value {
if let Some(current_best) = best_value {
let comparison = scalar.try_cmp(current_best)?;
let is_better = if find_greater {
comparison == std::cmp::Ordering::Greater
|| (comparison == std::cmp::Ordering::Equal
&& op == &Operator::Gt)
} else {
comparison == std::cmp::Ordering::Less
|| (comparison == std::cmp::Ordering::Equal
&& op == &Operator::Lt)
};
if is_better {
best_value = Some(scalar);
most_restrictive_idx = idx;
}
} else {
best_value = Some(scalar);
most_restrictive_idx = idx;
}
}
}
}
Ok(Some(predicates[most_restrictive_idx].clone()))
}
/// Extracts a column reference from an expression, if present.
///
/// This function checks if the given expression is a column reference or contains one,
/// such as within a cast operation. It returns the `Column` if found.
///
/// # Arguments
/// * `expr` - A reference to an `Expr` to inspect for a column reference.
///
/// # Returns
/// An `Option<Column>` containing the column reference if found, otherwise `None`.
fn extract_column_from_expr(expr: &Expr) -> Option<Column> {
match expr {
Expr::Column(col) => Some(col.clone()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::datatypes::DataType;
use datafusion_expr::{cast, col, lit};
#[test]
fn test_simplify_predicates_with_cast() {
// Test that predicates on cast expressions are not grouped with predicates on the raw column
// a < 5 AND CAST(a AS varchar) < 'abc' AND a < 6
// Should simplify to:
// a < 5 AND CAST(a AS varchar) < 'abc'
let predicates = vec![
col("a").lt(lit(5i32)),
cast(col("a"), DataType::Utf8).lt(lit("abc")),
col("a").lt(lit(6i32)),
];
let result = simplify_predicates(predicates).unwrap();
// Should have 2 predicates: a < 5 and CAST(a AS varchar) < 'abc'
assert_eq!(result.len(), 2);
// Check that the cast predicate is preserved
let has_cast_predicate = result.iter().any(|p| {
matches!(p, Expr::BinaryExpr(BinaryExpr {
left,
op: Operator::Lt,
right
}) if matches!(left.as_ref(), Expr::Cast(_)) && right == &Box::new(lit("abc")))
});
assert!(has_cast_predicate, "Cast predicate should be preserved");
// Check that we have the more restrictive column predicate (a < 5)
let has_column_predicate = result.iter().any(|p| {
matches!(p, Expr::BinaryExpr(BinaryExpr {
left,
op: Operator::Lt,
right
}) if left == &Box::new(col("a")) && right == &Box::new(lit(5i32)))
});
assert!(has_column_predicate, "Should have a < 5 predicate");
}
#[test]
fn test_extract_column_ignores_cast() {
// Test that extract_column_from_expr does not extract columns from cast expressions
let cast_expr = cast(col("a"), DataType::Utf8);
assert_eq!(extract_column_from_expr(&cast_expr), None);
// Test that it still extracts from direct column references
let col_expr = col("a");
assert_eq!(extract_column_from_expr(&col_expr), Some(Column::from("a")));
}
#[test]
fn test_simplify_predicates_direct_columns_only() {
// Test that only predicates on direct columns are simplified together
let predicates = vec![
col("a").lt(lit(5i32)),
col("a").lt(lit(3i32)),
col("b").gt(lit(10i32)),
col("b").gt(lit(20i32)),
];
let result = simplify_predicates(predicates).unwrap();
// Should have 2 predicates: a < 3 and b > 20 (most restrictive for each column)
assert_eq!(result.len(), 2);
// Check for a < 3
let has_a_predicate = result.iter().any(|p| {
matches!(p, Expr::BinaryExpr(BinaryExpr {
left,
op: Operator::Lt,
right
}) if left == &Box::new(col("a")) && right == &Box::new(lit(3i32)))
});
assert!(has_a_predicate, "Should have a < 3 predicate");
// Check for b > 20
let has_b_predicate = result.iter().any(|p| {
matches!(p, Expr::BinaryExpr(BinaryExpr {
left,
op: Operator::Gt,
right
}) if left == &Box::new(col("b")) && right == &Box::new(lit(20i32)))
});
assert!(has_b_predicate, "Should have b > 20 predicate");
}
}