This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 84c3fd0d38a branch-4.2 [fix](fe) Prune empty and cascading CTE plans 
(#62828, #61146) (#69308)
84c3fd0d38a is described below

commit 84c3fd0d38ae497db8d0304ef93779863d169637
Author: minghong <[email protected]>
AuthorDate: Tue Oct 20 16:15:43 2026 +0800

    branch-4.2 [fix](fe) Prune empty and cascading CTE plans (#62828, #61146) 
(#69308)
    
    ### What problem does this PR solve?
    
    Issue Number: DORIS-29796
    
    Related PR: #62828 (source of the pick), #61146 (source of the
    dependency fix), #68211
    
    Problem Summary: backport the CTE empty/cascading pruning fix #62828 to
    branch-4.2, plus the
    `ColumnPruning` CTE protection that #62828 implicitly depends on.
    Without it the three branch-4.2
    RQG cases tracked by DORIS-29796 (6540158413 / DORIS-25384, 6540160970 /
    DORIS-25408,
    6540160107 / DORIS-25409) keep failing.
    
    #### Why #62828 alone is not enough on branch-4.2
    
    `ColumnPruning` on branch-4.2 only protects `LogicalCTEProducer` with
    `skipPruneThis` and has no
    `visitLogicalCTEAnchor`. When the required slots of an anchor do not
    include the producer outputs
    (the normal case, because consumers reference the producer slots
    directly), the producer subtree is
    pruned with an empty required set and `LogicalProject` materializes the
    empty projection as the
    dummy `1 AS 1` projection. The rewrite plan therefore becomes
    
    ```
    LogicalCteAnchor
    |--LogicalProject ( projects=[1 AS 1] )
    |  +--LogicalCteProducer
    +--...
    ```
    
    which (a) breaks the `anchor.left() instanceof LogicalCTEProducer`
    invariant that #62828's
    `normalizeCtePlan`/`refreshCteContext` relies on (`ClassCastException`),
    and (b) is an invalid CTE
    structure: the materialized columns no longer match the slot mapping the
    consumers were built from,
    which is what surfaces as `VSlotRef ... have invalid slot id` in case
    6540160107.
    
    Master has had the protection since #61146 (`visitLogicalCTEAnchor` +
    `skipPruneThisAndFirstLevelChildren`); this PR ports exactly that hunk.
    
    #### Hunk audit of the #62828 pick (source commit `c75eba5488f`)
    
    | Source file | Disposition |
    | --- | --- |
    | `nereids/jobs/executor/Optimizer.java` | Ported verbatim
    (`normalizeCtePlan`, `refreshCteContext`, `containsCte`,
    `generateFilterPushedDownInlinedPlan`, method rename). |
    | `nereids/rules/rewrite/CTEInliner.java` | Ported verbatim
    (`inlineByCurrentConsumerCount`, `InlineResult`, zero-consumer anchor
    removal, recursive anchor trimming). |
    | `nereids/StatementContext.java` | Not applied: branch-4.2 already has
    `clearCteEnvironment()` from the multi-mode CTE inline work (#68211),
    and its variant also clears `consumerIdToLimitRows`. |
    | `nereids/rules/rewrite/ClearContextStatus.java` | Not applied: 4.2
    keeps the explicit per-map clears, which are a superset of what the
    picked code needs. |
    | `qe/SessionVariable.java` | Not applied: 4.2 keeps
    `@VariableMgr.VarAttr(..., alias = "cbo_cte_inline_mode")`; the source
    commit only dropped the alias. |
    | `.../CTEInlineTest.java` | Ported verbatim, including the new
    `refreshCteConsumersAfterNormalizeEliminatesEmptyBranch`. |
    | `regression-test/suites/query_p0/cte/test_cbo_cte_inline_prune.groovy`
    | Ported verbatim, path adapted to the 4.2 layout
    (`suites/nereids_p0/cte/`; `suites/query_p0/cte` does not exist on 4.2).
    |
    | `regression-test/data/query_p0/...` baselines | Paths adapted to the
    4.2 layout (`data/nereids_p0/repeat/`,
    `data/nereids_rules_p0/unique_function/`); content unchanged. |
    
    The dependency fix is a separate commit, ported from `5de1ed88d4b`
    (#61146) and limited to the
    `ColumnPruning` hunk (12 added lines): CTE anchors and producers are no
    longer column pruned and
    keep their first level children's full output requirement.
    
    No BE changes.
    
    #### Isolation matrix (measured: which commit fixes which case)
    
    FE was rebuilt with subsets of the two commits (BE is unaffected by
    both), and the three RQG cases
    were replayed each time:
    
    | build | 6540158413 | 6540160970 | 6540160107 |
    | --- | --- | --- | --- |
    | branch-4.2, neither commit | BE `Check failed:
    thrift_sink.multi_cast_stream_sink.sinks.size() > 0 (0 vs. 0)` -> query
    fails | same | `VSlotRef ... invalid slot id` |
    | #62828 only | 0 rows | 0 rows | `ClassCastException: LogicalProject
    cannot be cast to LogicalCTEProducer` |
    | ColumnPruning only | same BE CHECK as baseline | same | **0 rows** |
    | this PR (both) | 0 rows | 0 rows | 0 rows |
    
    The three failures therefore come from two independent branch-4.2
    defects: the missing
    empty/zero-consumer CTE pruning (#62828) for 6540158413 / 6540160970,
    and the missing
    `ColumnPruning` CTE protection (#61146) for 6540160107. #62828 alone
    cannot fix the third case, and
    picking it without the pruning fix would turn that case into a
    `ClassCastException`.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test
    - [x] FE unit test: `./run-fe-ut.sh --run
    org.apache.doris.nereids.rules.rewrite.CTEInlineTest`
          → `Tests run: 2, Failures: 0, Errors: 0` (includes the new
          `refreshCteConsumersAfterNormalizeEliminatesEmptyBranch`).
    - [x] FE build: `./build.sh --fe` → `BUILD SUCCESS` on this branch
    (checkstyle included).
    - [x] Original RQG cases replayed against a local 4.2 cluster (FE+BE
    built from this branch,
    BE alive), using the full DDL/INSERT/session settings from the
    DORIS-29796 attachment
    `repro-cte-empty-42.sql` (database renamed to a hyphen-free name so
    branch-4.2's parser
          accepts it; no other change):
          | case | expected | branch-4.2 before | this branch |
          | --- | --- | --- | --- |
    | 6540158413 | 0 rows | `size of sources must be greater than 0` | 0
    rows, no error |
    | 6540160970 | 0 rows | `size of sources must be greater than 0` | 0
    rows, no error |
    | 6540160107 | 0 rows | `VSlotRef col_int_undef_signed have invalid slot
    id: 15` | 0 rows, no error |
    With only the #62828 commit replayed, cases 6540158413/6540160970 return
    0 rows and
    6540160107 still fails (`ClassCastException: LogicalProject cannot be
    cast to
    LogicalCTEProducer`), which is why the `ColumnPruning` commit is part of
    this PR.
    - [x] Candidate 6540166225 from the DORIS-29796 evidence could not be
    replayed: the evidence
    attachment only carries its error text, not its SQL, so there is no
    input to verify locally.
        - [x] Regression (local 4.2 cluster, JDBC `127.0.0.1:29731`):
    - `-d nereids_p0/cte` → 9 suites, failed 0 (includes the new
    `test_cbo_cte_inline_prune`)
    - `-s test_repeat_output_slot` → pass (baseline updated by the pick)
    - `-s agg_with_unique_function` → pass (baseline updated by the pick)
            - `-d nereids_rules_p0/cte` → 3 suites, failed 0
            - `-d nereids_rules_p0/cte_limit_pushdown` → 1 suite, failed 0
            - `-d nereids_rules_p0/column_pruning` → 9 suites, failed 0
    - Introspective analysis
    - [x] `normalizeCtePlan` iterates to a fixpoint instead of comparing
    `Plan.equals()`, because
    `LogicalCTEAnchor`/`LogicalSubQueryAlias` ignore children in `equals()`.
    - [x] Zero-consumer anchors are removed before memoization; anchors with
    an empty producer or
          consumers below `inline_cte_referenced_threshold` are inlined.
    - [x] CTE environment (`cteIdToProducer`, `cteIdToConsumers`, rewritten
    caches) is refreshed
    from the normalized plan so memo construction does not see stale CTE
    state.
---
 .../doris/nereids/jobs/executor/Optimizer.java     |  78 ++++++++++++--
 .../doris/nereids/rules/rewrite/CTEInliner.java    |  86 +++++++++++++++-
 .../doris/nereids/rules/rewrite/ColumnPruning.java |  13 ++-
 .../doris/nereids/rules/rewrite/CTEInlineTest.java |  38 +++++++
 .../nereids_p0/repeat/test_repeat_output_slot.out  |  28 ++---
 .../unique_function/agg_with_unique_function.out   |   8 +-
 .../cte/test_cbo_cte_inline_prune.groovy           | 113 +++++++++++++++++++++
 7 files changed, 326 insertions(+), 38 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Optimizer.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Optimizer.java
index 9d19fb0a664..3ddf1ee1db9 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Optimizer.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Optimizer.java
@@ -18,6 +18,7 @@
 package org.apache.doris.nereids.jobs.executor;
 
 import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.StatementContext;
 import org.apache.doris.nereids.jobs.cascades.DeriveStatsJob;
 import org.apache.doris.nereids.jobs.cascades.OptimizeGroupJob;
 import org.apache.doris.nereids.jobs.joinorder.JoinOrderJob;
@@ -32,6 +33,9 @@ import 
org.apache.doris.nereids.rules.rewrite.EliminateEmptyRelation;
 import org.apache.doris.nereids.rules.rewrite.EliminateUnnecessaryProject;
 import org.apache.doris.nereids.stats.MemoStatsAndCostRecomputer;
 import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEAnchor;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEProducer;
 import org.apache.doris.nereids.trees.plans.logical.LogicalEmptyRelation;
 import org.apache.doris.nereids.util.MoreFieldsThread;
 import org.apache.doris.qe.ConnectContext;
@@ -62,6 +66,12 @@ public class Optimizer {
      */
     public void execute() {
         MoreFieldsThread.keepFunctionSignature(() -> {
+            Plan rewritePlan = cascadesContext.getRewritePlan();
+            if (containsCte(rewritePlan)) {
+                Plan normalizedPlan = normalizeCtePlan(rewritePlan);
+                cascadesContext.setRewritePlan(normalizedPlan);
+                refreshCteContext(normalizedPlan);
+            }
             // generate inlined CTE alternative for CBO comparison
             Plan cboInlinedPlan = generateCTEInlineAlternative();
             // init memo
@@ -163,9 +173,9 @@ public class Optimizer {
     private Plan generateFullCTEInline() {
         Plan rewritePlan = cascadesContext.getRewritePlan();
         CTEInliner cteInliner = new 
CTEInliner(cascadesContext.getStatementContext());
-        Plan inlinedPlan = cteInliner.generateInlinedPlan(rewritePlan);
-        if (inlinedPlan != null) {
-            return rewriteInlinedPlan(inlinedPlan);
+        Plan pushedDownInlinedPlan = 
generateFilterPushedDownInlinedPlan(cteInliner, rewritePlan);
+        if (pushedDownInlinedPlan != null) {
+            return normalizeCtePlan(pushedDownInlinedPlan);
         }
         return null;
     }
@@ -176,22 +186,60 @@ public class Optimizer {
     private Plan generateSelectiveCTEInline() {
         Plan rewritePlan = cascadesContext.getRewritePlan();
         CTEInliner cteInliner = new 
CTEInliner(cascadesContext.getStatementContext(), true);
-        Plan inlinedPlan = cteInliner.generateInlinedPlan(rewritePlan);
-        if (inlinedPlan != null) {
-            inlinedPlan = rewriteInlinedPlan(inlinedPlan);
-            if (inlinedPlan.anyMatch(p -> p instanceof LogicalEmptyRelation)) {
-                inlinedPlan = eliminateEmptyRelation(inlinedPlan);
-                cascadesContext.setRewritePlan(inlinedPlan);
+        Plan pushedDownInlinedPlan = 
generateFilterPushedDownInlinedPlan(cteInliner, rewritePlan);
+        if (pushedDownInlinedPlan != null) {
+            if (pushedDownInlinedPlan.anyMatch(p -> p instanceof 
LogicalEmptyRelation)) {
+                pushedDownInlinedPlan = 
normalizeCtePlan(pushedDownInlinedPlan);
+                cascadesContext.setRewritePlan(pushedDownInlinedPlan);
+                refreshCteContext(pushedDownInlinedPlan);
                 return null;
             }
         }
         return null;
     }
 
+    private Plan normalizeCtePlan(Plan plan) {
+        Plan currentPlan = plan;
+        while (true) {
+            if (currentPlan.anyMatch(p -> p instanceof LogicalEmptyRelation)) {
+                currentPlan = eliminateEmptyRelation(currentPlan);
+            }
+            CTEInliner cteInliner = new 
CTEInliner(cascadesContext.getStatementContext());
+            CTEInliner.InlineResult inlineResult = 
cteInliner.inlineByCurrentConsumerCount(currentPlan);
+            Plan normalizedPlan = inlineResult.getPlan();
+            // Do not use Plan.equals() as a fixpoint check here. Some logical 
nodes,
+            // e.g. LogicalCTEAnchor and LogicalSubQueryAlias, intentionally 
ignore
+            // children in equals(), so a child CTE rewrite under a kept 
parent may be
+            // missed and block cascading consumer-count-based inlining.
+            if (!inlineResult.isChanged()) {
+                return normalizedPlan;
+            }
+            currentPlan = normalizedPlan;
+        }
+    }
+
+    private boolean containsCte(Plan plan) {
+        return plan.anyMatch(p -> p instanceof LogicalCTEAnchor || p 
instanceof LogicalCTEConsumer);
+    }
+
+    private void refreshCteContext(Plan plan) {
+        StatementContext statementContext = 
cascadesContext.getStatementContext();
+        statementContext.clearCteEnvironment();
+        plan.foreach(p -> {
+            if (p instanceof LogicalCTEAnchor) {
+                LogicalCTEAnchor<?, ?> anchor = (LogicalCTEAnchor<?, ?>) p;
+                statementContext.setCteProducer(anchor.getCteId(), 
(LogicalCTEProducer<?>) anchor.left());
+            } else if (p instanceof LogicalCTEConsumer) {
+                cascadesContext.putCTEIdToConsumer((LogicalCTEConsumer) p);
+            }
+            return false;
+        });
+    }
+
     private Plan eliminateEmptyRelation(Plan plan) {
         CascadesContext ctx = CascadesContext.initContext(
                 cascadesContext.getStatementContext(), plan, 
PhysicalProperties.ANY);
-        // Use getCteChildrenRewriter for the same reason as 
rewriteInlinedPlan:
+        // Use getCteChildrenRewriter for the same reason as 
pushDownFilterAndPruneInlinedPlan:
         // getWholeTreeRewriterWithCustomJobs would invoke RewriteCteChildren 
which
         // reads stale rewrittenCteConsumer cache from the main Rewriter phase,
         // reverting the inlined CTE subtrees back to the original structure.
@@ -202,6 +250,14 @@ public class Optimizer {
         return ctx.getRewritePlan();
     }
 
+    private Plan generateFilterPushedDownInlinedPlan(CTEInliner cteInliner, 
Plan rewritePlan) {
+        Plan inlinedPlan = cteInliner.generateInlinedPlan(rewritePlan);
+        if (inlinedPlan == null) {
+            return null;
+        }
+        return pushDownFilterAndPruneInlinedPlan(inlinedPlan);
+    }
+
     /**
      * Run filter pushdown and column pruning on the inlined plan using a 
temporary
      * CascadesContext.
@@ -214,7 +270,7 @@ public class Optimizer {
      * phase. That cached outer query still contains LogicalCTEConsumer nodes 
for the inlined CTE,
      * preventing the filter from ever reaching the inlined union body.
      */
-    private Plan rewriteInlinedPlan(Plan inlinedPlan) {
+    private Plan pushDownFilterAndPruneInlinedPlan(Plan inlinedPlan) {
         CascadesContext inlinedContext = CascadesContext.initContext(
                 cascadesContext.getStatementContext(), inlinedPlan, 
PhysicalProperties.ANY);
         Rewriter.getCteChildrenRewriter(inlinedContext, ImmutableList.of(
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInliner.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInliner.java
index 47ec88c0159..36e082ecb33 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInliner.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInliner.java
@@ -31,6 +31,7 @@ import 
org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
 import org.apache.doris.nereids.trees.plans.logical.LogicalCTEAnchor;
 import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
 import org.apache.doris.nereids.trees.plans.logical.LogicalCTEProducer;
+import org.apache.doris.nereids.trees.plans.logical.LogicalEmptyRelation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
@@ -41,8 +42,10 @@ import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Generate an inlined alternative plan for CTE optimization.
@@ -65,6 +68,7 @@ public class CTEInliner extends DefaultPlanRewriter<Void> {
     private final StatementContext statementContext;
     // Map from CTEId to the CTE producer node (extracted from 
CTEAnchor.left())
     private final Map<CTEId, LogicalCTEProducer<?>> cteProducers = new 
HashMap<>();
+    private final Set<CTEId> cteIdsToRemove = new HashSet<>();
     private final boolean unionAllOnly;
 
     public CTEInliner(StatementContext statementContext) {
@@ -81,6 +85,7 @@ public class CTEInliner extends DefaultPlanRewriter<Void> {
      * Returns null if no CTEs can be inlined.
      */
     public Plan generateInlinedPlan(Plan plan) {
+        clearRewriteCandidates();
         // First pass: collect all CTE producers that can be inlined
         collectCTEProducers(plan);
 
@@ -92,6 +97,44 @@ public class CTEInliner extends DefaultPlanRewriter<Void> {
         return plan.accept(this, null);
     }
 
+    /**
+     * Recursively remove unused CTE anchors and inline CTEs whose live 
consumer count
+     * is small enough after rewrite rules change the plan shape.
+     */
+    public InlineResult inlineByCurrentConsumerCount(Plan plan) {
+        Plan currentPlan = plan;
+        boolean changed = false;
+        while (collectConsumerDrivenCandidates(currentPlan)) {
+            changed = true;
+            currentPlan = currentPlan.accept(this, null);
+        }
+        return new InlineResult(currentPlan, changed);
+    }
+
+    /** Result of one consumer-count-driven CTE normalization round. */
+    public static class InlineResult {
+        private final Plan plan;
+        private final boolean changed;
+
+        public InlineResult(Plan plan, boolean changed) {
+            this.plan = plan;
+            this.changed = changed;
+        }
+
+        public Plan getPlan() {
+            return plan;
+        }
+
+        public boolean isChanged() {
+            return changed;
+        }
+    }
+
+    private void clearRewriteCandidates() {
+        cteProducers.clear();
+        cteIdsToRemove.clear();
+    }
+
     private void collectCTEProducers(Plan plan) {
         plan.foreach(p -> {
             if (p instanceof LogicalCTEAnchor) {
@@ -113,6 +156,40 @@ public class CTEInliner extends DefaultPlanRewriter<Void> {
         });
     }
 
+    private boolean collectConsumerDrivenCandidates(Plan plan) {
+        clearRewriteCandidates();
+        Map<CTEId, LogicalCTEProducer<?>> allCteProducers = new HashMap<>();
+        Map<CTEId, Integer> cteConsumerCounts = new HashMap<>();
+        plan.foreach(p -> {
+            if (p instanceof LogicalCTEAnchor) {
+                LogicalCTEAnchor<?, ?> anchor = (LogicalCTEAnchor<?, ?>) p;
+                allCteProducers.put(anchor.getCteId(), (LogicalCTEProducer<?>) 
anchor.left());
+            } else if (p instanceof LogicalCTEConsumer) {
+                LogicalCTEConsumer consumer = (LogicalCTEConsumer) p;
+                cteConsumerCounts.merge(consumer.getCteId(), 1, Integer::sum);
+            }
+        });
+
+        int threshold = 
statementContext.getConnectContext().getSessionVariable().inlineCTEReferencedThreshold;
+        for (Map.Entry<CTEId, LogicalCTEProducer<?>> entry : 
allCteProducers.entrySet()) {
+            CTEId cteId = entry.getKey();
+            LogicalCTEProducer<?> producer = entry.getValue();
+            int consumerCount = cteConsumerCounts.getOrDefault(cteId, 0);
+            if (consumerCount == 0) {
+                cteIdsToRemove.add(cteId);
+            } else if (producer.child() instanceof LogicalEmptyRelation
+                    || (consumerCount <= threshold && canInline(producer))) {
+                cteProducers.put(cteId, producer);
+            }
+        }
+        return !cteProducers.isEmpty() || !cteIdsToRemove.isEmpty();
+    }
+
+    private boolean canInline(LogicalCTEProducer<?> producer) {
+        return !statementContext.isForceMaterializeCTE(producer.getCteId())
+                && !containsNondeterministicFunction(producer);
+    }
+
     private boolean containsNondeterministicFunction(LogicalCTEProducer<?> 
producer) {
         List<Expression> nondeterministicFunctions = new ArrayList<>();
         producer.accept(NondeterministicFunctionCollector.INSTANCE, 
nondeterministicFunctions);
@@ -127,13 +204,14 @@ public class CTEInliner extends DefaultPlanRewriter<Void> 
{
     @Override
     public Plan visitLogicalCTEAnchor(LogicalCTEAnchor<? extends Plan, ? 
extends Plan> cteAnchor, Void context) {
         CTEId cteId = cteAnchor.getCteId();
-        if (cteProducers.containsKey(cteId)) {
-            // Inline: skip anchor and producer, process the right (consumer) 
subtree
+        if (cteProducers.containsKey(cteId) || cteIdsToRemove.contains(cteId)) 
{
+            // Inline or remove: skip anchor and producer, process the right 
(consumer) subtree
             return cteAnchor.right().accept(this, null);
         } else {
-            // Force materialize: keep the structure, only process the right 
subtree
+            // Keep the structure and continue trimming nested CTEs in both 
children.
+            Plan left = cteAnchor.left().accept(this, null);
             Plan right = cteAnchor.right().accept(this, null);
-            return cteAnchor.withChildren(cteAnchor.left(), right);
+            return cteAnchor.withChildren(left, right);
         }
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ColumnPruning.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ColumnPruning.java
index 4597e3d2707..84e52d6e57b 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ColumnPruning.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ColumnPruning.java
@@ -38,6 +38,7 @@ import org.apache.doris.nereids.trees.plans.algebra.Aggregate;
 import org.apache.doris.nereids.trees.plans.algebra.Project;
 import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
 import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEAnchor;
 import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
 import org.apache.doris.nereids.trees.plans.logical.LogicalCTEProducer;
 import org.apache.doris.nereids.trees.plans.logical.LogicalExcept;
@@ -267,9 +268,15 @@ public class ColumnPruning extends 
DefaultPlanRewriter<PruneContext> implements
         return pruneAggregate(repeat, context);
     }
 
+    @Override
+    public Plan visitLogicalCTEAnchor(LogicalCTEAnchor<? extends Plan, ? 
extends Plan> cteAnchor,
+            PruneContext context) {
+        return skipPruneThisAndFirstLevelChildren(cteAnchor);
+    }
+
     @Override
     public Plan visitLogicalCTEProducer(LogicalCTEProducer<? extends Plan> 
cteProducer, PruneContext context) {
-        return skipPruneThis(cteProducer);
+        return skipPruneThisAndFirstLevelChildren(cteProducer);
     }
 
     @Override
@@ -314,6 +321,10 @@ public class ColumnPruning extends 
DefaultPlanRewriter<PruneContext> implements
         return pruneChildren(plan, plan.getOutputExprIdBitSet());
     }
 
+    private Plan skipPruneThisAndFirstLevelChildren(Plan plan) {
+        return pruneChildren(plan, plan.getChildrenOutputExprIdBitSet());
+    }
+
     // some rules want to match the aggregate which contains all the group by 
keys and aggregate functions
     // in the output of LogicalAggregate, so here we not to prune the group by 
keys in the output of aggregate,
     // for example:
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CTEInlineTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CTEInlineTest.java
index e9767ef524b..7ae9aa1e995 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CTEInlineTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CTEInlineTest.java
@@ -21,7 +21,9 @@ import org.apache.doris.nereids.NereidsPlanner;
 import org.apache.doris.nereids.StatementContext;
 import org.apache.doris.nereids.parser.NereidsParser;
 import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.trees.expressions.CTEId;
 import org.apache.doris.nereids.trees.plans.commands.ExplainCommand;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.nereids.util.MemoPatternMatchSupported;
 import org.apache.doris.nereids.util.MemoTestUtils;
@@ -29,13 +31,24 @@ import org.apache.doris.nereids.util.PlanChecker;
 import org.apache.doris.qe.OriginStatement;
 import org.apache.doris.utframe.TestWithFeService;
 
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.Map;
+import java.util.Set;
+
 public class CTEInlineTest extends TestWithFeService implements 
MemoPatternMatchSupported {
     @Override
     protected void runBeforeAll() throws Exception {
         createDatabase("test");
         connectContext.setDatabase("test");
+        createTable("CREATE TABLE cte_inline_tbl (\n"
+                + "  id int NULL,\n"
+                + "  val int NULL\n"
+                + ") ENGINE=OLAP\n"
+                + "DUPLICATE KEY(id)\n"
+                + "DISTRIBUTED BY HASH(id) BUCKETS 1\n"
+                + "PROPERTIES (\"replication_num\" = \"1\")");
     }
 
     @Test
@@ -81,4 +94,29 @@ public class CTEInlineTest extends TestWithFeService 
implements MemoPatternMatch
                 ).when(cte -> cte.getCteName().equals("yy"))
         );
     }
+
+    @Test
+    public void refreshCteConsumersAfterNormalizeEliminatesEmptyBranch() {
+        int oldCteInlineMode = 
connectContext.getSessionVariable().cteInlineMode;
+        int oldInlineCteReferencedThreshold = 
connectContext.getSessionVariable().inlineCTEReferencedThreshold;
+        connectContext.getSessionVariable().cteInlineMode = 0;
+        connectContext.getSessionVariable().inlineCTEReferencedThreshold = 1;
+        
connectContext.getSessionVariable().setDisableNereidsRules("PRUNE_EMPTY_PARTITION");
+        String sql = "with cte as (select id, val from cte_inline_tbl) "
+                + "select * from cte where id = 1 "
+                + "union all select * from cte where id = 2 "
+                + "union all select * from cte where 1 = 0";
+        try {
+            PlanChecker.from(connectContext).checkPlannerResult(sql, planner 
-> {
+                Map<CTEId, Set<LogicalCTEConsumer>> consumers =
+                        
planner.getCascadesContext().getStatementContext().getCteIdToConsumers();
+                Assertions.assertEquals(1, consumers.size());
+                Assertions.assertEquals(2, 
consumers.values().iterator().next().size());
+            });
+        } finally {
+            connectContext.getSessionVariable().cteInlineMode = 
oldCteInlineMode;
+            connectContext.getSessionVariable().inlineCTEReferencedThreshold = 
oldInlineCteReferencedThreshold;
+            connectContext.getSessionVariable().setDisableNereidsRules("");
+        }
+    }
 }
diff --git a/regression-test/data/nereids_p0/repeat/test_repeat_output_slot.out 
b/regression-test/data/nereids_p0/repeat/test_repeat_output_slot.out
index f8ab9595435..8019e6bf2b5 100644
--- a/regression-test/data/nereids_p0/repeat/test_repeat_output_slot.out
+++ b/regression-test/data/nereids_p0/repeat/test_repeat_output_slot.out
@@ -39,22 +39,17 @@ PhysicalCteAnchor ( cteId=CTEId#0 )
 100000
 
 -- !sql_2_shape --
-PhysicalCteAnchor ( cteId=CTEId#0 )
---PhysicalCteProducer ( cteId=CTEId#0 )
-----hashAgg[GLOBAL]
-------hashAgg[LOCAL]
---------PhysicalProject
-----------PhysicalOlapScan[tbl_test_repeat_output_slot]
---PhysicalResultSink
-----PhysicalProject
-------PhysicalUnion
---------PhysicalProject
-----------filter((GROUPING_PREFIX_col_varchar_50__undef_signed__index_inverted_col_datetime_6__undef_signed_col_varchar_50__undef_signed
 > 0))
-------------hashAgg[GLOBAL]
---------------hashAgg[LOCAL]
-----------------PhysicalRepeat
-------------------PhysicalCteConsumer ( cteId=CTEId#0 )
---------PhysicalEmptyRelation
+PhysicalResultSink
+--PhysicalProject
+----filter((GROUPING_PREFIX_col_varchar_50__undef_signed__index_inverted_col_datetime_6__undef_signed_col_varchar_50__undef_signed
 > 0))
+------hashAgg[GLOBAL]
+--------hashAgg[LOCAL]
+----------PhysicalRepeat
+------------PhysicalProject
+--------------hashAgg[GLOBAL]
+----------------hashAgg[LOCAL]
+------------------PhysicalProject
+--------------------PhysicalOlapScan[tbl_test_repeat_output_slot]
 
 -- !sql_2_result --
 \N     ALL     1       6       \N      \N      \N
@@ -64,4 +59,3 @@ PhysicalCteAnchor ( cteId=CTEId#0 )
 2020-01-04T00:00       ALL     1       6       \N      \N      b
 2020-01-04T00:00       ALL     1       6       \N      \N      b
 2020-01-04T00:00       ALL     1       7       \N      \N      \N
-
diff --git 
a/regression-test/data/nereids_rules_p0/unique_function/agg_with_unique_function.out
 
b/regression-test/data/nereids_rules_p0/unique_function/agg_with_unique_function.out
index dfe9a28c4bb..adc172f81d6 100644
--- 
a/regression-test/data/nereids_rules_p0/unique_function/agg_with_unique_function.out
+++ 
b/regression-test/data/nereids_rules_p0/unique_function/agg_with_unique_function.out
@@ -264,9 +264,7 @@ PhysicalCteAnchor ( cteId=CTEId#0 )
 ----PhysicalProject[a + random(), a + random() + 0, abs(a + random()), sum(a + 
random() + 0), sum(a + random())]
 ------PhysicalQuickSort[MERGE_SORT]
 --------PhysicalQuickSort[LOCAL_SORT]
-----------PhysicalProject[(a + random() + 1.0) AS `(a + random() + 1.0)`, a + 
random(), a + random() + 0, abs(a + random()) AS `abs(a + random())`, sum(a + 
random() + 0), sum(a + random())]
-------------PhysicalUnion
---------------PhysicalEmptyRelation
---------------filter((.a + random() + 0 > 0.01))
-----------------PhysicalCteConsumer ( cteId=CTEId#0 )
+----------PhysicalProject[(a + random() + 1.0) AS `(a + random() + 1.0)`, a + 
random() + 0 AS `a + random() + 0`, a + random() AS `a + random()`, abs(a + 
random()) AS `abs(a + random())`, sum(a + random() + 0) AS `sum(a + random() + 
0)`, sum(a + random()) AS `sum(a + random())`]
+------------filter((.a + random() + 0 > 0.01))
+--------------PhysicalCteConsumer ( cteId=CTEId#0 )
 
diff --git 
a/regression-test/suites/nereids_p0/cte/test_cbo_cte_inline_prune.groovy 
b/regression-test/suites/nereids_p0/cte/test_cbo_cte_inline_prune.groovy
new file mode 100644
index 00000000000..ffd4f2acae4
--- /dev/null
+++ b/regression-test/suites/nereids_p0/cte/test_cbo_cte_inline_prune.groovy
@@ -0,0 +1,113 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_cbo_cte_inline_prune") {
+    sql "DROP TABLE IF EXISTS cte_cbo_inline_tbl"
+    sql """
+        CREATE TABLE cte_cbo_inline_tbl (
+            id INT,
+            val INT
+        ) ENGINE=OLAP
+        DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql "INSERT INTO cte_cbo_inline_tbl VALUES (1, 10), (2, 20), (3, 30)"
+
+    sql "DROP TABLE IF EXISTS cte_cbo_empty_array_tbl"
+    sql """
+        CREATE TABLE cte_cbo_empty_array_tbl (
+            id INT,
+            vals ARRAY<INT>
+        ) ENGINE=OLAP
+        DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+
+    sql "SET cte_inline_mode=1"
+    sql "SET inline_cte_referenced_threshold=1"
+
+    explain {
+        sql "SELECT count(*) FROM cte_cbo_empty_array_tbl"
+        contains("VEMPTYSET")
+    }
+
+    explain {
+        sql """
+            shape plan
+            WITH cte_base AS (
+                SELECT id, val, 1 AS tag FROM cte_cbo_inline_tbl
+            )
+            SELECT * FROM cte_base WHERE tag = 2
+            UNION ALL
+            SELECT * FROM cte_base WHERE tag = 3
+        """
+        contains("PhysicalEmptyRelation")
+        notContains("PhysicalCteProducer")
+    }
+
+    explain {
+        sql """
+            shape plan
+            WITH cte_base AS (
+                SELECT id, val, 1 AS tag FROM cte_cbo_inline_tbl
+            ),
+            cte_keep AS (
+                SELECT id, val FROM cte_base WHERE tag = 1
+            ),
+            cte_drop AS (
+                SELECT id, val FROM cte_base WHERE tag = 2
+            )
+            SELECT * FROM cte_keep
+            UNION ALL
+            SELECT * FROM cte_keep
+            UNION ALL
+            SELECT * FROM cte_drop WHERE 1 = 0
+        """
+        multiContains("PhysicalCteProducer", 0)
+    }
+
+    sql "SET cte_inline_mode=0"
+    explain {
+        sql """
+            shape plan
+            WITH cte_base AS (
+                SELECT id, val, 1 AS tag FROM cte_cbo_inline_tbl
+            ),
+            cte_keep AS (
+                SELECT id, val FROM cte_base WHERE tag = 1
+            ),
+            cte_drop AS (
+                SELECT id, val FROM cte_base WHERE tag = 2
+            ),
+            cte_outer AS (
+                SELECT * FROM cte_keep
+                UNION ALL
+                SELECT * FROM cte_drop WHERE 1 = 0
+            )
+            SELECT * FROM cte_outer
+            UNION ALL
+            SELECT * FROM cte_outer
+        """
+        // cte_outer is still referenced twice and remains materialized, but
+        // eliminating cte_drop makes cte_base have only one live consumer.
+        // The fixpoint check must revisit the kept outer CTE anchor and inline
+        // cte_base; otherwise two producers would remain.
+        multiContains("PhysicalCteProducer", 1)
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to