1use crate::optimizer::cost_based::subquery_unnesting::UnnestDecision;
2use crate::optimizer::cost_based::AggregateContext;
3use crate::optimizer::cost_based::{
4 AggregateStrategySelector, IndexSelector, SortEliminationOptimizer,
5};
6use crate::optimizer::partitioning::PartitioningLayoutInfo;
7use crate::optimizer::stats::StatsView;
8use crate::planning::plan::ExecutionPlan;
9use crate::planning::plan::PlanNodeEnum;
10
11use super::OptimizerEngine;
12use crate::optimizer::error::{OptimizeError, OptimizeResult};
13
14impl OptimizerEngine {
15 pub fn optimize(
16 &self,
17 plan: ExecutionPlan,
18 space: Option<&str>,
19 ) -> OptimizeResult<ExecutionPlan> {
20 self.optimize_with_layout(plan, space, &PartitioningLayoutInfo::default())
21 }
22
23 pub fn optimize_with_layout(
24 &self,
25 plan: ExecutionPlan,
26 space: Option<&str>,
27 layout: &PartitioningLayoutInfo,
28 ) -> OptimizeResult<ExecutionPlan> {
29 let mut current_plan = plan;
30
31 self.maybe_apply_feedback();
32
33 current_plan = self.ensure_logical_plan(current_plan);
34
35 current_plan = self.apply_remove_factorization(current_plan)?;
36
37 if self.enable_heuristic {
38 log::debug!("Starting logical heuristic optimization");
39 current_plan = self.apply_logical_heuristic(current_plan)?;
40 log::debug!("Logical heuristic optimization completed successfully");
41 }
42
43 log::debug!("Starting cost-based optimization");
44 current_plan = self.apply_cost_based(current_plan, space)?;
45 log::debug!("Cost-based optimization completed successfully");
46
47 current_plan = self.apply_factorization(current_plan)?;
48
49 current_plan = self.apply_intersect_to_join_rewrite(current_plan, space);
50
51 current_plan = self.apply_physical_mapping(current_plan);
52
53 if self.enable_heuristic {
54 log::debug!("Starting physical heuristic optimization");
55 current_plan =
56 self.apply_physical_heuristic(current_plan, self.max_heuristic_iterations)?;
57 log::debug!("Physical heuristic optimization completed successfully");
58 }
59
60 current_plan = self.apply_partitioning_selection(current_plan, space, layout);
61
62 {
69 let stats = StatsView::new(&self.stats_manager, space);
70 self.apply_row_estimates(&mut current_plan, &stats);
71 }
72
73 Ok(current_plan)
74 }
75
76 fn ensure_logical_plan(&self, mut plan: ExecutionPlan) -> ExecutionPlan {
77 if plan.logical_plan.is_none() {
78 if let Some(root) = plan.root.as_ref() {
79 let node_type = root.type_name().to_string();
80 match crate::planning::plan::logical_plan::LogicalPlan::from_plan_node(root) {
81 Ok(logical) => {
82 plan.set_logical_plan(logical);
83 }
84 Err(e) => {
85 let msg = format!(
86 "logical_plan fallback failed: {} (factorization skipped, flat execution)",
87 e
88 );
89 log::warn!("LogicalPlan::from_plan_node fallback failed: {}", e);
90 plan.cbo_notes.push(msg.clone());
91 plan.cbo_notes.push(format!(
92 "factorization: logical_plan_fallback_total=1 (node={})",
93 node_type
94 ));
95 if let Some(ref ms) = self.metrics_stats {
96 ms.add_value(linkrs_metrics::MetricType::FactorizationFallbackTotal);
97 }
98 if plan.parallel_fallback_reason.is_empty() {
99 plan.parallel_fallback_reason = msg;
100 } else {
101 plan.parallel_fallback_reason.push_str("; ");
102 plan.parallel_fallback_reason.push_str(&msg);
103 }
104 }
105 }
106 }
107 }
108 plan
109 }
110
111 fn apply_physical_mapping(&self, mut plan: ExecutionPlan) -> ExecutionPlan {
112 let needs_mapping = plan.logical_plan.as_ref().is_some_and(|logical| {
113 crate::planning::physical_mapper::PhysicalMapper::needs_physical_mapping(&logical.root)
114 });
115 if !needs_mapping {
116 return plan;
117 }
118 if let Some(logical) = plan.logical_plan.as_ref() {
119 if let Some(root) = plan.root.take() {
120 let mapped =
121 crate::planning::physical_mapper::PhysicalMapper::map(logical.root.clone());
122 let (merged, notes) =
123 crate::planning::physical_mapper::PhysicalMapper::merge_physical_hints(
124 mapped, root,
125 );
126 if notes.is_empty() {
127 log::debug!("PhysicalMapping: merged logical mapping into physical plan");
128 } else {
129 let fallback_count = notes.len();
130 for note in ¬es {
131 log::warn!("{note}");
132 }
133 plan.cbo_notes.extend(notes);
134 plan.cbo_notes.push(format!(
135 "factorization: physical_mapping_fallback_total={}",
136 fallback_count
137 ));
138 if let Some(ref ms) = self.metrics_stats {
139 ms.add_value_with_amount(
140 linkrs_metrics::MetricType::FactorizationPhysicalMappingFallbackTotal,
141 fallback_count as u64,
142 );
143 }
144 }
145 plan.set_root(merged);
146 }
147 }
148 plan
149 }
150
151 fn apply_partitioning_selection(
152 &self,
153 mut plan: ExecutionPlan,
154 space: Option<&str>,
155 layout: &PartitioningLayoutInfo,
156 ) -> ExecutionPlan {
157 if plan.partition_spec().is_some() {
158 return plan;
159 }
160 let Some(root) = plan.root.as_ref() else {
161 return plan;
162 };
163 let stats = StatsView::new(&self.stats_manager, space);
164 let decision = self
165 .partitioning_planner
166 .decide_with_layout(root, &stats, layout);
167 if let Some(spec) = decision.partition_spec {
168 log::debug!("Selected partition layout: {}", decision.reason);
169 plan.set_partition_spec(spec);
170 } else if !decision.reason.is_empty() {
171 if plan.parallel_fallback_reason.is_empty() {
172 plan.parallel_fallback_reason = decision.reason;
173 } else {
174 plan.parallel_fallback_reason.push_str("; ");
175 plan.parallel_fallback_reason.push_str(&decision.reason);
176 }
177 }
178 plan
179 }
180
181 fn apply_logical_heuristic(&self, mut plan: ExecutionPlan) -> OptimizeResult<ExecutionPlan> {
182 self.logical_heuristic
183 .set_max_iterations(self.max_heuristic_iterations);
184 if let Some(logical) = plan.logical_plan.as_mut() {
185 self.logical_heuristic.optimize(&mut logical.root)?;
186 }
187 Ok(plan)
188 }
189
190 fn apply_physical_heuristic(
191 &self,
192 mut plan: ExecutionPlan,
193 max_iterations: usize,
194 ) -> OptimizeResult<ExecutionPlan> {
195 self.physical_heuristic.set_max_iterations(max_iterations);
196
197 let Some(root) = plan.root.take() else {
198 return Ok(plan);
199 };
200 let result = self
201 .physical_heuristic
202 .optimize(root)
203 .map_err(|e| OptimizeError::HeuristicFailed(e.to_string()))?;
204 if let Ok(mut guard) = self.last_batch_statistics.lock() {
205 *guard = result.batch_statistics.clone();
206 }
207 plan.set_root(result.optimized_plan);
208 Ok(plan)
209 }
210
211 fn apply_cost_based(
212 &self,
213 plan: ExecutionPlan,
214 space: Option<&str>,
215 ) -> OptimizeResult<ExecutionPlan> {
216 let mut plan = plan;
217 let stats = StatsView::new(&self.stats_manager, space);
218
219 if plan.logical_plan().is_some() {
220 self.optimize_logical(&stats, space, &mut plan)?;
221 } else {
222 self.optimize_plan_nodes(&stats, space, &mut plan)?;
223 }
224 Ok(plan)
225 }
226
227 fn optimize_logical(
228 &self,
229 stats: &StatsView,
230 space: Option<&str>,
231 plan: &mut ExecutionPlan,
232 ) -> OptimizeResult<()> {
233 self.apply_unnesting(plan, stats);
234 self.apply_unnesting_logical(plan);
235 self.apply_join_order_logical(stats, space, plan);
236 self.apply_index_selection_logical(space, plan);
237 self.apply_topn_wiring(space, plan, stats);
238 self.apply_topn_wiring_logical(stats, space, plan);
239 self.apply_aggregate_strategy_logical(stats, space, plan);
240 self.apply_precompute_notes(space, plan);
241 Ok(())
242 }
243
244 fn optimize_plan_nodes(
245 &self,
246 stats: &StatsView,
247 space: Option<&str>,
248 plan: &mut ExecutionPlan,
249 ) -> OptimizeResult<()> {
250 self.apply_unnesting(plan, stats);
251
252 if let Some(root) = plan.root.take() {
253 let calculator = self.cost_calculator_for(space);
254 let mut notes = Vec::new();
255 let mut decisions = std::collections::HashMap::new();
256 let rewritten = crate::optimizer::cost_based::join_order_rewriter::
257 walk_and_optimize_joins_with_decisions(
258 &root,
259 stats,
260 &calculator,
261 &mut notes,
262 &mut Some(&mut decisions),
263 );
264 plan.set_root(rewritten);
265 plan.join_algorithms = decisions;
266 plan.cbo_notes.extend(notes);
267 }
268
269 if let Some(root) = plan.root.take() {
270 let selector = IndexSelector::new(
271 self.cost_calculator_for(space),
272 self.selectivity_estimator.clone(),
273 );
274 let mut notes = Vec::new();
275 let rewritten = crate::optimizer::cost_based::index_selection::rewrite_index_scans(
276 &root,
277 &selector,
278 &self.stats_manager,
279 space,
280 &mut notes,
281 );
282 plan.set_root(rewritten);
283 plan.cbo_notes.extend(notes);
284 }
285
286 self.apply_topn_wiring(space, plan, stats);
287
288 if let Some(root) = plan.root.take() {
289 let selector = AggregateStrategySelector::new(self.cost_calculator_for(space));
290 let mut notes = Vec::new();
291 let rewritten = self.select_aggregate_strategies(&root, stats, &selector, &mut notes);
292 plan.set_root(rewritten);
293 plan.cbo_notes.extend(notes);
294 }
295
296 self.apply_precompute_notes(space, plan);
297 Ok(())
298 }
299
300 fn apply_unnesting(&self, plan: &mut ExecutionPlan, stats: &StatsView) {
301 if let Some(root) = plan.root.take() {
302 let mut notes = Vec::new();
303 let rewritten = self.unnest_subqueries(&root, stats, &mut notes);
304 plan.set_root(rewritten);
305 plan.cbo_notes.extend(notes);
306 }
307 }
308
309 fn apply_unnesting_logical(&self, plan: &mut ExecutionPlan) {
310 use crate::optimizer::cost_based::subquery_unnesting::unnest_pattern_applies_logical;
311
312 let Some(logical) = plan.logical_plan().cloned() else {
313 return;
314 };
315 let mut notes = Vec::new();
316 let rewritten = unnest_pattern_applies_logical(logical.root(), &mut notes);
317 plan.cbo_notes.extend(notes);
318 let mut updated_logical = logical;
319 updated_logical.root = rewritten;
320 plan.set_logical_plan(updated_logical);
321 }
322
323 pub(super) fn apply_join_order_logical(
324 &self,
325 stats: &StatsView,
326 space: Option<&str>,
327 plan: &mut ExecutionPlan,
328 ) {
329 use crate::optimizer::cost_based::join_order_rewriter::walk_and_optimize_joins_logical;
330
331 let Some(logical) = plan.logical_plan().cloned() else {
332 return;
333 };
334
335 if logical.join_order_hinted {
336 plan.cbo_notes
337 .push("join_order: kept planning order (user hint, reviewer skipped)".to_string());
338 return;
339 }
340
341 let calculator = self.cost_calculator_for(space);
342 let mut notes = Vec::new();
343 let rewritten_logical =
344 walk_and_optimize_joins_logical(logical.root(), stats, &calculator, &mut notes);
345 plan.cbo_notes.extend(notes);
346
347 let mut updated_logical = logical;
348 updated_logical.root = rewritten_logical;
349 plan.set_logical_plan(updated_logical);
350
351 if let Some(root) = plan.root.take() {
352 let mut scratch = Vec::new();
353 let mut decisions = std::collections::HashMap::new();
354 let rewritten = crate::optimizer::cost_based::join_order_rewriter::
355 walk_and_optimize_joins_with_decisions(
356 &root,
357 stats,
358 &calculator,
359 &mut scratch,
360 &mut Some(&mut decisions),
361 );
362 plan.set_root(rewritten);
363 plan.join_algorithms = decisions;
364 }
365 }
366
367 fn apply_index_selection_logical(&self, space: Option<&str>, plan: &mut ExecutionPlan) {
368 use crate::optimizer::cost_based::index_selection::rewrite_index_scans_logical;
369
370 let Some(logical) = plan.logical_plan().cloned() else {
371 return;
372 };
373
374 let selector = IndexSelector::new(
375 self.cost_calculator_for(space),
376 self.selectivity_estimator.clone(),
377 );
378
379 let mut notes = Vec::new();
380 let rewritten_logical = rewrite_index_scans_logical(
381 logical.root(),
382 &selector,
383 &self.stats_manager,
384 space,
385 &mut notes,
386 );
387 plan.cbo_notes.extend(notes);
388
389 let mut updated_logical = logical;
390 updated_logical.root = rewritten_logical;
391 plan.set_logical_plan(updated_logical);
392
393 if let Some(root) = plan.root.take() {
394 let mut scratch = Vec::new();
395 let rewritten = crate::optimizer::cost_based::index_selection::rewrite_index_scans(
396 &root,
397 &selector,
398 &self.stats_manager,
399 space,
400 &mut scratch,
401 );
402 plan.set_root(rewritten);
403 }
404 }
405
406 fn apply_aggregate_strategy_logical(
407 &self,
408 stats: &StatsView,
409 space: Option<&str>,
410 plan: &mut ExecutionPlan,
411 ) {
412 use crate::optimizer::cost_based::aggregate_strategy::walk_aggregate_strategies_logical;
413
414 let Some(logical) = plan.logical_plan().cloned() else {
415 return;
416 };
417 let selector = AggregateStrategySelector::new(self.cost_calculator_for(space));
418 let mut notes = Vec::new();
419 walk_aggregate_strategies_logical(
420 logical.root(),
421 stats,
422 &selector,
423 &self.selectivity_estimator,
424 &mut notes,
425 );
426 plan.cbo_notes.extend(notes);
427 }
428
429 fn apply_topn_wiring_logical(
430 &self,
431 stats: &StatsView,
432 space: Option<&str>,
433 plan: &mut ExecutionPlan,
434 ) {
435 use crate::optimizer::cost_based::topn_wiring::rewrite_sort_with_limits_logical;
436
437 let Some(logical) = plan.logical_plan().cloned() else {
438 return;
439 };
440 let optimizer = SortEliminationOptimizer::new(self.cost_calculator_for(space));
441 let mut notes = Vec::new();
442 let rewritten = rewrite_sort_with_limits_logical(
443 logical.root(),
444 &optimizer,
445 stats,
446 &self.selectivity_estimator,
447 &mut notes,
448 );
449 plan.cbo_notes.extend(notes);
450 let mut updated_logical = logical;
451 updated_logical.root = rewritten;
452 plan.set_logical_plan(updated_logical);
453 }
454
455 fn apply_topn_wiring(&self, space: Option<&str>, plan: &mut ExecutionPlan, stats: &StatsView) {
456 if let Some(root) = plan.root.take() {
457 let optimizer = SortEliminationOptimizer::new(self.cost_calculator_for(space));
458 let mut notes = Vec::new();
459 let rewritten = crate::optimizer::cost_based::topn_wiring::rewrite_sort_with_limits(
460 &root,
461 &optimizer,
462 stats,
463 &self.selectivity_estimator,
464 &self.cardinality_feedback,
465 &mut notes,
466 );
467 plan.set_root(rewritten);
468 plan.cbo_notes.extend(notes);
469 }
470 }
471
472 fn apply_row_estimates(&self, plan: &mut ExecutionPlan, stats: &StatsView) {
473 if let Some(root) = plan.root.as_ref() {
474 plan.row_estimates =
475 crate::optimizer::cost_based::row_estimates::collect_node_row_estimates(
476 root,
477 stats,
478 &self.selectivity_estimator,
479 );
480 crate::optimizer::cost_based::row_estimates::register_plan_estimates(
481 &self.cardinality_feedback,
482 root,
483 stats,
484 &self.selectivity_estimator,
485 );
486 }
487 }
488
489 fn apply_precompute_notes(&self, space: Option<&str>, plan: &mut ExecutionPlan) {
490 if let Some(root) = plan.root.as_ref() {
491 let optimizer = crate::optimizer::cost_based::expression_precomputation::ExpressionPrecomputationOptimizer::new(self.cost_calculator_for(space));
492 let notes =
493 crate::optimizer::cost_based::precomputation_wiring::collect_precompute_notes(
494 root, &optimizer,
495 );
496 plan.cbo_notes.extend(notes);
497 }
498 }
499
500 fn select_aggregate_strategies(
501 &self,
502 node: &PlanNodeEnum,
503 stats: &StatsView,
504 selector: &AggregateStrategySelector,
505 notes: &mut Vec<String>,
506 ) -> PlanNodeEnum {
507 use crate::optimizer::cost_based::row_estimates::estimate_node_output_rows;
508 use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
509 use PlanNodeEnum::*;
510
511 if let Aggregate(aggregate) = node {
512 let input_rows =
513 estimate_node_output_rows(aggregate.input(), stats, &self.selectivity_estimator);
514 let context = AggregateContext {
515 input_rows,
516 group_keys: aggregate.group_keys().to_vec(),
517 agg_function_count: aggregate.aggregation_functions().len(),
518 memory_limit: 0,
519 input_is_sorted: false,
520 sort_keys_match_group_keys: false,
521 is_deterministic: true,
522 complexity_score: 0,
523 table_name: None,
524 };
525 let decision = selector.select_strategy(stats.space().unwrap_or(""), &context);
526 notes.push(format!(
527 "aggregate: strategy={:?} (reason={:?}, est_rows={})",
528 decision.strategy, decision.reason, decision.estimated_output_rows
529 ));
530 }
531
532 let mut closure =
533 |child: &PlanNodeEnum| self.select_aggregate_strategies(child, stats, selector, notes);
534 crate::optimizer::cost_based::traversal::rewrite_children(node, &mut closure)
535 }
536
537 fn unnest_subqueries(
538 &self,
539 node: &PlanNodeEnum,
540 stats: &StatsView,
541 notes: &mut Vec<String>,
542 ) -> PlanNodeEnum {
543 use PlanNodeEnum::*;
544
545 if let PatternApply(apply) = node {
546 let analysis = self.batch_plan_analyzer.analyze(node);
547 let advice = self.decision_feedback.advice(stats.space().unwrap_or(""));
548 if let UnnestDecision::ShouldUnnest { ref reason, .. } =
549 self.subquery_unnesting_optimizer.should_unnest(
550 apply,
551 &analysis,
552 stats,
553 &self.selectivity_estimator,
554 &self.cardinality_feedback,
555 &advice,
556 )
557 {
558 log::debug!("CBO: unnesting PatternApply -> SemiJoin ({:?})", reason);
559 notes.push(format!("unnest pattern_apply -> semi_join ({:?})", reason));
560 if let Ok(join) = self.subquery_unnesting_optimizer.unnest(apply.clone()) {
561 return self.unnest_subqueries(&join, stats, notes);
562 }
563 }
564 }
565
566 use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
567 macro_rules! rewrite_single {
568 ($n:expr) => {{
569 let mut cloned = $n.clone();
570 let new_input = self.unnest_subqueries(cloned.input(), stats, notes);
571 cloned.set_input(new_input);
572 cloned
573 }};
574 }
575 macro_rules! rewrite_binary {
576 ($n:expr) => {{
577 let mut cloned = $n.clone();
578 let new_left = self.unnest_subqueries(cloned.left_input(), stats, notes);
579 let new_right = self.unnest_subqueries(cloned.right_input(), stats, notes);
580 cloned.set_left_input(new_left);
581 cloned.set_right_input(new_right);
582 cloned
583 }};
584 }
585
586 match node {
587 Project(n) => Project(rewrite_single!(n)),
588 Filter(n) => Filter(rewrite_single!(n)),
589 Sort(n) => Sort(rewrite_single!(n)),
590 Limit(n) => Limit(rewrite_single!(n)),
591 TopN(n) => TopN(rewrite_single!(n)),
592 Sample(n) => Sample(rewrite_single!(n)),
593 Dedup(n) => Dedup(rewrite_single!(n)),
594 Aggregate(n) => Aggregate(rewrite_single!(n)),
595 Window(n) => Window(rewrite_single!(n)),
596
597 InnerJoin(n) => InnerJoin(rewrite_binary!(n)),
598 LeftJoin(n) => LeftJoin(rewrite_binary!(n)),
599 RightJoin(n) => RightJoin(rewrite_binary!(n)),
600 CrossJoin(n) => CrossJoin(rewrite_binary!(n)),
601 FullOuterJoin(n) => FullOuterJoin(rewrite_binary!(n)),
602 SemiJoin(n) => SemiJoin(rewrite_binary!(n)),
603
604 PatternApply(n) => PatternApply(rewrite_single!(n)),
605 CorrelatedApply(n) => CorrelatedApply(rewrite_single!(n)),
606
607 _ => node.clone(),
608 }
609 }
610}