Skip to main content

linkrs_query/optimizer/engine/
optimizer.rs

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        // Row estimates are collected last so the per-node map keys match the
63        // final plan ids: physical mapping and the physical heuristic rebuild
64        // scan/filter nodes with fresh ids after cost-based decisions, and a
65        // mid-pipeline map misses them (scan est_rows=1, filter est_rows=0.5
66        // fallbacks in EXPLAIN). Decisions already consumed direct estimator
67        // calls, so only the writeback map and feedback baselines move here.
68        {
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 &notes {
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}