linkrs_query/optimizer/heuristic/join_optimization/
join_reorder.rs1use 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#[derive(Debug, Clone)]
49struct TableInfo {
50 estimated_rows: f64,
51}
52
53#[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}