blob: c76de942de805e417e9eadf3136f8242150e2df2 [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.
//! [`ExtractEquijoinPredicate`] identifies equality join (equijoin) predicates
use crate::optimizer::ApplyOrder;
use crate::{OptimizerConfig, OptimizerRule};
use datafusion_common::tree_node::Transformed;
use datafusion_common::{internal_err, DFSchema};
use datafusion_common::{NullEquality, Result};
use datafusion_expr::utils::split_conjunction_owned;
use datafusion_expr::utils::{can_hash, find_valid_equijoin_key_pair};
use datafusion_expr::{BinaryExpr, Expr, ExprSchemable, Join, LogicalPlan, Operator};
// equijoin predicate
type EquijoinPredicate = (Expr, Expr);
/// Optimizer that splits conjunctive join predicates into equijoin
/// predicates and (other) filter predicates.
///
/// Join algorithms are often highly optimized for equality predicates such as `x = y`,
/// often called `equijoin` predicates, so it is important to locate such predicates
/// and treat them specially.
///
/// For example, `SELECT ... FROM A JOIN B ON (A.x = B.y AND B.z > 50)`
/// has one equijoin predicate (`A.x = B.y`) and one filter predicate (`B.z > 50`).
/// See [find_valid_equijoin_key_pair] for more information on what predicates
/// are considered equijoins.
#[derive(Default, Debug)]
pub struct ExtractEquijoinPredicate;
impl ExtractEquijoinPredicate {
#[allow(missing_docs)]
pub fn new() -> Self {
Self {}
}
}
impl OptimizerRule for ExtractEquijoinPredicate {
fn supports_rewrite(&self) -> bool {
true
}
fn name(&self) -> &str {
"extract_equijoin_predicate"
}
fn apply_order(&self) -> Option<ApplyOrder> {
Some(ApplyOrder::BottomUp)
}
fn rewrite(
&self,
plan: LogicalPlan,
_config: &dyn OptimizerConfig,
) -> Result<Transformed<LogicalPlan>> {
match plan {
LogicalPlan::Join(Join {
left,
right,
mut on,
filter: Some(expr),
join_type,
join_constraint,
schema,
null_equality,
}) => {
let left_schema = left.schema();
let right_schema = right.schema();
let (equijoin_predicates, non_equijoin_expr) =
split_eq_and_noneq_join_predicate(expr, left_schema, right_schema)?;
// Equi-join operators like HashJoin support a special behavior
// that evaluates `NULL = NULL` as true instead of NULL. Therefore,
// we transform `t1.c1 IS NOT DISTINCT FROM t2.c1` into an equi-join
// and set the `NullEquality` configuration in the join operator.
// This allows certain queries to use Hash Join instead of
// Nested Loop Join, resulting in better performance.
//
// Only convert when there are NO equijoin predicates, to be conservative.
if on.is_empty()
&& equijoin_predicates.is_empty()
&& non_equijoin_expr.is_some()
{
// SAFETY: checked in the outer `if`
let expr = non_equijoin_expr.clone().unwrap();
let (equijoin_predicates, non_equijoin_expr) =
split_is_not_distinct_from_and_other_join_predicate(
expr,
left_schema,
right_schema,
)?;
if !equijoin_predicates.is_empty() {
on.extend(equijoin_predicates);
return Ok(Transformed::yes(LogicalPlan::Join(Join {
left,
right,
on,
filter: non_equijoin_expr,
join_type,
join_constraint,
schema,
// According to `is not distinct from`'s semantics, it's
// safe to override it
null_equality: NullEquality::NullEqualsNull,
})));
}
}
if !equijoin_predicates.is_empty() {
on.extend(equijoin_predicates);
Ok(Transformed::yes(LogicalPlan::Join(Join {
left,
right,
on,
filter: non_equijoin_expr,
join_type,
join_constraint,
schema,
null_equality,
})))
} else {
Ok(Transformed::no(LogicalPlan::Join(Join {
left,
right,
on,
filter: non_equijoin_expr,
join_type,
join_constraint,
schema,
null_equality,
})))
}
}
_ => Ok(Transformed::no(plan)),
}
}
}
/// Splits an ANDed filter expression into equijoin predicates and remaining filters.
/// Returns all equijoin predicates and the remaining filters combined with AND.
///
/// # Example
///
/// For the expression `a.id = b.id AND a.x > 10 AND b.x > b.id`, this function will extract `a.id = b.id` as an equijoin predicate.
///
/// It first splits the ANDed sub-expressions:
/// - expr1: a.id = b.id
/// - expr2: a.x > 10
/// - expr3: b.x > b.id
///
/// Then, it filters out the equijoin predicates and collects the non-equality expressions.
/// The equijoin condition is:
/// - It is an equality expression like `lhs == rhs`
/// - All column references in `lhs` are from the left schema, and all in `rhs` are from the right schema
///
/// According to the above rule, `expr1` is the equijoin predicate, while `expr2` and `expr3` are not.
/// The function returns Ok(\[expr1\], Some(expr2 AND expr3))
fn split_eq_and_noneq_join_predicate(
filter: Expr,
left_schema: &DFSchema,
right_schema: &DFSchema,
) -> Result<(Vec<EquijoinPredicate>, Option<Expr>)> {
split_op_and_other_join_predicates(filter, left_schema, right_schema, Operator::Eq)
}
/// See `split_eq_and_noneq_join_predicate`'s comment for the idea. This function
/// is splitting out `is not distinct from` expressions instead of equal exprs.
/// The `is not distinct from` exprs will be return as `EquijoinPredicate`.
///
/// # Example
/// - Input: `a.id IS NOT DISTINCT FROM b.id AND a.x > 10 AND b.x > b.id`
/// - Output from this splitter: `Ok([a.id, b.id], Some((a.x > 10) AND (b.x > b.id)))`
///
/// # Note
/// Caller should be cautious -- `is not distinct from` is not equivalent to an
/// equal expression; the caller is responsible for correctly setting the
/// `nulls equals nulls` property in the join operator (if it supports it) to
/// make the transformation valid.
///
/// For the above example: in downstream, a valid plan that uses the extracted
/// equijoin keys should look like:
///
/// HashJoin
/// - on: `a.id = b.id` (equality)
/// - join_filter: `(a.x > 10) AND (b.x > b.id)`
/// - nulls_equals_null: `true`
///
/// This reflects that `IS NOT DISTINCT FROM` treats `NULL = NULL` as true and
/// thus requires setting `NullEquality::NullEqualsNull` in the join operator to
/// preserve semantics while enabling an equi-join implementation (e.g., HashJoin).
fn split_is_not_distinct_from_and_other_join_predicate(
filter: Expr,
left_schema: &DFSchema,
right_schema: &DFSchema,
) -> Result<(Vec<EquijoinPredicate>, Option<Expr>)> {
split_op_and_other_join_predicates(
filter,
left_schema,
right_schema,
Operator::IsNotDistinctFrom,
)
}
/// See comments in `split_eq_and_noneq_join_predicate` for details.
fn split_op_and_other_join_predicates(
filter: Expr,
left_schema: &DFSchema,
right_schema: &DFSchema,
operator: Operator,
) -> Result<(Vec<EquijoinPredicate>, Option<Expr>)> {
if !matches!(operator, Operator::Eq | Operator::IsNotDistinctFrom) {
return internal_err!(
"split_op_and_other_join_predicates only supports 'Eq' or 'IsNotDistinctFrom' operators, \
but received: {:?}",
operator
);
}
let exprs = split_conjunction_owned(filter);
// Treat 'is not distinct from' comparison as join key in equal joins
let mut accum_join_keys: Vec<(Expr, Expr)> = vec![];
let mut accum_filters: Vec<Expr> = vec![];
for expr in exprs {
match expr {
Expr::BinaryExpr(BinaryExpr {
ref left,
ref op,
ref right,
}) if *op == operator => {
let join_key_pair =
find_valid_equijoin_key_pair(left, right, left_schema, right_schema)?;
if let Some((left_expr, right_expr)) = join_key_pair {
let left_expr_type = left_expr.get_type(left_schema)?;
let right_expr_type = right_expr.get_type(right_schema)?;
if can_hash(&left_expr_type) && can_hash(&right_expr_type) {
accum_join_keys.push((left_expr, right_expr));
} else {
accum_filters.push(expr);
}
} else {
accum_filters.push(expr);
}
}
_ => accum_filters.push(expr),
}
}
let result_filter = accum_filters.into_iter().reduce(Expr::and);
Ok((accum_join_keys, result_filter))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_optimized_plan_eq_display_indent_snapshot;
use crate::test::*;
use arrow::datatypes::DataType;
use datafusion_expr::{
col, lit, logical_plan::builder::LogicalPlanBuilder, JoinType,
};
use std::sync::Arc;
macro_rules! assert_optimized_plan_equal {
(
$plan:expr,
@ $expected:literal $(,)?
) => {{
let rule: Arc<dyn crate::OptimizerRule + Send + Sync> = Arc::new(ExtractEquijoinPredicate {});
assert_optimized_plan_eq_display_indent_snapshot!(
rule,
$plan,
@ $expected,
)
}};
}
#[test]
fn join_with_only_column_equi_predicate() -> Result<()> {
let t1 = test_table_scan_with_name("t1")?;
let t2 = test_table_scan_with_name("t2")?;
let plan = LogicalPlanBuilder::from(t1)
.join_on(t2, JoinType::Left, Some(col("t1.a").eq(col("t2.a"))))?
.build()?;
assert_optimized_plan_equal!(
plan,
@r"
Left Join: t1.a = t2.a [a:UInt32, b:UInt32, c:UInt32, a:UInt32;N, b:UInt32;N, c:UInt32;N]
TableScan: t1 [a:UInt32, b:UInt32, c:UInt32]
TableScan: t2 [a:UInt32, b:UInt32, c:UInt32]
"
)
}
#[test]
fn join_with_only_equi_expr_predicate() -> Result<()> {
let t1 = test_table_scan_with_name("t1")?;
let t2 = test_table_scan_with_name("t2")?;
let plan = LogicalPlanBuilder::from(t1)
.join_on(
t2,
JoinType::Left,
Some((col("t1.a") + lit(10i64)).eq(col("t2.a") * lit(2u32))),
)?
.build()?;
assert_optimized_plan_equal!(
plan,
@r"
Left Join: t1.a + Int64(10) = t2.a * UInt32(2) [a:UInt32, b:UInt32, c:UInt32, a:UInt32;N, b:UInt32;N, c:UInt32;N]
TableScan: t1 [a:UInt32, b:UInt32, c:UInt32]
TableScan: t2 [a:UInt32, b:UInt32, c:UInt32]
"
)
}
#[test]
fn join_with_only_none_equi_predicate() -> Result<()> {
let t1 = test_table_scan_with_name("t1")?;
let t2 = test_table_scan_with_name("t2")?;
let plan = LogicalPlanBuilder::from(t1)
.join_on(
t2,
JoinType::Left,
Some(
(col("t1.a") + lit(10i64))
.gt_eq(col("t2.a") * lit(2u32))
.and(col("t1.b").lt(lit(100i32))),
),
)?
.build()?;
assert_optimized_plan_equal!(
plan,
@r"
Left Join: Filter: t1.a + Int64(10) >= t2.a * UInt32(2) AND t1.b < Int32(100) [a:UInt32, b:UInt32, c:UInt32, a:UInt32;N, b:UInt32;N, c:UInt32;N]
TableScan: t1 [a:UInt32, b:UInt32, c:UInt32]
TableScan: t2 [a:UInt32, b:UInt32, c:UInt32]
"
)
}
#[test]
fn join_with_expr_both_from_filter_and_keys() -> Result<()> {
let t1 = test_table_scan_with_name("t1")?;
let t2 = test_table_scan_with_name("t2")?;
let plan = LogicalPlanBuilder::from(t1)
.join_with_expr_keys(
t2,
JoinType::Left,
(
vec![col("t1.a") + lit(11u32)],
vec![col("t2.a") * lit(2u32)],
),
Some(
(col("t1.a") + lit(10i64))
.eq(col("t2.a") * lit(2u32))
.and(col("t1.b").lt(lit(100i32))),
),
)?
.build()?;
assert_optimized_plan_equal!(
plan,
@r"
Left Join: t1.a + UInt32(11) = t2.a * UInt32(2), t1.a + Int64(10) = t2.a * UInt32(2) Filter: t1.b < Int32(100) [a:UInt32, b:UInt32, c:UInt32, a:UInt32;N, b:UInt32;N, c:UInt32;N]
TableScan: t1 [a:UInt32, b:UInt32, c:UInt32]
TableScan: t2 [a:UInt32, b:UInt32, c:UInt32]
"
)
}
#[test]
fn join_with_and_or_filter() -> Result<()> {
let t1 = test_table_scan_with_name("t1")?;
let t2 = test_table_scan_with_name("t2")?;
let plan = LogicalPlanBuilder::from(t1)
.join_on(
t2,
JoinType::Left,
Some(
col("t1.c")
.eq(col("t2.c"))
.or((col("t1.a") + col("t1.b")).gt(col("t2.b") + col("t2.c")))
.and(
col("t1.a").eq(col("t2.a")).and(col("t1.b").eq(col("t2.b"))),
),
),
)?
.build()?;
assert_optimized_plan_equal!(
plan,
@r"
Left Join: t1.a = t2.a, t1.b = t2.b Filter: t1.c = t2.c OR t1.a + t1.b > t2.b + t2.c [a:UInt32, b:UInt32, c:UInt32, a:UInt32;N, b:UInt32;N, c:UInt32;N]
TableScan: t1 [a:UInt32, b:UInt32, c:UInt32]
TableScan: t2 [a:UInt32, b:UInt32, c:UInt32]
"
)
}
#[test]
fn join_with_multiple_table() -> Result<()> {
let t1 = test_table_scan_with_name("t1")?;
let t2 = test_table_scan_with_name("t2")?;
let t3 = test_table_scan_with_name("t3")?;
let input = LogicalPlanBuilder::from(t2)
.join_on(
t3,
JoinType::Left,
Some(
col("t2.a")
.eq(col("t3.a"))
.and((col("t2.a") + col("t3.b")).gt(lit(100u32))),
),
)?
.build()?;
let plan = LogicalPlanBuilder::from(t1)
.join_on(
input,
JoinType::Left,
Some(
col("t1.a")
.eq(col("t2.a"))
.and((col("t1.c") + col("t2.c") + col("t3.c")).lt(lit(100u32))),
),
)?
.build()?;
assert_optimized_plan_equal!(
plan,
@r"
Left Join: t1.a = t2.a Filter: t1.c + t2.c + t3.c < UInt32(100) [a:UInt32, b:UInt32, c:UInt32, a:UInt32;N, b:UInt32;N, c:UInt32;N, a:UInt32;N, b:UInt32;N, c:UInt32;N]
TableScan: t1 [a:UInt32, b:UInt32, c:UInt32]
Left Join: t2.a = t3.a Filter: t2.a + t3.b > UInt32(100) [a:UInt32, b:UInt32, c:UInt32, a:UInt32;N, b:UInt32;N, c:UInt32;N]
TableScan: t2 [a:UInt32, b:UInt32, c:UInt32]
TableScan: t3 [a:UInt32, b:UInt32, c:UInt32]
"
)
}
#[test]
fn join_with_multiple_table_and_eq_filter() -> Result<()> {
let t1 = test_table_scan_with_name("t1")?;
let t2 = test_table_scan_with_name("t2")?;
let t3 = test_table_scan_with_name("t3")?;
let input = LogicalPlanBuilder::from(t2)
.join_on(
t3,
JoinType::Left,
Some(
col("t2.a")
.eq(col("t3.a"))
.and((col("t2.a") + col("t3.b")).gt(lit(100u32))),
),
)?
.build()?;
let plan = LogicalPlanBuilder::from(t1)
.join_on(
input,
JoinType::Left,
Some(col("t1.a").eq(col("t2.a")).and(col("t2.c").eq(col("t3.c")))),
)?
.build()?;
assert_optimized_plan_equal!(
plan,
@r"
Left Join: t1.a = t2.a Filter: t2.c = t3.c [a:UInt32, b:UInt32, c:UInt32, a:UInt32;N, b:UInt32;N, c:UInt32;N, a:UInt32;N, b:UInt32;N, c:UInt32;N]
TableScan: t1 [a:UInt32, b:UInt32, c:UInt32]
Left Join: t2.a = t3.a Filter: t2.a + t3.b > UInt32(100) [a:UInt32, b:UInt32, c:UInt32, a:UInt32;N, b:UInt32;N, c:UInt32;N]
TableScan: t2 [a:UInt32, b:UInt32, c:UInt32]
TableScan: t3 [a:UInt32, b:UInt32, c:UInt32]
"
)
}
#[test]
fn join_with_alias_filter() -> Result<()> {
let t1 = test_table_scan_with_name("t1")?;
let t2 = test_table_scan_with_name("t2")?;
let t1_schema = Arc::clone(t1.schema());
let t2_schema = Arc::clone(t2.schema());
// filter: t1.a + CAST(Int64(1), UInt32) = t2.a + CAST(Int64(2), UInt32) as t1.a + 1 = t2.a + 2
let filter = Expr::eq(
col("t1.a") + lit(1i64).cast_to(&DataType::UInt32, &t1_schema)?,
col("t2.a") + lit(2i32).cast_to(&DataType::UInt32, &t2_schema)?,
)
.alias("t1.a + 1 = t2.a + 2");
let plan = LogicalPlanBuilder::from(t1)
.join_on(t2, JoinType::Left, Some(filter))?
.build()?;
assert_optimized_plan_equal!(
plan,
@r"
Left Join: t1.a + CAST(Int64(1) AS UInt32) = t2.a + CAST(Int32(2) AS UInt32) [a:UInt32, b:UInt32, c:UInt32, a:UInt32;N, b:UInt32;N, c:UInt32;N]
TableScan: t1 [a:UInt32, b:UInt32, c:UInt32]
TableScan: t2 [a:UInt32, b:UInt32, c:UInt32]
"
)
}
}