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

englefly pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new c75eba5488f [fix](fe) Prune empty and cascading CTE plans (#62828)
c75eba5488f is described below

commit c75eba5488f893f0679682dfabc088eef60ccfc8
Author: minghong <[email protected]>
AuthorDate: Tue May 19 10:05:49 2026 +0800

    [fix](fe) Prune empty and cascading CTE plans (#62828)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #60601
    
    Problem Summary: Fix optimizer-side CTE pruning so empty-relation
    producers, zero-consumer anchors, and cascading inline opportunities are
    normalized to a fixpoint before memoization, and add regression coverage
    for empty and cascading CTE elimination.
---
 .../org/apache/doris/nereids/StatementContext.java |  11 ++
 .../doris/nereids/jobs/executor/Optimizer.java     |  78 ++++++++++++--
 .../doris/nereids/rules/rewrite/CTEInliner.java    |  86 +++++++++++++++-
 .../nereids/rules/rewrite/ClearContextStatus.java  |   5 +-
 .../java/org/apache/doris/qe/SessionVariable.java  |   2 +-
 .../doris/nereids/rules/rewrite/CTEInlineTest.java |  38 +++++++
 .../unique_function/agg_with_unique_function.out   |   8 +-
 .../query_p0/repeat/test_repeat_output_slot.out    |  28 ++---
 .../query_p0/cte/test_cbo_cte_inline_prune.groovy  | 113 +++++++++++++++++++++
 9 files changed, 327 insertions(+), 42 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
index 3dbf1fc02c7..2af64752cc4 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
@@ -667,6 +667,17 @@ public class StatementContext implements Closeable {
         return rewrittenCteConsumer;
     }
 
+    /** Clear CTE-related rewrite and memo state before rebuilding it from a 
new plan tree. */
+    public void clearCteEnvironment() {
+        cteIdToConsumers.clear();
+        cteIdToOutputIds.clear();
+        cteIdToProducer.clear();
+        consumerIdToFilters.clear();
+        cteIdToConsumerGroup.clear();
+        rewrittenCteProducer.clear();
+        rewrittenCteConsumer.clear();
+    }
+
     /**
      * Snapshot current CTE-related environment for temporary 
rewrite/optimization.
      */
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 9d7f95970c4..b66a6a30c60 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;
@@ -36,6 +37,9 @@ import 
org.apache.doris.nereids.rules.rewrite.EliminateUnnecessaryProject;
 import org.apache.doris.nereids.rules.rewrite.MergeProjectable;
 import 
org.apache.doris.nereids.rules.rewrite.PushDownExpressionsInHashCondition;
 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;
@@ -66,6 +70,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
@@ -195,9 +205,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;
     }
@@ -208,22 +218,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.
@@ -234,6 +282,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.
@@ -246,7 +302,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/ClearContextStatus.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ClearContextStatus.java
index cb5dcf5526a..72cb6328d4a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ClearContextStatus.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ClearContextStatus.java
@@ -31,10 +31,7 @@ public class ClearContextStatus implements CustomRewriter {
 
     @Override
     public Plan rewriteRoot(Plan plan, JobContext jobContext) {
-        
jobContext.getCascadesContext().getStatementContext().getRewrittenCteConsumer().clear();
-        
jobContext.getCascadesContext().getStatementContext().getRewrittenCteProducer().clear();
-        
jobContext.getCascadesContext().getStatementContext().getCteIdToOutputIds().clear();
-        
jobContext.getCascadesContext().getStatementContext().getConsumerIdToFilters().clear();
+        
jobContext.getCascadesContext().getStatementContext().clearCteEnvironment();
         return plan;
     }
 }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java 
b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
index 76c4ab87bf7..54095c9e08a 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
@@ -2577,7 +2577,7 @@ public class SessionVariable implements Serializable, 
Writable {
     @VarAttrDef.VarAttr(name = ENABLE_ORDERED_SCAN_RANGE_LOCATIONS)
     public boolean enableOrderedScanRangeLocations = false;
 
-    @VarAttrDef.VarAttr(name = CTE_INLINE_MODE, alias = "cbo_cte_inline_mode", 
description = {
+    @VarAttrDef.VarAttr(name = CTE_INLINE_MODE, description = {
             "CTE内联模式。<0:禁用; =0:仅当CTE体含UNION ALL且filter可消除部分分支时内联; 
>=1:CBO比较物化与内联",
             "CTE inline mode. <0: disable; =0: only inline when CTE body 
contains UNION ALL "
                     + "and consumer filters can eliminate some union branches; 
"
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_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/data/query_p0/repeat/test_repeat_output_slot.out 
b/regression-test/data/query_p0/repeat/test_repeat_output_slot.out
index f8ab9595435..8019e6bf2b5 100644
--- a/regression-test/data/query_p0/repeat/test_repeat_output_slot.out
+++ b/regression-test/data/query_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/suites/query_p0/cte/test_cbo_cte_inline_prune.groovy 
b/regression-test/suites/query_p0/cte/test_cbo_cte_inline_prune.groovy
new file mode 100644
index 00000000000..ffd4f2acae4
--- /dev/null
+++ b/regression-test/suites/query_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