Skip to content

Commit d1864ae

Browse files
committed
implement optimizer rule
1 parent 34860b7 commit d1864ae

4 files changed

Lines changed: 184 additions & 2 deletions

File tree

‎datafusion/optimizer/src/reorder_join/cost.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use datafusion_common::{plan_datafusion_err, plan_err, stats::Precision, Result};
22
use datafusion_expr::{Join, LogicalPlan};
33

4-
pub trait JoinCostEstimator {
4+
pub trait JoinCostEstimator: std::fmt::Debug {
55
fn cardinality(&self, plan: &LogicalPlan) -> Option<usize> {
66
estimate_cardinality(plan).ok()
77
}
@@ -15,6 +15,12 @@ pub trait JoinCostEstimator {
1515
}
1616
}
1717

18+
/// Default implementation of JoinCostEstimator
19+
#[derive(Debug, Clone, Copy)]
20+
pub struct DefaultCostEstimator;
21+
22+
impl JoinCostEstimator for DefaultCostEstimator {}
23+
1824
fn estimate_cardinality(plan: &LogicalPlan) -> Result<usize> {
1925
match plan {
2026
LogicalPlan::Filter(filter) => {

‎datafusion/optimizer/src/reorder_join/left_deep_join_plan.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,7 @@ mod tests {
519519
use datafusion_expr::{col, LogicalPlanBuilder};
520520

521521
/// A simple cost estimator for testing
522+
#[derive(Debug)]
522523
struct TestCostEstimator;
523524

524525
impl JoinCostEstimator for TestCostEstimator {}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,178 @@
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+
//! Optimizer rule for reordering joins to minimize query execution cost
19+
20+
use std::rc::Rc;
21+
22+
use datafusion_common::tree_node::{Transformed, TreeNode, TreeNodeRecursion};
23+
use datafusion_common::{Result};
24+
use datafusion_expr::LogicalPlan;
25+
26+
use crate::optimizer::ApplyOrder;
27+
use crate::{OptimizerConfig, OptimizerRule};
28+
129
pub mod cost;
230
pub mod left_deep_join_plan;
331
pub mod query_graph;
32+
33+
use cost::JoinCostEstimator;
34+
use left_deep_join_plan::optimal_left_deep_join_plan;
35+
use query_graph::{contains_join, QueryGraph};
36+
37+
/// Optimizer rule that reorders joins to minimize query execution cost.
38+
///
39+
/// This rule identifies consecutive join operations in a query plan and reorders
40+
/// them using the Ibaraki-Kameda algorithm. The algorithm:
41+
///
42+
/// 1. Converts a join subtree into a query graph representation
43+
/// 2. Builds precedence trees to explore different join orderings
44+
/// 3. Normalizes and denormalizes the trees to find optimal orderings
45+
/// 4. Selects the ordering with the lowest estimated cost
46+
///
47+
/// The rule only reorders inner joins and requires all joins to be consecutive
48+
/// in the plan tree (no other operations between them).
49+
///
50+
/// # Example
51+
///
52+
/// Given a query plan like:
53+
/// ```text
54+
/// Join(customer.c_custkey = orders.o_custkey)
55+
/// Join(orders.o_orderkey = lineitem.l_orderkey)
56+
/// TableScan(customer)
57+
/// TableScan(orders)
58+
/// TableScan(lineitem)
59+
/// ```
60+
///
61+
/// The optimizer will evaluate different join orderings and select the one
62+
/// that minimizes intermediate result sizes and overall execution cost.
63+
#[derive(Debug)]
64+
pub struct JoinReorder {
65+
cost_estimator: Rc<dyn JoinCostEstimator>,
66+
}
67+
68+
impl JoinReorder {
69+
/// Creates a new join reorder optimizer rule with the given cost estimator
70+
pub fn new(cost_estimator: Rc<dyn JoinCostEstimator>) -> Self {
71+
Self { cost_estimator }
72+
}
73+
}
74+
75+
impl Default for JoinReorder {
76+
fn default() -> Self {
77+
Self {
78+
cost_estimator: Rc::new(cost::DefaultCostEstimator),
79+
}
80+
}
81+
}
82+
83+
impl OptimizerRule for JoinReorder {
84+
fn name(&self) -> &str {
85+
"join_reorder"
86+
}
87+
88+
fn apply_order(&self) -> Option<ApplyOrder> {
89+
// We need bottom-up traversal to process join subtrees from leaves to root
90+
Some(ApplyOrder::BottomUp)
91+
}
92+
93+
fn rewrite(
94+
&self,
95+
plan: LogicalPlan,
96+
_config: &dyn OptimizerConfig,
97+
) -> Result<Transformed<LogicalPlan>> {
98+
// Only try to reorder if this is a join node
99+
if !matches!(plan, LogicalPlan::Join(_)) {
100+
return Ok(Transformed::no(plan));
101+
}
102+
103+
// Check if this join is the root of a consecutive join subtree
104+
// (i.e., all its children are either joins or leaf nodes)
105+
if !is_join_subtree_root(&plan) {
106+
return Ok(Transformed::no(plan));
107+
}
108+
109+
// Try to convert the join subtree to a query graph and optimize it
110+
match optimize_join_subtree(plan.clone(), Rc::clone(&self.cost_estimator)) {
111+
Ok(optimized_plan) => Ok(Transformed::yes(optimized_plan)),
112+
Err(_) => {
113+
// If optimization fails (e.g., unsupported join type), return original plan
114+
Ok(Transformed::no(plan))
115+
}
116+
}
117+
}
118+
}
119+
120+
/// Checks if a plan node is the root of a consecutive join subtree.
121+
///
122+
/// A node is considered a join subtree root if:
123+
/// - It is a Join node
124+
/// - All its descendants are either Join nodes or don't contain any joins
125+
///
126+
/// This ensures we only try to optimize complete join subtrees that can be
127+
/// safely reordered without breaking other operators.
128+
fn is_join_subtree_root(plan: &LogicalPlan) -> bool {
129+
if !matches!(plan, LogicalPlan::Join(_)) {
130+
return false;
131+
}
132+
133+
// Check if all children either are joins themselves or don't contain any joins
134+
let mut all_valid = true;
135+
let _ = plan.apply_children(|child| {
136+
if matches!(child, LogicalPlan::Join(_)) {
137+
// This child is a join, continue checking down the tree
138+
Ok(TreeNodeRecursion::Continue)
139+
} else if !contains_join(child) {
140+
// This child doesn't contain any joins - it's a leaf subtree
141+
Ok(TreeNodeRecursion::Continue)
142+
} else {
143+
// Found a non-join node that contains joins - this breaks the consecutive join pattern
144+
all_valid = false;
145+
Ok(TreeNodeRecursion::Stop)
146+
}
147+
});
148+
149+
all_valid
150+
}
151+
152+
/// Optimizes a join subtree by converting it to a query graph and finding
153+
/// the optimal join ordering.
154+
///
155+
/// # Arguments
156+
///
157+
/// * `plan` - The join subtree to optimize (must be a Join node at the root)
158+
/// * `cost_estimator` - The cost estimator to use for optimization
159+
///
160+
/// # Returns
161+
///
162+
/// Returns an optimized LogicalPlan with joins reordered for minimal cost.
163+
///
164+
/// # Errors
165+
///
166+
/// Returns an error if:
167+
/// - The plan cannot be converted to a query graph
168+
/// - The optimization algorithm fails
169+
fn optimize_join_subtree(
170+
plan: LogicalPlan,
171+
cost_estimator: Rc<dyn JoinCostEstimator>,
172+
) -> Result<LogicalPlan> {
173+
// Convert the join subtree to a query graph
174+
let query_graph = QueryGraph::try_from(plan)?;
175+
176+
// Use the Ibaraki-Kameda algorithm to find the optimal join ordering
177+
optimal_left_deep_join_plan(query_graph, cost_estimator)
178+
}

‎datafusion/optimizer/src/reorder_join/query_graph.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ fn flatten_joins_recursive(
271271
/// # Returns
272272
///
273273
/// `true` if the plan contains at least one join node, `false` otherwise
274-
fn contains_join(plan: &LogicalPlan) -> bool {
274+
pub(crate) fn contains_join(plan: &LogicalPlan) -> bool {
275275
let mut has_join = false;
276276

277277
// Use TreeNode's apply method to traverse the plan

0 commit comments

Comments
 (0)