Skip to main content

risingwave_frontend/optimizer/
logical_optimization.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use itertools::Itertools;
16use risingwave_common::bail;
17use thiserror_ext::AsReport as _;
18
19use super::plan_node::{ConventionMarker, Logical, LogicalPlanRef};
20use super::plan_visitor::has_logical_max_one_row;
21use crate::error::Result;
22use crate::expr::NowProcTimeFinder;
23use crate::optimizer::PlanRef;
24use crate::optimizer::heuristic_optimizer::{ApplyOrder, HeuristicOptimizer};
25use crate::optimizer::plan_node::{
26    ColPrunable, ColumnPruningContext, PredicatePushdown, PredicatePushdownContext,
27    VisitExprsRecursive,
28};
29use crate::optimizer::plan_rewriter::ShareSourceRewriter;
30#[cfg(debug_assertions)]
31use crate::optimizer::plan_visitor::InputRefValidator;
32use crate::optimizer::plan_visitor::{
33    HasMaxOneRowApply, PlanCheckApplyEliminationExt, PlanVisitor, has_logical_apply,
34};
35use crate::optimizer::rule::*;
36use crate::utils::Condition;
37use crate::{Binder, Explain, OptimizerContextRef, Planner};
38
39impl<C: ConventionMarker> PlanRef<C> {
40    fn optimize_by_rules_inner(
41        self,
42        heuristic_optimizer: &mut HeuristicOptimizer<'_, C>,
43        stage_name: &str,
44    ) -> Result<PlanRef<C>> {
45        let ctx = self.ctx();
46
47        let result = heuristic_optimizer.optimize(self);
48        let stats = heuristic_optimizer.get_stats();
49
50        if ctx.is_explain_trace() && stats.has_applied_rule() {
51            ctx.trace(format!("{}:", stage_name));
52            ctx.trace(format!("{}", stats));
53            ctx.trace(match &result {
54                Ok(plan) => plan.explain_to_string(),
55                Err(error) => format!("Optimization failed: {}", error.as_report()),
56            });
57        }
58        ctx.add_rule_applied(stats.total_applied());
59
60        result
61    }
62
63    pub(crate) fn optimize_by_rules(
64        self,
65        OptimizationStage {
66            stage_name,
67            rules,
68            apply_order,
69        }: &OptimizationStage<C>,
70    ) -> Result<PlanRef<C>> {
71        self.optimize_by_rules_inner(&mut HeuristicOptimizer::new(apply_order, rules), stage_name)
72    }
73
74    pub(crate) fn optimize_by_rules_until_fix_point(
75        mut self,
76        OptimizationStage {
77            stage_name,
78            rules,
79            apply_order,
80        }: &OptimizationStage<C>,
81    ) -> Result<PlanRef<C>> {
82        loop {
83            let mut heuristic_optimizer = HeuristicOptimizer::new(apply_order, rules);
84            self = self.optimize_by_rules_inner(&mut heuristic_optimizer, stage_name)?;
85            if !heuristic_optimizer.get_stats().has_applied_rule() {
86                return Ok(self);
87            }
88        }
89    }
90}
91
92pub struct OptimizationStage<C: ConventionMarker = Logical> {
93    stage_name: String,
94    rules: Vec<BoxedRule<C>>,
95    apply_order: ApplyOrder,
96}
97
98impl<C: ConventionMarker> OptimizationStage<C> {
99    pub fn new<S>(name: S, rules: Vec<BoxedRule<C>>, apply_order: ApplyOrder) -> Self
100    where
101        S: Into<String>,
102    {
103        OptimizationStage {
104            stage_name: name.into(),
105            rules,
106            apply_order,
107        }
108    }
109}
110
111use std::collections::{HashMap, HashSet};
112use std::sync::LazyLock;
113
114use risingwave_common::id::ObjectId;
115use risingwave_pb::common::PbObjectType;
116use risingwave_sqlparser::ast::Statement;
117
118use crate::optimizer::plan_node::generic::GenericPlanRef;
119use crate::optimizer::plan_visitor::RelationCollectorVisitor;
120use crate::session::SessionImpl;
121
122pub struct LogicalOptimizer {}
123
124static DAG_TO_TREE: LazyLock<OptimizationStage> = LazyLock::new(|| {
125    OptimizationStage::new(
126        "DAG To Tree",
127        vec![DagToTreeRule::create()],
128        ApplyOrder::TopDown,
129    )
130});
131
132static STREAM_GENERATE_SERIES_WITH_NOW: LazyLock<OptimizationStage> = LazyLock::new(|| {
133    OptimizationStage::new(
134        "Convert GENERATE_SERIES Ends With NOW",
135        vec![GenerateSeriesWithNowRule::create()],
136        ApplyOrder::TopDown,
137    )
138});
139
140static TABLE_FUNCTION_CONVERT: LazyLock<OptimizationStage> = LazyLock::new(|| {
141    OptimizationStage::new(
142        "Table Function Convert",
143        vec![
144            // Apply file scan rule first
145            TableFunctionToFileScanRule::create(),
146            // Apply internal backfill progress rule first
147            TableFunctionToInternalBackfillProgressRule::create(),
148            // Apply internal source backfill progress rule next
149            TableFunctionToInternalSourceBackfillProgressRule::create(),
150            // Apply internal get channel delta stats rule next
151            TableFunctionToInternalGetChannelDeltaStatsRule::create(),
152            // Apply postgres query rule next
153            TableFunctionToPostgresQueryRule::create(),
154            // Apply mysql query rule next
155            TableFunctionToMySqlQueryRule::create(),
156            // Apply project set rule last
157            TableFunctionToProjectSetRule::create(),
158        ],
159        ApplyOrder::TopDown,
160    )
161});
162
163static TABLE_FUNCTION_TO_FILE_SCAN: LazyLock<OptimizationStage> = LazyLock::new(|| {
164    OptimizationStage::new(
165        "Table Function To FileScan",
166        vec![TableFunctionToFileScanRule::create()],
167        ApplyOrder::TopDown,
168    )
169});
170
171static TABLE_FUNCTION_TO_POSTGRES_QUERY: LazyLock<OptimizationStage> = LazyLock::new(|| {
172    OptimizationStage::new(
173        "Table Function To PostgresQuery",
174        vec![TableFunctionToPostgresQueryRule::create()],
175        ApplyOrder::TopDown,
176    )
177});
178
179static TABLE_FUNCTION_TO_MYSQL_QUERY: LazyLock<OptimizationStage> = LazyLock::new(|| {
180    OptimizationStage::new(
181        "Table Function To MySQL",
182        vec![TableFunctionToMySqlQueryRule::create()],
183        ApplyOrder::TopDown,
184    )
185});
186
187static TABLE_FUNCTION_TO_INTERNAL_BACKFILL_PROGRESS: LazyLock<OptimizationStage> =
188    LazyLock::new(|| {
189        OptimizationStage::new(
190            "Table Function To Internal Backfill Progress",
191            vec![TableFunctionToInternalBackfillProgressRule::create()],
192            ApplyOrder::TopDown,
193        )
194    });
195
196static TABLE_FUNCTION_TO_INTERNAL_SOURCE_BACKFILL_PROGRESS: LazyLock<OptimizationStage> =
197    LazyLock::new(|| {
198        OptimizationStage::new(
199            "Table Function To Internal Source Backfill Progress",
200            vec![TableFunctionToInternalSourceBackfillProgressRule::create()],
201            ApplyOrder::TopDown,
202        )
203    });
204
205static TABLE_FUNCTION_TO_INTERNAL_GET_CHANNEL_DELTA_STATS: LazyLock<OptimizationStage> =
206    LazyLock::new(|| {
207        OptimizationStage::new(
208            "Table Function To Internal Get Channel Delta Stats",
209            vec![TableFunctionToInternalGetChannelDeltaStatsRule::create()],
210            ApplyOrder::TopDown,
211        )
212    });
213
214static VALUES_EXTRACT_PROJECT: LazyLock<OptimizationStage> = LazyLock::new(|| {
215    OptimizationStage::new(
216        "Values Extract Project",
217        vec![ValuesExtractProjectRule::create()],
218        ApplyOrder::TopDown,
219    )
220});
221
222static SIMPLE_UNNESTING: LazyLock<OptimizationStage> = LazyLock::new(|| {
223    OptimizationStage::new(
224        "Simple Unnesting",
225        vec![
226            // Pull correlated predicates up the algebra tree to unnest simple subquery.
227            PullUpCorrelatedPredicateRule::create(),
228            // Pull correlated project expressions with values to inline scalar subqueries.
229            PullUpCorrelatedProjectValueRule::create(),
230            PullUpCorrelatedPredicateAggRule::create(),
231            // Eliminate max one row
232            MaxOneRowEliminateRule::create(),
233            // Eliminate lateral table-function apply into a unary ProjectSet.
234            ApplyTableFunctionToProjectSetRule::create(),
235            // Convert apply to join.
236            ApplyToJoinRule::create(),
237        ],
238        ApplyOrder::BottomUp,
239    )
240});
241
242static SET_OPERATION_MERGE: LazyLock<OptimizationStage> = LazyLock::new(|| {
243    OptimizationStage::new(
244        "Set Operation Merge",
245        vec![
246            UnionMergeRule::create(),
247            IntersectMergeRule::create(),
248            ExceptMergeRule::create(),
249        ],
250        ApplyOrder::BottomUp,
251    )
252});
253
254static GENERAL_UNNESTING_TRANS_APPLY_WITH_SHARE: LazyLock<OptimizationStage> =
255    LazyLock::new(|| {
256        OptimizationStage::new(
257            "General Unnesting(Translate Apply)",
258            vec![TranslateApplyRule::create(true)],
259            ApplyOrder::TopDown,
260        )
261    });
262
263static GENERAL_UNNESTING_TRANS_APPLY_WITHOUT_SHARE: LazyLock<OptimizationStage> =
264    LazyLock::new(|| {
265        OptimizationStage::new(
266            "General Unnesting(Translate Apply)",
267            vec![TranslateApplyRule::create(false)],
268            ApplyOrder::TopDown,
269        )
270    });
271
272static GENERAL_UNNESTING_PUSH_DOWN_APPLY: LazyLock<OptimizationStage> = LazyLock::new(|| {
273    OptimizationStage::new(
274        "General Unnesting(Push Down Apply)",
275        vec![
276            ApplyEliminateRule::create(),
277            ApplyAggTransposeRule::create(),
278            ApplyDedupTransposeRule::create(),
279            ApplyFilterTransposeRule::create(),
280            ApplyProjectTransposeRule::create(),
281            ApplyProjectSetTransposeRule::create(),
282            ApplyTopNTransposeRule::create(),
283            ApplyLimitTransposeRule::create(),
284            ApplyJoinTransposeRule::create(),
285            ApplyUnionTransposeRule::create(),
286            ApplyOverWindowTransposeRule::create(),
287            ApplyExpandTransposeRule::create(),
288            ApplyHopWindowTransposeRule::create(),
289            CrossJoinEliminateRule::create(),
290            ApplyShareEliminateRule::create(),
291        ],
292        ApplyOrder::TopDown,
293    )
294});
295
296static TO_MULTI_JOIN: LazyLock<OptimizationStage> = LazyLock::new(|| {
297    OptimizationStage::new(
298        "To MultiJoin",
299        vec![MergeMultiJoinRule::create()],
300        ApplyOrder::TopDown,
301    )
302});
303
304static LEFT_DEEP_JOIN_ORDERING: LazyLock<OptimizationStage> = LazyLock::new(|| {
305    OptimizationStage::new(
306        "Join Ordering".to_owned(),
307        vec![LeftDeepTreeJoinOrderingRule::create()],
308        ApplyOrder::TopDown,
309    )
310});
311
312static BUSHY_TREE_JOIN_ORDERING: LazyLock<OptimizationStage> = LazyLock::new(|| {
313    OptimizationStage::new(
314        "Join Ordering".to_owned(),
315        vec![BushyTreeJoinOrderingRule::create()],
316        ApplyOrder::TopDown,
317    )
318});
319
320static FILTER_WITH_NOW_TO_JOIN: LazyLock<OptimizationStage> = LazyLock::new(|| {
321    OptimizationStage::new(
322        "Push down filter with now into a left semijoin",
323        vec![
324            SplitNowAndRule::create(),
325            SplitNowOrRule::create(),
326            FilterWithNowToJoinRule::create(),
327        ],
328        ApplyOrder::TopDown,
329    )
330});
331
332static PUSH_CALC_OF_JOIN: LazyLock<OptimizationStage> = LazyLock::new(|| {
333    OptimizationStage::new(
334        "Push down the calculation of inputs of join's condition",
335        vec![PushCalculationOfJoinRule::create()],
336        ApplyOrder::TopDown,
337    )
338});
339
340static CONVERT_DISTINCT_AGG_FOR_STREAM: LazyLock<OptimizationStage> = LazyLock::new(|| {
341    OptimizationStage::new(
342        "Convert Distinct Aggregation",
343        vec![UnionToDistinctRule::create(), DistinctAggRule::create(true)],
344        ApplyOrder::TopDown,
345    )
346});
347
348static CONVERT_DISTINCT_AGG_FOR_BATCH: LazyLock<OptimizationStage> = LazyLock::new(|| {
349    OptimizationStage::new(
350        "Convert Distinct Aggregation",
351        vec![
352            UnionToDistinctRule::create(),
353            DistinctAggRule::create(false),
354        ],
355        ApplyOrder::TopDown,
356    )
357});
358
359static SIMPLIFY_AGG: LazyLock<OptimizationStage> = LazyLock::new(|| {
360    OptimizationStage::new(
361        "Simplify Aggregation",
362        vec![
363            AggGroupBySimplifyRule::create(),
364            AggCallMergeRule::create(),
365            UnifyFirstLastValueRule::create(),
366        ],
367        ApplyOrder::TopDown,
368    )
369});
370
371static JOIN_COMMUTE: LazyLock<OptimizationStage> = LazyLock::new(|| {
372    OptimizationStage::new(
373        "Join Commute".to_owned(),
374        vec![JoinCommuteRule::create()],
375        ApplyOrder::TopDown,
376    )
377});
378
379static CONSTANT_OUTPUT_REMOVE: LazyLock<OptimizationStage> = LazyLock::new(|| {
380    OptimizationStage::new(
381        "Constant Output Operator Remove",
382        vec![EmptyAggRemoveRule::create()],
383        ApplyOrder::TopDown,
384    )
385});
386
387static PROJECT_REMOVE: LazyLock<OptimizationStage> = LazyLock::new(|| {
388    OptimizationStage::new(
389        "Project Remove",
390        vec![
391            // merge should be applied before eliminate
392            ProjectMergeRule::create(),
393            ProjectEliminateRule::create(),
394            TrivialProjectToValuesRule::create(),
395            UnionInputValuesMergeRule::create(),
396            JoinProjectTransposeRule::create(),
397            // project-join merge should be applied after merge
398            // eliminate and to values
399            ProjectJoinMergeRule::create(),
400            AggProjectMergeRule::create(),
401        ],
402        ApplyOrder::BottomUp,
403    )
404});
405
406static SPLIT_OVER_WINDOW: LazyLock<OptimizationStage> = LazyLock::new(|| {
407    OptimizationStage::new(
408        "Split Over Window",
409        vec![OverWindowSplitRule::create()],
410        ApplyOrder::TopDown,
411    )
412});
413
414// the `OverWindowToTopNRule` need to match the pattern of Proj-Filter-OverWindow so it is
415// 1. conflict with `ProjectJoinMergeRule`, `AggProjectMergeRule` or other rules
416// 2. should be after merge the multiple projects
417static CONVERT_OVER_WINDOW: LazyLock<OptimizationStage> = LazyLock::new(|| {
418    OptimizationStage::new(
419        "Convert Over Window",
420        vec![
421            ProjectMergeRule::create(),
422            ProjectEliminateRule::create(),
423            TrivialProjectToValuesRule::create(),
424            UnionInputValuesMergeRule::create(),
425            OverWindowToAggAndJoinRule::create(),
426            OverWindowToTopNRule::create(),
427        ],
428        ApplyOrder::TopDown,
429    )
430});
431
432// DataFusion cannot apply `OverWindowToTopNRule`
433static CONVERT_OVER_WINDOW_FOR_BATCH: LazyLock<OptimizationStage> = LazyLock::new(|| {
434    OptimizationStage::new(
435        "Convert Over Window",
436        vec![
437            ProjectMergeRule::create(),
438            ProjectEliminateRule::create(),
439            TrivialProjectToValuesRule::create(),
440            UnionInputValuesMergeRule::create(),
441            OverWindowToAggAndJoinRule::create(),
442        ],
443        ApplyOrder::TopDown,
444    )
445});
446
447static MERGE_OVER_WINDOW: LazyLock<OptimizationStage> = LazyLock::new(|| {
448    OptimizationStage::new(
449        "Merge Over Window",
450        vec![OverWindowMergeRule::create()],
451        ApplyOrder::TopDown,
452    )
453});
454
455static REWRITE_LIKE_EXPR: LazyLock<OptimizationStage> = LazyLock::new(|| {
456    OptimizationStage::new(
457        "Rewrite Like Expr",
458        vec![RewriteLikeExprRule::create()],
459        ApplyOrder::TopDown,
460    )
461});
462
463static TOP_N_AGG_ON_INDEX: LazyLock<OptimizationStage> = LazyLock::new(|| {
464    OptimizationStage::new(
465        "TopN/SimpleAgg on Index",
466        vec![
467            TopNProjectTransposeRule::create(),
468            TopNOnIndexRule::create(),
469            MinMaxOnIndexRule::create(),
470        ],
471        ApplyOrder::TopDown,
472    )
473});
474
475static PROJECT_TOP_N_TRANSPOSE: LazyLock<OptimizationStage> = LazyLock::new(|| {
476    OptimizationStage::new(
477        "Project TopN Transpose",
478        vec![ProjectTopNTransposeRule::create()],
479        ApplyOrder::TopDown,
480    )
481});
482
483static ALWAYS_FALSE_FILTER: LazyLock<OptimizationStage> = LazyLock::new(|| {
484    OptimizationStage::new(
485        "Void always-false filter's downstream",
486        vec![AlwaysFalseFilterRule::create()],
487        ApplyOrder::TopDown,
488    )
489});
490
491static LIMIT_PUSH_DOWN: LazyLock<OptimizationStage> = LazyLock::new(|| {
492    OptimizationStage::new(
493        "Push Down Limit",
494        vec![LimitPushDownRule::create()],
495        ApplyOrder::TopDown,
496    )
497});
498
499static PULL_UP_HOP: LazyLock<OptimizationStage> = LazyLock::new(|| {
500    OptimizationStage::new(
501        "Pull Up Hop",
502        vec![PullUpHopRule::create()],
503        ApplyOrder::BottomUp,
504    )
505});
506
507static SET_OPERATION_TO_JOIN: LazyLock<OptimizationStage> = LazyLock::new(|| {
508    OptimizationStage::new(
509        "Set Operation To Join",
510        vec![
511            IntersectToSemiJoinRule::create(),
512            ExceptToAntiJoinRule::create(),
513        ],
514        ApplyOrder::BottomUp,
515    )
516});
517
518static GROUPING_SETS: LazyLock<OptimizationStage> = LazyLock::new(|| {
519    OptimizationStage::new(
520        "Grouping Sets",
521        vec![
522            GroupingSetsToExpandRule::create(),
523            ExpandToProjectRule::create(),
524        ],
525        ApplyOrder::TopDown,
526    )
527});
528
529static COMMON_SUB_EXPR_EXTRACT: LazyLock<OptimizationStage> = LazyLock::new(|| {
530    OptimizationStage::new(
531        "Common Sub Expression Extract",
532        vec![CommonSubExprExtractRule::create()],
533        ApplyOrder::TopDown,
534    )
535});
536
537static LOGICAL_FILTER_EXPRESSION_SIMPLIFY: LazyLock<OptimizationStage> = LazyLock::new(|| {
538    OptimizationStage::new(
539        "Logical Filter Expression Simplify",
540        vec![LogicalFilterExpressionSimplifyRule::create()],
541        ApplyOrder::TopDown,
542    )
543});
544
545static REWRITE_SOURCE_FOR_BATCH: LazyLock<OptimizationStage> = LazyLock::new(|| {
546    OptimizationStage::new(
547        "Rewrite Source For Batch",
548        vec![SourceToKafkaScanRule::create()],
549        ApplyOrder::TopDown,
550    )
551});
552
553static MATERIALIZE_ICEBERG_SCAN: LazyLock<OptimizationStage> = LazyLock::new(|| {
554    OptimizationStage::new(
555        "Materialize Iceberg Scan",
556        vec![
557            // When storage mode is auto, may rewrite Iceberg intermediate scan to Hummock scan based on statistics.
558            IcebergEngineStorageSelectionRule::create(),
559            // This converts LogicalIcebergIntermediateScan to LogicalIcebergScan with anti-joins
560            // for delete files.
561            IcebergIntermediateScanRule::create(),
562        ],
563        ApplyOrder::TopDown,
564    )
565});
566
567static ICEBERG_COUNT_STAR: LazyLock<OptimizationStage> = LazyLock::new(|| {
568    OptimizationStage::new(
569        "Iceberg Count Star Optimization",
570        vec![IcebergCountStarRule::create()],
571        ApplyOrder::BottomUp,
572    )
573});
574
575static TOP_N_TO_VECTOR_SEARCH: LazyLock<OptimizationStage> = LazyLock::new(|| {
576    OptimizationStage::new(
577        "TopN to Vector Search",
578        vec![TopNToVectorSearchRule::create()],
579        ApplyOrder::BottomUp,
580    )
581});
582
583static CORRELATED_TOP_N_TO_VECTOR_SEARCH_FOR_BATCH: LazyLock<OptimizationStage> =
584    LazyLock::new(|| {
585        OptimizationStage::new(
586            "Correlated TopN to Vector Search",
587            vec![CorrelatedTopNToVectorSearchRule::create(true)],
588            ApplyOrder::BottomUp,
589        )
590    });
591
592static CORRELATED_TOP_N_TO_VECTOR_SEARCH_FOR_STREAM: LazyLock<OptimizationStage> =
593    LazyLock::new(|| {
594        OptimizationStage::new(
595            "Correlated TopN to Vector Search",
596            vec![CorrelatedTopNToVectorSearchRule::create(false)],
597            ApplyOrder::BottomUp,
598        )
599    });
600
601static BATCH_MV_SELECTION: LazyLock<OptimizationStage> = LazyLock::new(|| {
602    OptimizationStage::new(
603        "Batch Mv Selection",
604        vec![MvSelectionRule::create()],
605        ApplyOrder::TopDown,
606    )
607});
608
609impl LogicalOptimizer {
610    pub fn predicate_pushdown(
611        plan: LogicalPlanRef,
612        explain_trace: bool,
613        ctx: &OptimizerContextRef,
614    ) -> LogicalPlanRef {
615        // Go through the trait method instead of `PredicatePushdownContext::run` so that the
616        // top-level `check_equivalent_plan` debug check covers the whole pass.
617        let mut pushdown_ctx = PredicatePushdownContext::new(plan.clone());
618        let plan = plan.predicate_pushdown(Condition::true_cond(), &mut pushdown_ctx);
619        if explain_trace {
620            ctx.trace("Predicate Push Down:");
621            ctx.trace(plan.explain_to_string());
622        }
623        plan
624    }
625
626    pub fn subquery_unnesting(
627        mut plan: LogicalPlanRef,
628        enable_share_plan: bool,
629        explain_trace: bool,
630        ctx: &OptimizerContextRef,
631    ) -> Result<LogicalPlanRef> {
632        // Bail our if no apply operators.
633        if !has_logical_apply(plan.clone()) {
634            return Ok(plan);
635        }
636        // Simple Unnesting.
637        plan = plan.optimize_by_rules(&SIMPLE_UNNESTING)?;
638        debug_assert!(!HasMaxOneRowApply().visit(plan.clone()));
639        // Predicate push down before translate apply, because we need to calculate the domain
640        // and predicate push down can reduce the size of domain.
641        plan = Self::predicate_pushdown(plan, explain_trace, ctx);
642        // In order to unnest values with correlated input ref, we need to extract project first.
643        plan = plan.optimize_by_rules(&VALUES_EXTRACT_PROJECT)?;
644        // General Unnesting.
645        // Translate Apply, push Apply down the plan and finally replace Apply with regular inner
646        // join.
647        plan = if enable_share_plan {
648            plan.optimize_by_rules(&GENERAL_UNNESTING_TRANS_APPLY_WITH_SHARE)?
649        } else {
650            plan.optimize_by_rules(&GENERAL_UNNESTING_TRANS_APPLY_WITHOUT_SHARE)?
651        };
652        plan = plan.optimize_by_rules_until_fix_point(&GENERAL_UNNESTING_PUSH_DOWN_APPLY)?;
653
654        // Check if all `Apply`s are eliminated and the subquery is unnested.
655        plan.check_apply_elimination()?;
656
657        Ok(plan)
658    }
659
660    pub fn column_pruning(
661        mut plan: LogicalPlanRef,
662        explain_trace: bool,
663        ctx: &OptimizerContextRef,
664    ) -> LogicalPlanRef {
665        let required_cols = (0..plan.schema().len()).collect_vec();
666        // Go through the trait method instead of `ColumnPruningContext::run` so that the
667        // top-level `check_equivalent_plan` debug check covers the whole pass.
668        let mut column_pruning_ctx = ColumnPruningContext::new(plan.clone());
669        plan = plan.prune_col(&required_cols, &mut column_pruning_ctx);
670        // Column pruning may introduce additional projects, and filter can be pushed again.
671        if explain_trace {
672            ctx.trace("Prune Columns:");
673            ctx.trace(plan.explain_to_string());
674        }
675        plan
676    }
677
678    pub fn inline_now_proc_time(plan: LogicalPlanRef, ctx: &OptimizerContextRef) -> LogicalPlanRef {
679        // If now() and proctime() are not found, bail out.
680        let mut v = NowProcTimeFinder::default();
681        plan.visit_exprs_recursive(&mut v);
682        if !v.has() {
683            return plan;
684        }
685
686        let mut v = ctx.session_ctx().pinned_snapshot().inline_now_proc_time();
687
688        let plan = plan.rewrite_exprs_recursive(&mut v);
689
690        if ctx.is_explain_trace() {
691            ctx.trace("Inline Now and ProcTime:");
692            ctx.trace(plan.explain_to_string());
693        }
694        plan
695    }
696
697    pub fn gen_optimized_logical_plan_for_stream(
698        mut plan: LogicalPlanRef,
699    ) -> Result<LogicalPlanRef> {
700        let ctx = plan.ctx();
701        let explain_trace = ctx.is_explain_trace();
702
703        if explain_trace {
704            ctx.trace("Begin:");
705            ctx.trace(plan.explain_to_string());
706        }
707
708        // Convert grouping sets at first because other agg rule can't handle grouping sets.
709        plan = plan.optimize_by_rules(&GROUPING_SETS)?;
710        // Remove nodes with constant output.
711        plan = plan.optimize_by_rules(&CONSTANT_OUTPUT_REMOVE)?;
712        // Remove project to make common sub-plan sharing easier.
713        plan = plan.optimize_by_rules(&PROJECT_REMOVE)?;
714
715        // If share plan is disable, we need to remove all the share operator generated by the
716        // binder, e.g. CTE and View. However, we still need to share source to ensure self
717        // source join can return correct result.
718        let enable_share_plan = ctx.session_ctx().config().enable_share_plan();
719        if enable_share_plan {
720            // Common sub-plan sharing.
721            plan = plan.common_subplan_sharing();
722            plan = plan.prune_share();
723            if explain_trace {
724                ctx.trace("Common Sub-plan Sharing:");
725                ctx.trace(plan.explain_to_string());
726            }
727        } else {
728            plan = plan.optimize_by_rules(&DAG_TO_TREE)?;
729
730            // Replace source to share source.
731            // Perform share source at the beginning so that we can benefit from predicate pushdown
732            // and column pruning for the share operator.
733            plan = ShareSourceRewriter::share_source(plan);
734            if explain_trace {
735                ctx.trace("Share Source:");
736                ctx.trace(plan.explain_to_string());
737            }
738        }
739        plan = plan.optimize_by_rules(&SET_OPERATION_MERGE)?;
740        plan = plan.optimize_by_rules(&SET_OPERATION_TO_JOIN)?;
741        // Convert `generate_series` ends with `now()` to a `Now` source. Only for streaming mode.
742        // Should be applied before converting table function to project set.
743        plan = plan.optimize_by_rules(&STREAM_GENERATE_SERIES_WITH_NOW)?;
744        // In order to unnest a table function, we need to convert it into a `project_set` first.
745        plan = plan.optimize_by_rules(&TABLE_FUNCTION_CONVERT)?;
746
747        plan = plan.optimize_by_rules(&CORRELATED_TOP_N_TO_VECTOR_SEARCH_FOR_STREAM)?;
748
749        plan = Self::subquery_unnesting(plan, enable_share_plan, explain_trace, &ctx)?;
750        if has_logical_max_one_row(plan.clone()) {
751            // `MaxOneRow` is currently only used for the runtime check of
752            // scalar subqueries, while it's not supported in streaming mode, so
753            // we raise a precise error here.
754            bail!("Scalar subquery might produce more than one row.");
755        }
756
757        // Same to batch plan optimization, this rule shall be applied before
758        // predicate push down
759        plan = plan.optimize_by_rules(&LOGICAL_FILTER_EXPRESSION_SIMPLIFY)?;
760
761        // Predicate Push-down
762        plan = Self::predicate_pushdown(plan, explain_trace, &ctx);
763
764        if plan.ctx().session_ctx().config().enable_join_ordering() {
765            // Merge inner joins and intermediate filters into multijoin
766            // This rule assumes that filters have already been pushed down near to
767            // their relevant joins.
768            plan = plan.optimize_by_rules(&TO_MULTI_JOIN)?;
769
770            // Reorder multijoin into join tree.
771            if plan
772                .ctx()
773                .session_ctx()
774                .config()
775                .streaming_enable_bushy_join()
776            {
777                plan = plan.optimize_by_rules(&BUSHY_TREE_JOIN_ORDERING)?;
778            } else {
779                plan = plan.optimize_by_rules(&LEFT_DEEP_JOIN_ORDERING)?;
780            }
781        }
782
783        // Predicate Push-down: apply filter pushdown rules again since we pullup all join
784        // conditions into a filter above the multijoin.
785        plan = Self::predicate_pushdown(plan, explain_trace, &ctx);
786
787        // For stream, push down predicates with now into a left-semi join
788        plan = plan.optimize_by_rules(&FILTER_WITH_NOW_TO_JOIN)?;
789
790        // Push down the calculation of inputs of join's condition.
791        plan = plan.optimize_by_rules(&PUSH_CALC_OF_JOIN)?;
792
793        plan = plan.optimize_by_rules(&SPLIT_OVER_WINDOW)?;
794        // Must push down predicates again after split over window so that OverWindow can be
795        // optimized to TopN.
796        plan = Self::predicate_pushdown(plan, explain_trace, &ctx);
797        plan = plan.optimize_by_rules(&CONVERT_OVER_WINDOW)?;
798        plan = plan.optimize_by_rules(&MERGE_OVER_WINDOW)?;
799
800        let force_split_distinct_agg = ctx.session_ctx().config().force_split_distinct_agg();
801        // TODO: better naming of the OptimizationStage
802        // Convert distinct aggregates.
803        plan = if force_split_distinct_agg {
804            plan.optimize_by_rules(&CONVERT_DISTINCT_AGG_FOR_BATCH)?
805        } else {
806            plan.optimize_by_rules(&CONVERT_DISTINCT_AGG_FOR_STREAM)?
807        };
808
809        plan = plan.optimize_by_rules(&SIMPLIFY_AGG)?;
810
811        plan = plan.optimize_by_rules(&JOIN_COMMUTE)?;
812
813        // Do a final column pruning and predicate pushing down to clean up the plan.
814        plan = Self::column_pruning(plan, explain_trace, &ctx);
815        plan = Self::predicate_pushdown(plan, explain_trace, &ctx);
816
817        plan = plan.optimize_by_rules(&CONSTANT_OUTPUT_REMOVE)?;
818        plan = plan.optimize_by_rules(&PROJECT_REMOVE)?;
819
820        plan = plan.optimize_by_rules(&COMMON_SUB_EXPR_EXTRACT)?;
821
822        #[cfg(debug_assertions)]
823        InputRefValidator.validate(plan.clone());
824
825        ctx.may_store_explain_logical(&plan);
826
827        Ok(plan)
828    }
829
830    pub fn gen_optimized_logical_plan_for_batch(
831        mut plan: LogicalPlanRef,
832    ) -> Result<LogicalPlanRef> {
833        let ctx = plan.ctx();
834        let explain_trace = ctx.is_explain_trace();
835
836        if explain_trace {
837            ctx.trace("Begin:");
838            ctx.trace(plan.explain_to_string());
839        }
840
841        if ctx.session_ctx().config().enable_mv_selection() {
842            let query_relations =
843                RelationCollectorVisitor::collect_with(HashSet::new(), plan.clone());
844            Self::register_batch_mview_candidates(ctx.session_ctx(), &ctx, &query_relations);
845            plan = plan.optimize_by_rules(&BATCH_MV_SELECTION)?;
846        }
847
848        // Inline `NOW()` and `PROCTIME()`, only for batch queries.
849        plan = Self::inline_now_proc_time(plan, &ctx);
850
851        // Convert the dag back to the tree, because we don't support DAG plan for batch.
852        plan = plan.optimize_by_rules(&DAG_TO_TREE)?;
853
854        plan = plan.optimize_by_rules(&REWRITE_SOURCE_FOR_BATCH)?;
855        plan = plan.optimize_by_rules(&GROUPING_SETS)?;
856        plan = plan.optimize_by_rules(&REWRITE_LIKE_EXPR)?;
857        plan = plan.optimize_by_rules(&SET_OPERATION_MERGE)?;
858        plan = plan.optimize_by_rules(&SET_OPERATION_TO_JOIN)?;
859        plan = plan.optimize_by_rules(&ALWAYS_FALSE_FILTER)?;
860        // Table function should be converted into `file_scan` before `project_set`.
861        plan = plan.optimize_by_rules(&TABLE_FUNCTION_TO_FILE_SCAN)?;
862        plan = plan.optimize_by_rules(&TABLE_FUNCTION_TO_POSTGRES_QUERY)?;
863        plan = plan.optimize_by_rules(&TABLE_FUNCTION_TO_MYSQL_QUERY)?;
864        plan = plan.optimize_by_rules(&TABLE_FUNCTION_TO_INTERNAL_BACKFILL_PROGRESS)?;
865        plan = plan.optimize_by_rules(&TABLE_FUNCTION_TO_INTERNAL_GET_CHANNEL_DELTA_STATS)?;
866        plan = plan.optimize_by_rules(&TABLE_FUNCTION_TO_INTERNAL_SOURCE_BACKFILL_PROGRESS)?;
867        // In order to unnest a table function, we need to convert it into a `project_set` first.
868        plan = plan.optimize_by_rules(&TABLE_FUNCTION_CONVERT)?;
869
870        plan = plan.optimize_by_rules(&CORRELATED_TOP_N_TO_VECTOR_SEARCH_FOR_BATCH)?;
871
872        plan = Self::subquery_unnesting(plan, false, explain_trace, &ctx)?;
873
874        // Filter simplification must be applied before predicate push-down
875        // otherwise the filter for some nodes (e.g., `LogicalScan`)
876        // may not be properly applied.
877        plan = plan.optimize_by_rules(&LOGICAL_FILTER_EXPRESSION_SIMPLIFY)?;
878
879        // Predicate Push-down
880        let mut last_total_rule_applied_before_predicate_pushdown = ctx.total_rule_applied();
881        plan = Self::predicate_pushdown(plan, explain_trace, &ctx);
882
883        if plan.ctx().session_ctx().config().enable_join_ordering() {
884            // Merge inner joins and intermediate filters into multijoin
885            // This rule assumes that filters have already been pushed down near to
886            // their relevant joins.
887            plan = plan.optimize_by_rules(&TO_MULTI_JOIN)?;
888
889            // Reorder multijoin into left-deep join tree.
890            plan = plan.optimize_by_rules(&LEFT_DEEP_JOIN_ORDERING)?;
891        }
892
893        // Predicate Push-down: apply filter pushdown rules again since we pullup all join
894        // conditions into a filter above the multijoin.
895        if last_total_rule_applied_before_predicate_pushdown != ctx.total_rule_applied() {
896            last_total_rule_applied_before_predicate_pushdown = ctx.total_rule_applied();
897            plan = Self::predicate_pushdown(plan, explain_trace, &ctx);
898        }
899
900        // Push down the calculation of inputs of join's condition.
901        plan = plan.optimize_by_rules(&PUSH_CALC_OF_JOIN)?;
902
903        plan = plan.optimize_by_rules(&SPLIT_OVER_WINDOW)?;
904        // Must push down predicates again after split over window so that OverWindow can be
905        // optimized to TopN.
906        if last_total_rule_applied_before_predicate_pushdown != ctx.total_rule_applied() {
907            last_total_rule_applied_before_predicate_pushdown = ctx.total_rule_applied();
908            plan = Self::predicate_pushdown(plan, explain_trace, &ctx);
909        }
910        plan = plan.optimize_by_rules(&CONVERT_OVER_WINDOW_FOR_BATCH)?;
911        plan = plan.optimize_by_rules(&MERGE_OVER_WINDOW)?;
912
913        // Convert distinct aggregates.
914        plan = plan.optimize_by_rules(&CONVERT_DISTINCT_AGG_FOR_BATCH)?;
915
916        plan = plan.optimize_by_rules(&SIMPLIFY_AGG)?;
917
918        plan = plan.optimize_by_rules(&JOIN_COMMUTE)?;
919
920        plan = plan.optimize_by_rules(&TOP_N_TO_VECTOR_SEARCH)?;
921
922        // Do a final column pruning and predicate pushing down to clean up the plan.
923        plan = Self::column_pruning(plan, explain_trace, &ctx);
924        if last_total_rule_applied_before_predicate_pushdown != ctx.total_rule_applied() {
925            let _ = ctx.total_rule_applied();
926            plan = Self::predicate_pushdown(plan, explain_trace, &ctx);
927        }
928
929        // Materialize Iceberg intermediate scans after predicate pushdown and column pruning.
930        plan = plan.optimize_by_rules(&MATERIALIZE_ICEBERG_SCAN)?;
931
932        plan = plan.optimize_by_rules(&CONSTANT_OUTPUT_REMOVE)?;
933        plan = plan.optimize_by_rules(&PROJECT_REMOVE)?;
934
935        plan = plan.optimize_by_rules(&COMMON_SUB_EXPR_EXTRACT)?;
936
937        // This need to be apply after PROJECT_REMOVE to ensure there is no projection between agg and iceberg scan.
938        plan = plan.optimize_by_rules(&ICEBERG_COUNT_STAR)?;
939
940        plan = plan.optimize_by_rules(&PULL_UP_HOP)?;
941
942        plan = plan.optimize_by_rules(&TOP_N_AGG_ON_INDEX)?;
943
944        plan = plan.optimize_by_rules(&PROJECT_TOP_N_TRANSPOSE)?;
945
946        plan = plan.optimize_by_rules(&LIMIT_PUSH_DOWN)?;
947
948        plan = plan.optimize_by_rules(&DAG_TO_TREE)?;
949
950        #[cfg(debug_assertions)]
951        InputRefValidator.validate(plan.clone());
952
953        ctx.may_store_explain_logical(&plan);
954
955        Ok(plan)
956    }
957
958    fn register_batch_mview_candidates(
959        session: &SessionImpl,
960        context: &OptimizerContextRef,
961        query_relations: &HashSet<ObjectId>,
962    ) {
963        let catalog_reader = session.env().catalog_reader().read_guard();
964        let user_reader = session.env().user_info_reader().read_guard();
965        let Some(current_user) = user_reader.get_user_by_name(&session.user_name()) else {
966            return;
967        };
968        let mut mv_dependencies: HashMap<ObjectId, HashSet<ObjectId>> = HashMap::new();
969        let mut mviews_with_source_dependency: HashSet<ObjectId> = HashSet::new();
970        for dep in catalog_reader.iter_object_dependencies() {
971            mv_dependencies
972                .entry(dep.object_id)
973                .or_default()
974                .insert(dep.referenced_object_id);
975            if dep.referenced_object_type == PbObjectType::Source {
976                mviews_with_source_dependency.insert(dep.object_id);
977            }
978        }
979        let db_name = session.database();
980        let Ok(schemas) = catalog_reader.iter_schemas(&db_name) else {
981            return;
982        };
983
984        for schema in schemas {
985            for mv in schema.iter_created_mvs_with_acl(current_user) {
986                let mv_object_id = mv.id().as_object_id();
987                if mviews_with_source_dependency.contains(&mv_object_id) {
988                    continue;
989                }
990                let is_subset = mv_dependencies
991                    .get(&mv_object_id)
992                    .is_some_and(|deps| deps.is_subset(query_relations));
993                if !is_subset {
994                    continue;
995                }
996                let Ok(stmt) = mv.create_sql_ast() else {
997                    continue;
998                };
999                let Statement::CreateView {
1000                    materialized: true,
1001                    query,
1002                    ..
1003                } = stmt
1004                else {
1005                    continue;
1006                };
1007                let mut binder = Binder::new_for_batch(session);
1008                let Ok(bound_query) = binder.bind_query(&query) else {
1009                    continue;
1010                };
1011                let mut planner = Planner::new_for_batch_dql(context.clone());
1012                let Ok(plan_root) = planner.plan_query(bound_query) else {
1013                    continue;
1014                };
1015                context.add_batch_mview_candidate(mv.clone(), plan_root.plan.clone());
1016            }
1017        }
1018    }
1019}