1use std::collections::HashMap;
11use std::sync::Arc;
12
13use linkrs_core::types::expr::contextual::ContextualExpression;
14use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
15use linkrs_core::types::expr::ExpressionMeta;
16use linkrs_core::Expression;
17
18use crate::planning::plan::core::next_node_id;
19use crate::planning::plan::logical::logical_nodes::access::{
20 LogicalScanEdgesNode, LogicalScanVerticesNode,
21};
22use crate::planning::plan::logical::logical_nodes::join::LogicalInnerJoinNode;
23use crate::planning::plan::logical::LogicalNodeEnum;
24
25use super::cardinality_estimator::{CardinalityEstimator, JoinOrderStats};
26use super::cost_model::CostModel;
27use super::join_tree::{JoinHint, JoinHintError, JoinTreeConstructor};
28use super::query_graph::QueryGraph;
29use super::subplans_table::{JoinOrderPlan, SubPlansTable};
30use super::subquery_graph::SubqueryGraph;
31
32pub const MAX_LEVEL_TO_PLAN_EXACTLY: usize = 7;
35
36pub struct JoinOrderEnumeratorContext {
38 pub sub_plans_table: SubPlansTable,
39 pub cardinality_estimator: CardinalityEstimator,
40 pub cost_model: CostModel,
41 pub max_cost: u64,
42 space_id: u64,
43 space_name: String,
44 expr_ctx: Arc<ExpressionAnalysisContext>,
45 key_cache: HashMap<String, ContextualExpression>,
46}
47
48impl std::fmt::Debug for JoinOrderEnumeratorContext {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("JoinOrderEnumeratorContext")
51 .field("max_cost", &self.max_cost)
52 .field("num_dp_levels", &self.sub_plans_table.num_levels())
53 .finish()
54 }
55}
56
57impl Default for JoinOrderEnumeratorContext {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl JoinOrderEnumeratorContext {
64 pub fn new() -> Self {
65 Self {
66 sub_plans_table: SubPlansTable::new(),
67 cardinality_estimator: CardinalityEstimator::new(),
68 cost_model: CostModel,
69 max_cost: u64::MAX,
70 space_id: 1,
71 space_name: "default".to_string(),
72 expr_ctx: Arc::new(ExpressionAnalysisContext::new()),
73 key_cache: HashMap::new(),
74 }
75 }
76
77 pub fn with_space(mut self, space_id: u64, space_name: impl Into<String>) -> Self {
79 self.space_id = space_id;
80 self.space_name = space_name.into();
81 self
82 }
83
84 pub fn with_stats(mut self, stats: JoinOrderStats) -> Self {
86 self.cardinality_estimator.set_stats(stats);
87 self
88 }
89
90 pub fn join_key(&mut self, variable: &str) -> ContextualExpression {
93 if let Some(cached) = self.key_cache.get(variable) {
94 return cached.clone();
95 }
96 let id = self
97 .expr_ctx
98 .register_expression(ExpressionMeta::new(Expression::Variable(
99 variable.to_string(),
100 )));
101 let key = ContextualExpression::new(id, Arc::clone(&self.expr_ctx));
102 self.key_cache.insert(variable.to_string(), key.clone());
103 key
104 }
105}
106
107#[derive(Debug, Default)]
109pub struct JoinOrderEnumerator {
110 context: JoinOrderEnumeratorContext,
111}
112
113impl JoinOrderEnumerator {
114 pub fn new() -> Self {
115 Self {
116 context: JoinOrderEnumeratorContext::new(),
117 }
118 }
119
120 pub fn with_space(mut self, space_id: u64, space_name: impl Into<String>) -> Self {
122 self.context = std::mem::take(&mut self.context).with_space(space_id, space_name);
123 self
124 }
125
126 pub fn with_stats(mut self, stats: JoinOrderStats) -> Self {
128 self.context = std::mem::take(&mut self.context).with_stats(stats);
129 self
130 }
131
132 pub fn context(&self) -> &JoinOrderEnumeratorContext {
133 &self.context
134 }
135
136 pub(crate) fn context_mut(&mut self) -> &mut JoinOrderEnumeratorContext {
137 &mut self.context
138 }
139
140 fn enumerate_levels(&mut self, query_graph: &Arc<QueryGraph>) {
143 self.plan_base_table_scans(query_graph);
144 let max_level = query_graph.num_nodes() + query_graph.num_rels();
145 for level in 2..=max_level {
146 if level <= MAX_LEVEL_TO_PLAN_EXACTLY {
147 self.plan_level_exactly(level, query_graph);
148 } else {
149 self.plan_level_approximately(level, query_graph);
150 }
151 }
152 }
153
154 pub fn plan_query_graph(&mut self, query_graph: &Arc<QueryGraph>) -> Option<LogicalNodeEnum> {
157 self.enumerate_levels(query_graph);
158 let mut full = SubqueryGraph::new(Arc::clone(query_graph));
159 for node_pos in 0..query_graph.num_nodes() {
160 full.add_query_node(node_pos);
161 }
162 for rel_pos in 0..query_graph.num_rels() {
163 full.add_query_rel(rel_pos);
164 }
165 self.context
166 .sub_plans_table
167 .get_best_plan(&full)
168 .map(|p| p.plan.clone())
169 }
170
171 fn plan_base_table_scans(&mut self, query_graph: &Arc<QueryGraph>) {
178 for (node_pos, node) in query_graph.query_nodes.iter().enumerate() {
179 let key = self.context.join_key(&node.variable);
180 let cardinality = self
181 .context
182 .cardinality_estimator
183 .estimate_node_scan(&node.labels);
184 self.context
185 .cardinality_estimator
186 .set_node_id_domain(key.id().clone(), cardinality);
187 let plan = LogicalNodeEnum::ScanVertices(LogicalScanVerticesNode {
188 id: next_node_id(),
189 space_id: self.context.space_id,
190 space_name: self.context.space_name.clone(),
191 tag: node.labels.first().cloned(),
192 expression: Some(key),
193 limit: None,
194 projected_properties: vec![],
195 index_hint: None,
196 estimated_cardinality: Some(cardinality),
197 output_var: Some(node.variable.clone()),
198 col_names: vec![node.variable.clone()],
199 column_types: vec![],
200 });
201 let cost = CostModel::compute_extend_cost(0, cardinality);
202 let encoding = self.encode_plan(&plan, query_graph);
203 let subgraph = SubqueryGraph::single_node(query_graph, node_pos);
204 self.context.sub_plans_table.add_plan(
205 &subgraph,
206 JoinOrderPlan::new(plan, cost, cardinality, encoding),
207 );
208 }
209 for (rel_pos, rel) in query_graph.query_rels.iter().enumerate() {
210 let key = self.context.join_key(&rel.variable);
211 let cardinality = self
212 .context
213 .cardinality_estimator
214 .estimate_rel_scan(&rel.edge_types);
215 let plan = LogicalNodeEnum::ScanEdges(LogicalScanEdgesNode {
216 id: next_node_id(),
217 space_id: self.context.space_id,
218 edge_type: rel.edge_types.first().cloned(),
219 expression: Some(key),
220 limit: None,
221 projected_properties: vec![],
222 index_hint: None,
223 estimated_cardinality: Some(cardinality),
224 output_var: Some(rel.variable.clone()),
225 col_names: vec![rel.variable.clone()],
226 column_types: vec![],
227 });
228 let cost = CostModel::compute_extend_cost(0, cardinality);
229 let encoding = self.encode_plan(&plan, query_graph);
230 let subgraph = SubqueryGraph::single_rel(query_graph, rel_pos);
231 self.context.sub_plans_table.add_plan(
232 &subgraph,
233 JoinOrderPlan::new(plan, cost, cardinality, encoding),
234 );
235 }
236 }
237
238 pub(crate) fn encode_plan(
251 &mut self,
252 plan: &LogicalNodeEnum,
253 query_graph: &Arc<QueryGraph>,
254 ) -> u64 {
255 let mut owned = plan.clone();
256 let schema = match Self::compute_schema_for_plan(&mut owned) {
263 Ok(s) => s,
264 Err(e) => {
265 log::warn!("encode_plan: schema error {e}; degrade to all-unflat encoding");
266 crate::planning::plan::factorization::FactorizedSchema::new()
267 }
268 };
269 let mut encoding = 0u64;
270 for (i, node) in query_graph.query_nodes.iter().enumerate() {
271 if !Self::variable_is_flat(&schema, &mut self.context, &node.variable) {
272 continue;
273 }
274 if i < 64 {
275 encoding |= 1u64 << i;
276 } else {
277 encoding = Self::mix_overflow(encoding, i as u64);
278 }
279 }
280 for (j, rel) in query_graph.query_rels.iter().enumerate() {
281 if !Self::variable_is_flat(&schema, &mut self.context, &rel.variable) {
282 continue;
283 }
284 encoding = Self::mix_overflow(encoding, 0x9e37u64 + j as u64);
287 }
288 encoding
289 }
290
291 fn plan_level_exactly(&mut self, level: usize, query_graph: &Arc<QueryGraph>) {
293 let max_left = level / 2;
294 for left_level in 1..=max_left.max(1).min(level - 1) {
295 let right_level = level - left_level;
296 if left_level > 1 {
297 self.plan_wco_join(left_level, right_level, query_graph);
298 }
299 self.plan_inner_join(left_level, right_level, query_graph);
300 }
301 }
302
303 fn plan_level_approximately(&mut self, level: usize, query_graph: &Arc<QueryGraph>) {
305 if level < 2 {
306 return;
307 }
308 self.plan_inner_join(1, level - 1, query_graph);
309 }
310
311 fn plan_inner_join(
313 &mut self,
314 left_level: usize,
315 right_level: usize,
316 query_graph: &Arc<QueryGraph>,
317 ) {
318 let right_subgraphs: Vec<SubqueryGraph> = self
319 .context
320 .sub_plans_table
321 .get_subgraphs(right_level)
322 .iter()
323 .map(|(sg, _)| sg.clone())
324 .collect();
325 for right_subgraph in &right_subgraphs {
326 let neighbors = right_subgraph.get_neighbor_subgraphs(left_level);
327 for nbr_subgraph in &neighbors {
328 if !nbr_subgraph.is_joinable_with(right_subgraph) {
329 continue;
330 }
331 if self.try_plan_extend_join(right_subgraph, nbr_subgraph, query_graph) {
334 continue;
335 }
336 self.plan_inner_hash_join(right_subgraph, nbr_subgraph, query_graph);
337 }
338 }
339 }
340
341 fn try_plan_extend_join(
345 &mut self,
346 right_subgraph: &SubqueryGraph,
347 nbr_subgraph: &SubqueryGraph,
348 query_graph: &Arc<QueryGraph>,
349 ) -> bool {
350 if right_subgraph.is_connected(nbr_subgraph) {
351 return false;
352 }
353 if !right_subgraph.is_extendable_with(nbr_subgraph) {
354 return false;
355 }
356 let right_plans: Vec<JoinOrderPlan> = self
357 .context
358 .sub_plans_table
359 .get_plans(right_subgraph)
360 .to_vec();
361 let nbr_plans: Vec<JoinOrderPlan> = self
362 .context
363 .sub_plans_table
364 .get_plans(nbr_subgraph)
365 .to_vec();
366 if right_plans.is_empty() || nbr_plans.is_empty() {
367 return false;
368 }
369 let mut combined = right_subgraph.clone();
370 combined.add_subquery_graph(nbr_subgraph);
371 for right_plan in &right_plans {
372 for nbr_plan in &nbr_plans {
373 let (plan, cost, cardinality) =
374 self.append_extend(right_plan, nbr_plan, query_graph);
375 let encoding = self.encode_plan(&plan, query_graph);
376 self.context.sub_plans_table.add_plan(
377 &combined,
378 JoinOrderPlan::new(plan, cost, cardinality, encoding),
379 );
380 }
381 }
382 true
383 }
384
385 fn plan_inner_hash_join(
387 &mut self,
388 right_subgraph: &SubqueryGraph,
389 nbr_subgraph: &SubqueryGraph,
390 query_graph: &Arc<QueryGraph>,
391 ) {
392 if !right_subgraph.is_connected(nbr_subgraph) {
393 return;
394 }
395 let right_plans: Vec<JoinOrderPlan> = self
396 .context
397 .sub_plans_table
398 .get_plans(right_subgraph)
399 .to_vec();
400 let nbr_plans: Vec<JoinOrderPlan> = self
401 .context
402 .sub_plans_table
403 .get_plans(nbr_subgraph)
404 .to_vec();
405 if right_plans.is_empty() || nbr_plans.is_empty() {
406 return;
407 }
408 let mut combined = right_subgraph.clone();
409 combined.add_subquery_graph(nbr_subgraph);
410 for right_plan in &right_plans {
411 for nbr_plan in &nbr_plans {
412 let (plan, cost, cardinality) = self.append_inner_hash_join(
413 right_plan,
414 nbr_plan,
415 right_subgraph,
416 nbr_subgraph,
417 query_graph,
418 );
419 let encoding = self.encode_plan(&plan, query_graph);
420 self.context.sub_plans_table.add_plan(
421 &combined,
422 JoinOrderPlan::new(plan, cost, cardinality, encoding),
423 );
424 }
425 }
426 }
427
428 pub(crate) fn append_inner_hash_join(
430 &mut self,
431 probe: &JoinOrderPlan,
432 build: &JoinOrderPlan,
433 probe_subgraph: &SubqueryGraph,
434 build_subgraph: &SubqueryGraph,
435 query_graph: &Arc<QueryGraph>,
436 ) -> (LogicalNodeEnum, u64, u64) {
437 let shared = probe_subgraph.get_connected_node_positions(build_subgraph);
438 let mut hash_keys = Vec::new();
439 let mut probe_keys = Vec::new();
440 let mut domains = Vec::new();
441 for node_pos in &shared {
442 if let Some(node) = query_graph.query_nodes.get(*node_pos) {
443 let key = self.context.join_key(&node.variable);
444 domains.push(
445 self.context
446 .cardinality_estimator
447 .get_node_id_domain(key.id()),
448 );
449 hash_keys.push(key.clone());
450 probe_keys.push(key);
451 }
452 }
453 let cardinality = self.context.cardinality_estimator.estimate_hash_join(
454 probe.cardinality,
455 build.cardinality,
456 &domains,
457 );
458 let join_key_card = domains.into_iter().min().unwrap_or(cardinality);
459 let cost = CostModel::compute_hash_join_cost(
466 probe.cost,
467 probe.cardinality,
468 build.cost,
469 join_key_card,
470 )
471 .saturating_add(cardinality);
472 let mut col_names = probe.plan.col_names().to_vec();
473 for name in build.plan.col_names() {
474 if !col_names.contains(name) {
475 col_names.push(name.clone());
476 }
477 }
478 let plan = LogicalNodeEnum::InnerJoin(LogicalInnerJoinNode {
479 id: next_node_id(),
480 left: Box::new(probe.plan.clone()),
481 right: Box::new(build.plan.clone()),
482 hash_keys,
483 probe_keys,
484 recommended_algorithm: None,
485 output_var: None,
486 col_names,
487 column_types: vec![],
488 });
489 (plan, cost, cardinality)
490 }
491
492 fn append_extend(
497 &mut self,
498 probe: &JoinOrderPlan,
499 build: &JoinOrderPlan,
500 _query_graph: &Arc<QueryGraph>,
501 ) -> (LogicalNodeEnum, u64, u64) {
502 let fanout = build
503 .cardinality
504 .min(self.context.cardinality_estimator.avg_degree_hint());
505 let cardinality = self
506 .context
507 .cardinality_estimator
508 .estimate_extend(probe.cardinality, fanout);
509 let cost = CostModel::compute_extend_cost(probe.cost, probe.cardinality)
515 .saturating_add(build.cost)
516 .saturating_add(cardinality);
517 let mut col_names = probe.plan.col_names().to_vec();
518 for name in build.plan.col_names() {
519 if !col_names.contains(name) {
520 col_names.push(name.clone());
521 }
522 }
523 let plan = LogicalNodeEnum::InnerJoin(LogicalInnerJoinNode {
524 id: next_node_id(),
525 left: Box::new(probe.plan.clone()),
526 right: Box::new(build.plan.clone()),
527 hash_keys: vec![],
528 probe_keys: vec![],
529 recommended_algorithm: None,
530 output_var: None,
531 col_names,
532 column_types: vec![],
533 });
534 (plan, cost, cardinality)
535 }
536
537 pub fn plan_with_hint(
547 &mut self,
548 query_graph: &Arc<QueryGraph>,
549 hint: &JoinHint,
550 ) -> Result<LogicalNodeEnum, JoinHintError> {
551 self.enumerate_levels(query_graph);
554 let tree = JoinTreeConstructor::construct(query_graph, hint)?;
555 let (plan, _subgraph, _cost, _card) = self.solve_tree_node(&tree.root, query_graph)?;
556 Ok(plan)
557 }
558}
559
560mod dp;
561mod encoding;
562#[cfg(test)]
563mod tests;