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