Skip to main content

linkrs_query/optimizer/heuristic/join_optimization/
join_reorder.rs

1//! Rules for multi-table JOIN reordering
2//!
3//! This rule reorders multiple JOINs to minimize intermediate result sizes
4//! and improve query performance. It uses cardinality estimation and
5//! cost-based optimization to determine the optimal JOIN order.
6//!
7//! # Conversion examples
8//!
9//! ## Case 1: Reorder for smaller intermediate results
10//! Before:
11//! ```text
12//!   InnerJoin(ON a.id = c.id)
13//!     → InnerJoin(ON a.id = b.id) → A → B
14//!     → C
15//! ```
16//! After (if |A×B| > |B×C|):
17//! ```text
18//!   InnerJoin(ON b.id = c.id)
19//!     → InnerJoin(ON a.id = b.id) → A → B
20//!     → C
21//! ```
22//!
23//! ## Case 2: Bushy tree optimization
24//! Before:
25//! ```text
26//!   InnerJoin
27//!     → InnerJoin
28//!       → InnerJoin → A → B
29//!       → C
30//!     → D
31//! ```
32//! After:
33//! ```text
34//!   InnerJoin
35//!     → InnerJoin → A → B
36//!     → InnerJoin → C → D
37//! ```
38
39use crate::optimizer::heuristic::context::RewriteContext;
40use crate::optimizer::heuristic::pattern::Pattern;
41use crate::optimizer::heuristic::result::{RewriteResult, TransformResult};
42use crate::optimizer::heuristic::rule::RewriteRule;
43use crate::planning::plan::core::nodes::join::join_node::InnerJoinNode;
44use crate::planning::plan::PlanNodeEnum;
45use std::collections::HashSet;
46
47/// Information about a table in the JOIN tree
48#[derive(Debug, Clone)]
49struct TableInfo {
50    estimated_rows: f64,
51}
52
53/// Rules for multi-table JOIN reordering
54#[derive(Debug)]
55pub struct JoinReorderRule;
56
57impl JoinReorderRule {
58    pub fn new() -> Self {
59        Self
60    }
61
62    fn collect_tables(&self, node: &PlanNodeEnum) -> Vec<TableInfo> {
63        let mut tables = Vec::new();
64        self.collect_tables_recursive(node, &mut tables);
65        tables
66    }
67
68    fn collect_tables_recursive(&self, node: &PlanNodeEnum, tables: &mut Vec<TableInfo>) {
69        match node {
70            PlanNodeEnum::ScanVertices(_) => {
71                tables.push(TableInfo {
72                    estimated_rows: 10000.0,
73                });
74            }
75            PlanNodeEnum::ScanEdges(_) => {
76                tables.push(TableInfo {
77                    estimated_rows: 50000.0,
78                });
79            }
80            PlanNodeEnum::InnerJoin(join) => {
81                self.collect_tables_recursive(join.left_input(), tables);
82                self.collect_tables_recursive(join.right_input(), tables);
83            }
84            _ => {}
85        }
86    }
87
88    fn estimate_join_cardinality(&self, left_rows: f64, right_rows: f64, selectivity: f64) -> f64 {
89        left_rows * right_rows * selectivity
90    }
91
92    fn estimate_selectivity(&self, _join_keys: &[String]) -> f64 {
93        0.01
94    }
95
96    fn find_best_join_order(&self, tables: &[TableInfo]) -> Option<Vec<usize>> {
97        if tables.len() < 2 {
98            return None;
99        }
100
101        let mut best_order: Vec<usize> = (0..tables.len()).collect();
102        let mut best_cost = self.calculate_join_cost(&best_order, tables);
103
104        let n = tables.len();
105        if n <= 4 {
106            self.permute_and_find_best(&mut best_order, &mut best_cost, tables, 0);
107        } else {
108            self.greedy_find_best(&mut best_order, &mut best_cost, tables);
109        }
110
111        Some(best_order)
112    }
113
114    fn permute_and_find_best(
115        &self,
116        best_order: &mut Vec<usize>,
117        best_cost: &mut f64,
118        tables: &[TableInfo],
119        start: usize,
120    ) {
121        if start == best_order.len() - 1 {
122            let cost = self.calculate_join_cost(best_order, tables);
123            if cost < *best_cost {
124                *best_cost = cost;
125            }
126            return;
127        }
128
129        for i in start..best_order.len() {
130            best_order.swap(start, i);
131            self.permute_and_find_best(best_order, best_cost, tables, start + 1);
132            best_order.swap(start, i);
133        }
134    }
135
136    fn greedy_find_best(
137        &self,
138        best_order: &mut Vec<usize>,
139        best_cost: &mut f64,
140        tables: &[TableInfo],
141    ) {
142        let mut remaining: HashSet<usize> = (0..tables.len()).collect();
143        let mut result = Vec::new();
144
145        let mut min_rows = f64::MAX;
146        let mut first_table = 0;
147        for (i, table) in tables.iter().enumerate() {
148            if table.estimated_rows < min_rows {
149                min_rows = table.estimated_rows;
150                first_table = i;
151            }
152        }
153        result.push(first_table);
154        remaining.remove(&first_table);
155
156        while !remaining.is_empty() {
157            let mut best_next = None;
158            let mut best_next_cost = f64::MAX;
159
160            for &idx in &remaining {
161                let mut test_order = result.clone();
162                test_order.push(idx);
163                let cost = self.calculate_join_cost(&test_order, tables);
164                if cost < best_next_cost {
165                    best_next_cost = cost;
166                    best_next = Some(idx);
167                }
168            }
169
170            if let Some(idx) = best_next {
171                result.push(idx);
172                remaining.remove(&idx);
173            } else {
174                break;
175            }
176        }
177
178        *best_order = result;
179        *best_cost = self.calculate_join_cost(best_order, tables);
180    }
181
182    fn calculate_join_cost(&self, order: &[usize], tables: &[TableInfo]) -> f64 {
183        if order.is_empty() {
184            return 0.0;
185        }
186
187        let mut total_cost = 0.0;
188        let mut current_rows = tables[order[0]].estimated_rows;
189
190        for i in 1..order.len() {
191            let right_rows = tables[order[i]].estimated_rows;
192            let selectivity = self.estimate_selectivity(&[]);
193            let join_rows = self.estimate_join_cardinality(current_rows, right_rows, selectivity);
194
195            total_cost += current_rows + right_rows;
196            current_rows = join_rows;
197        }
198
199        total_cost
200    }
201
202    fn apply_to_inner_join(&self, join: &InnerJoinNode) -> RewriteResult<Option<TransformResult>> {
203        let tables = self.collect_tables(&PlanNodeEnum::InnerJoin(join.clone()));
204
205        if tables.len() < 3 {
206            return Ok(None);
207        }
208
209        let _best_order = match self.find_best_join_order(&tables) {
210            Some(order) => order,
211            None => return Ok(None),
212        };
213
214        Ok(None)
215    }
216}
217
218impl Default for JoinReorderRule {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224impl RewriteRule for JoinReorderRule {
225    fn name(&self) -> &'static str {
226        "JoinReorderRule"
227    }
228
229    fn pattern(&self) -> Pattern {
230        Pattern::new_with_name("InnerJoin")
231    }
232
233    fn apply(
234        &self,
235        _ctx: &mut RewriteContext,
236        node: &PlanNodeEnum,
237    ) -> RewriteResult<Option<TransformResult>> {
238        match node {
239            PlanNodeEnum::InnerJoin(join) => self.apply_to_inner_join(join),
240            _ => Ok(None),
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_rule_name() {
251        let rule = JoinReorderRule::new();
252        assert_eq!(rule.name(), "JoinReorderRule");
253    }
254
255    #[test]
256    fn test_rule_pattern() {
257        let rule = JoinReorderRule::new();
258        let pattern = rule.pattern();
259        assert!(pattern.node.is_some());
260    }
261
262    #[test]
263    fn test_estimate_join_cardinality() {
264        let rule = JoinReorderRule::new();
265        let cardinality = rule.estimate_join_cardinality(1000.0, 1000.0, 0.1);
266        assert!((cardinality - 100000.0).abs() < 0.01);
267    }
268
269    #[test]
270    fn test_find_best_join_order() {
271        let rule = JoinReorderRule::new();
272        let tables = vec![
273            TableInfo {
274                estimated_rows: 1000.0,
275            },
276            TableInfo {
277                estimated_rows: 100.0,
278            },
279            TableInfo {
280                estimated_rows: 10.0,
281            },
282        ];
283
284        let best_order = rule.find_best_join_order(&tables);
285        assert!(best_order.is_some());
286
287        let order = best_order.unwrap();
288        assert_eq!(order.len(), 3);
289    }
290
291    #[test]
292    fn test_greedy_find_best() {
293        let rule = JoinReorderRule::new();
294        let tables = vec![
295            TableInfo {
296                estimated_rows: 10000.0,
297            },
298            TableInfo {
299                estimated_rows: 1000.0,
300            },
301            TableInfo {
302                estimated_rows: 100.0,
303            },
304            TableInfo {
305                estimated_rows: 10.0,
306            },
307        ];
308
309        let mut best_order: Vec<usize> = (0..tables.len()).collect();
310        let mut best_cost = f64::MAX;
311        rule.greedy_find_best(&mut best_order, &mut best_cost, &tables);
312
313        assert_eq!(best_order.len(), 4);
314        assert!(best_cost > 0.0);
315    }
316}