You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@doris.apache.org by yi...@apache.org on 2026/03/31 06:34:47 UTC

(doris) branch branch-4.1 updated: [Feature](cte) support recursive cte fe part (#61283)

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

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


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 8a2212fc9a8 [Feature](cte) support recursive cte fe part (#61283)
8a2212fc9a8 is described below

commit 8a2212fc9a8a10b0698d2d420f3dba1f89986d09
Author: starocean999 <li...@selectdb.com>
AuthorDate: Tue Mar 31 14:34:39 2026 +0800

    [Feature](cte) support recursive cte fe part (#61283)
    
    ### What problem does this PR solve?
    
    Issue Number: close #xxx
    
    pick https://github.com/apache/doris/pull/58916 fe part
    
    Problem Summary:
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 .../antlr4/org/apache/doris/nereids/DorisLexer.g4  |   1 +
 .../antlr4/org/apache/doris/nereids/DorisParser.g4 |   3 +-
 .../org/apache/doris/analysis/DescriptorTable.java |   5 +
 .../doris/catalog/RecursiveCteTempTable.java       |  35 +
 .../java/org/apache/doris/catalog/TableIf.java     |  19 +-
 .../org/apache/doris/nereids/CascadesContext.java  |  41 +-
 .../glue/translator/PhysicalPlanTranslator.java    | 105 ++-
 .../doris/nereids/parser/LogicalPlanBuilder.java   |  10 +-
 .../apache/doris/nereids/pattern/MemoPatterns.java |  24 +
 .../processor/post/RuntimeFilterPruner.java        |   9 +
 .../processor/pre/PullUpSubqueryAliasToCTE.java    |  16 +-
 .../properties/ChildOutputPropertyDeriver.java     |  20 +
 .../nereids/properties/RequestPropertyDeriver.java |  11 +
 .../org/apache/doris/nereids/rules/RuleSet.java    |   6 +
 .../org/apache/doris/nereids/rules/RuleType.java   |   3 +
 .../doris/nereids/rules/analysis/AnalyzeCTE.java   | 169 +++-
 .../doris/nereids/rules/analysis/BindRelation.java |  36 +-
 .../nereids/rules/analysis/CollectRelation.java    |  11 +-
 .../nereids/rules/analysis/SubExprAnalyzer.java    |   4 +-
 ...eChildToPhysicalRecursiveCteRecursiveChild.java |  37 +
 ...RecursiveCteScanToPhysicalRecursiveCteScan.java |  42 +
 .../LogicalRecursiveCteToPhysicalRecursiveCte.java |  39 +
 .../nereids/rules/rewrite/AdjustNullable.java      |  48 ++
 .../doris/nereids/rules/rewrite/CTEInline.java     |  44 +-
 .../doris/nereids/rules/rewrite/ColumnPruning.java |  55 ++
 .../doris/nereids/stats/StatsCalculator.java       | 109 +++
 .../trees/copier/LogicalPlanDeepCopier.java        |  26 +
 .../apache/doris/nereids/trees/plans/PlanType.java |   6 +
 .../nereids/trees/plans/algebra/RecursiveCte.java  |  36 +
 .../plans/commands/UpdateMvByPartitionCommand.java |   3 +-
 .../worker/job/UnassignedJobBuilder.java           |  13 +
 .../worker/job/UnassignedRecursiveCteScanJob.java  |  65 ++
 .../nereids/trees/plans/logical/LogicalCTE.java    |  27 +-
 .../trees/plans/logical/LogicalRecursiveCte.java   | 284 ++++++
 .../logical/LogicalRecursiveCteRecursiveChild.java | 117 +++
 .../plans/logical/LogicalRecursiveCteScan.java     |  73 ++
 .../trees/plans/logical/LogicalSubQueryAlias.java  |  22 +
 .../trees/plans/physical/PhysicalRecursiveCte.java | 292 +++++++
 .../PhysicalRecursiveCteRecursiveChild.java        | 121 +++
 .../plans/physical/PhysicalRecursiveCteScan.java   |  85 ++
 .../nereids/trees/plans/visitor/PlanVisitor.java   |  22 +
 .../trees/plans/visitor/RelationVisitor.java       |  10 +
 .../org/apache/doris/planner/RecursiveCteNode.java |  87 ++
 .../apache/doris/planner/RecursiveCteScanNode.java | 123 +++
 .../java/org/apache/doris/qe/SessionVariable.java  |   8 +
 .../doris/qe/runtime/ThriftPlansBuilder.java       | 231 ++++-
 .../apache/doris/statistics/StatisticalType.java   |   2 +
 .../nereids/postprocess/RuntimeFilterTest.java     |  29 +
 .../nereids/rules/analysis/AnalyzeCTETest.java     | 219 +++++
 .../doris/nereids/rules/rewrite/CTEInlineTest.java |  84 ++
 .../nereids/rules/rewrite/ColumnPruningTest.java   |  30 +
 .../data/rec_cte_p0/rec_cte/rec_cte.out            | 953 +++++++++++++++++++++
 .../rec_cte_from_ck_doc/rec_cte_from_ck_doc.out    |  29 +
 .../rec_cte_from_duckdb_doc.out                    |  30 +
 .../rec_cte_from_mysql_doc.out                     |  42 +
 .../suites/rec_cte_p0/rec_cte/rec_cte.groovy       | 271 ++++++
 .../rec_cte_from_ck_doc/rec_cte_from_ck_doc.groovy | 144 ++++
 .../rec_cte_from_duckdb_doc.groovy                 | 185 ++++
 .../rec_cte_from_mysql_doc.groovy                  | 140 +++
 .../column_attribute_null_test.groovy              |  94 ++
 .../recursive_cte/complex_rec_cte_test.groovy      | 478 +++++++++++
 .../create_and_insert_select_test.groovy           | 140 +++
 .../suites/recursive_cte/mtmv_and_cte_test.groovy  |  88 ++
 .../suites/recursive_cte/mv_and_cte_test.groovy    |  88 ++
 .../suites/recursive_cte/no_data_test.groovy       |  77 ++
 .../recursive_cte/recursive_100_number_test.groovy | 106 +++
 .../recursive_cte_exception_test.groovy            | 216 +++++
 .../same_data_type_recursive_test.groovy           | 430 ++++++++++
 68 files changed, 6351 insertions(+), 77 deletions(-)

diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4 b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
index 47ee4eea108..2841b31f12c 100644
--- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
+++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisLexer.g4
@@ -463,6 +463,7 @@ REAL: 'REAL';
 REBALANCE: 'REBALANCE';
 RECENT: 'RECENT';
 RECOVER: 'RECOVER';
+RECURSIVE: 'RECURSIVE';
 RECYCLE: 'RECYCLE';
 REFRESH: 'REFRESH';
 REFERENCES: 'REFERENCES';
diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
index eb11ce49cd1..a244e7fd7a6 100644
--- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
+++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
@@ -1272,7 +1272,7 @@ querySpecification
     ;
 
 cte
-    : WITH aliasQuery (COMMA aliasQuery)*
+    : WITH RECURSIVE? aliasQuery (COMMA aliasQuery)*
     ;
 
 aliasQuery
@@ -2256,6 +2256,7 @@ nonReserved
     | RANDOM
     | RECENT
     | RECOVER
+    | RECURSIVE
     | RECYCLE
     | REFRESH
     | REPEATABLE
diff --git a/fe/fe-core/src/main/java/org/apache/doris/analysis/DescriptorTable.java b/fe/fe-core/src/main/java/org/apache/doris/analysis/DescriptorTable.java
index 254edea5fbb..d793c4f70c5 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/DescriptorTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/DescriptorTable.java
@@ -20,6 +20,7 @@
 
 package org.apache.doris.analysis;
 
+import org.apache.doris.catalog.RecursiveCteTempTable;
 import org.apache.doris.catalog.TableIf;
 import org.apache.doris.common.IdGenerator;
 import org.apache.doris.thrift.TDescriptorTable;
@@ -100,6 +101,10 @@ public class DescriptorTable {
         }
 
         for (TableIf tbl : referencedTbls.values()) {
+            if (tbl instanceof RecursiveCteTempTable) {
+                // skip recursive cte temp table
+                continue;
+            }
             result.addToTableDescriptors(tbl.toThrift());
         }
         thriftDescTable = result;
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/RecursiveCteTempTable.java b/fe/fe-core/src/main/java/org/apache/doris/catalog/RecursiveCteTempTable.java
new file mode 100644
index 00000000000..54c819d45dd
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/RecursiveCteTempTable.java
@@ -0,0 +1,35 @@
+// 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.
+
+package org.apache.doris.catalog;
+
+import org.apache.doris.common.SystemIdGenerator;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+public class RecursiveCteTempTable extends Table {
+    public RecursiveCteTempTable(String tableName, List<Column> fullSchema) {
+        super(SystemIdGenerator.getNextId(), tableName, TableType.RECURSIVE_CTE_TEMP_TABLE, fullSchema);
+    }
+
+    @Override
+    public List<String> getFullQualifiers() {
+        return ImmutableList.of(name);
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/TableIf.java b/fe/fe-core/src/main/java/org/apache/doris/catalog/TableIf.java
index 00146b5514a..74fa5229828 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/TableIf.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/TableIf.java
@@ -320,13 +320,13 @@ public interface TableIf {
     default void addForeignConstraint(String name, ImmutableList<String> columns,
             TableIf referencedTable, ImmutableList<String> referencedColumns, boolean replay) {
         Map<String, Constraint> constraintMap = getConstraintsMapUnsafe();
-        ForeignKeyConstraint foreignKeyConstraint =
-                new ForeignKeyConstraint(name, columns, referencedTable, referencedColumns);
+        ForeignKeyConstraint foreignKeyConstraint = new ForeignKeyConstraint(name, columns, referencedTable,
+                referencedColumns);
         checkConstraintNotExistenceUnsafe(name, foreignKeyConstraint, constraintMap);
         PrimaryKeyConstraint requirePrimaryKeyName = new PrimaryKeyConstraint(name,
                 foreignKeyConstraint.getReferencedColumnNames());
-        PrimaryKeyConstraint primaryKeyConstraint =
-                tryGetPrimaryKeyForForeignKeyUnsafe(requirePrimaryKeyName, referencedTable);
+        PrimaryKeyConstraint primaryKeyConstraint = tryGetPrimaryKeyForForeignKeyUnsafe(requirePrimaryKeyName,
+                referencedTable);
         primaryKeyConstraint.addForeignTable(this);
         constraintMap.put(name, foreignKeyConstraint);
         if (!replay) {
@@ -446,10 +446,13 @@ public interface TableIf {
      */
     enum TableType {
         MYSQL, ODBC, OLAP, SCHEMA, INLINE_VIEW, VIEW, BROKER, ELASTICSEARCH, HIVE,
-        @Deprecated ICEBERG, @Deprecated HUDI, JDBC,
+        @Deprecated
+        ICEBERG, @Deprecated
+        HUDI, JDBC,
         TABLE_VALUED_FUNCTION, HMS_EXTERNAL_TABLE, ES_EXTERNAL_TABLE, MATERIALIZED_VIEW, JDBC_EXTERNAL_TABLE,
         ICEBERG_EXTERNAL_TABLE, TEST_EXTERNAL_TABLE, PAIMON_EXTERNAL_TABLE, MAX_COMPUTE_EXTERNAL_TABLE,
-        HUDI_EXTERNAL_TABLE, TRINO_CONNECTOR_EXTERNAL_TABLE, LAKESOUl_EXTERNAL_TABLE, DICTIONARY, DORIS_EXTERNAL_TABLE;
+        HUDI_EXTERNAL_TABLE, TRINO_CONNECTOR_EXTERNAL_TABLE, LAKESOUl_EXTERNAL_TABLE, DICTIONARY, DORIS_EXTERNAL_TABLE,
+        RECURSIVE_CTE_TEMP_TABLE;
 
         public String toEngineName() {
             switch (this) {
@@ -492,6 +495,8 @@ public interface TableIf {
                     return "dictionary";
                 case DORIS_EXTERNAL_TABLE:
                     return "External_Doris";
+                case RECURSIVE_CTE_TEMP_TABLE:
+                    return "RecursiveCteTempTable";
                 default:
                     return null;
             }
@@ -531,6 +536,7 @@ public interface TableIf {
                 case MATERIALIZED_VIEW:
                 case TRINO_CONNECTOR_EXTERNAL_TABLE:
                 case DORIS_EXTERNAL_TABLE:
+                case RECURSIVE_CTE_TEMP_TABLE:
                     return "BASE TABLE";
                 default:
                     return null;
@@ -664,4 +670,3 @@ public interface TableIf {
         return Optional.empty();
     }
 }
-
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/CascadesContext.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/CascadesContext.java
index 6980765049f..1517d6f2a7a 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/CascadesContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/CascadesContext.java
@@ -133,6 +133,8 @@ public class CascadesContext implements ScheduleContext {
     private final boolean isEnableExprTrace;
 
     private int groupExpressionCount = 0;
+    private Optional<String> currentRecursiveCteName;
+    private List<Slot> recursiveCteOutputs;
 
     /**
      * Constructor of OptimizerContext.
@@ -142,7 +144,8 @@ public class CascadesContext implements ScheduleContext {
      */
     private CascadesContext(Optional<CascadesContext> parent, Optional<CTEId> currentTree,
             StatementContext statementContext, Plan plan, Memo memo,
-            CTEContext cteContext, PhysicalProperties requireProperties, boolean isLeadingDisableJoinReorder) {
+            CTEContext cteContext, PhysicalProperties requireProperties, boolean isLeadingDisableJoinReorder,
+            Optional<String> currentRecursiveCteName, List<Slot> recursiveCteOutputs) {
         this.parent = Objects.requireNonNull(parent, "parent should not null");
         this.currentTree = Objects.requireNonNull(currentTree, "currentTree should not null");
         this.statementContext = Objects.requireNonNull(statementContext, "statementContext should not null");
@@ -167,6 +170,8 @@ public class CascadesContext implements ScheduleContext {
             this.isEnableExprTrace = false;
         }
         this.isLeadingDisableJoinReorder = isLeadingDisableJoinReorder;
+        this.currentRecursiveCteName = currentRecursiveCteName;
+        this.recursiveCteOutputs = recursiveCteOutputs;
     }
 
     /** init a temporary context to rewrite expression */
@@ -181,7 +186,7 @@ public class CascadesContext implements ScheduleContext {
         }
         return newContext(Optional.empty(), Optional.empty(),
                 statementContext, DUMMY_PLAN,
-                new CTEContext(), PhysicalProperties.ANY, false);
+                new CTEContext(), PhysicalProperties.ANY, false, Optional.empty(), ImmutableList.of());
     }
 
     /**
@@ -190,24 +195,25 @@ public class CascadesContext implements ScheduleContext {
     public static CascadesContext initContext(StatementContext statementContext,
             Plan initPlan, PhysicalProperties requireProperties) {
         return newContext(Optional.empty(), Optional.empty(), statementContext,
-                initPlan, new CTEContext(), requireProperties, false);
+                initPlan, new CTEContext(), requireProperties, false, Optional.empty(), ImmutableList.of());
     }
 
     /**
      * use for analyze cte. we must pass CteContext from outer since we need to get right scope of cte
      */
     public static CascadesContext newContextWithCteContext(CascadesContext cascadesContext,
-            Plan initPlan, CTEContext cteContext) {
+            Plan initPlan, CTEContext cteContext, Optional<String> currentRecursiveCteName,
+            List<Slot> recursiveCteOutputs) {
         return newContext(Optional.of(cascadesContext), Optional.empty(),
                 cascadesContext.getStatementContext(), initPlan, cteContext, PhysicalProperties.ANY,
-                cascadesContext.isLeadingDisableJoinReorder
-        );
+                cascadesContext.isLeadingDisableJoinReorder, currentRecursiveCteName, recursiveCteOutputs);
     }
 
     public static CascadesContext newCurrentTreeContext(CascadesContext context) {
         return CascadesContext.newContext(context.getParent(), context.getCurrentTree(), context.getStatementContext(),
                 context.getRewritePlan(), context.getCteContext(),
-                context.getCurrentJobContext().getRequiredProperties(), context.isLeadingDisableJoinReorder);
+                context.getCurrentJobContext().getRequiredProperties(), context.isLeadingDisableJoinReorder,
+                Optional.empty(), ImmutableList.of());
     }
 
     /**
@@ -216,14 +222,17 @@ public class CascadesContext implements ScheduleContext {
     public static CascadesContext newSubtreeContext(Optional<CTEId> subtree, CascadesContext context,
             Plan plan, PhysicalProperties requireProperties) {
         return CascadesContext.newContext(Optional.of(context), subtree, context.getStatementContext(),
-                plan, context.getCteContext(), requireProperties, context.isLeadingDisableJoinReorder);
+                plan, context.getCteContext(), requireProperties, context.isLeadingDisableJoinReorder, Optional.empty(),
+                ImmutableList.of());
     }
 
     private static CascadesContext newContext(Optional<CascadesContext> parent, Optional<CTEId> subtree,
             StatementContext statementContext, Plan initPlan, CTEContext cteContext,
-            PhysicalProperties requireProperties, boolean isLeadingDisableJoinReorder) {
+            PhysicalProperties requireProperties, boolean isLeadingDisableJoinReorder,
+            Optional<String> currentRecursiveCteName, List<Slot> recursiveCteOutputs) {
         return new CascadesContext(parent, subtree, statementContext, initPlan, null,
-            cteContext, requireProperties, isLeadingDisableJoinReorder);
+                cteContext, requireProperties, isLeadingDisableJoinReorder, currentRecursiveCteName,
+                recursiveCteOutputs);
     }
 
     public CascadesContext getRoot() {
@@ -250,6 +259,18 @@ public class CascadesContext implements ScheduleContext {
         return isTimeout;
     }
 
+    public Optional<String> getCurrentRecursiveCteName() {
+        return currentRecursiveCteName;
+    }
+
+    public List<Slot> getRecursiveCteOutputs() {
+        return recursiveCteOutputs;
+    }
+
+    public boolean isAnalyzingRecursiveCteAnchorChild() {
+        return currentRecursiveCteName.isPresent() && recursiveCteOutputs.isEmpty();
+    }
+
     /**
      * Init memo with plan
      */
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
index 028007eab6f..d3ffe2491b5 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
@@ -165,6 +165,9 @@ import org.apache.doris.nereids.trees.plans.physical.PhysicalPartitionTopN;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalQuickSort;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCte;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCteRecursiveChild;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCteScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalRelation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalRepeat;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalResultSink;
@@ -218,6 +221,8 @@ import org.apache.doris.planner.OlapTableSink;
 import org.apache.doris.planner.PartitionSortNode;
 import org.apache.doris.planner.PlanFragment;
 import org.apache.doris.planner.PlanNode;
+import org.apache.doris.planner.RecursiveCteNode;
+import org.apache.doris.planner.RecursiveCteScanNode;
 import org.apache.doris.planner.RepeatNode;
 import org.apache.doris.planner.ResultFileSink;
 import org.apache.doris.planner.ResultSink;
@@ -1170,6 +1175,28 @@ public class PhysicalPlanTranslator extends DefaultPlanVisitor<PlanFragment, Pla
         return planFragment;
     }
 
+    @Override
+    public PlanFragment visitPhysicalRecursiveCteScan(PhysicalRecursiveCteScan recursiveCteScan,
+            PlanTranslatorContext context) {
+        TableIf table = recursiveCteScan.getTable();
+        List<Slot> slots = ImmutableList.copyOf(recursiveCteScan.getOutput());
+        TupleDescriptor tupleDescriptor = generateTupleDesc(slots, null, context);
+
+        RecursiveCteScanNode scanNode = new RecursiveCteScanNode(table != null ? table.getName() : "",
+                context.nextPlanNodeId(), tupleDescriptor, context.getScanContext());
+        scanNode.setNereidsId(recursiveCteScan.getId());
+        context.getNereidsIdToPlanNodeIdMap().put(recursiveCteScan.getId(), scanNode.getId());
+        Utils.execWithUncheckedException(scanNode::initScanRangeLocations);
+
+        translateRuntimeFilter(recursiveCteScan, scanNode, context);
+
+        context.addScanNode(scanNode, recursiveCteScan);
+        PlanFragment planFragment = createPlanFragment(scanNode, DataPartition.RANDOM, recursiveCteScan);
+        context.addPlanFragment(planFragment);
+        updateLegacyPlanIdToPhysicalPlan(planFragment.getPlanRoot(), recursiveCteScan);
+        return planFragment;
+    }
+
     private List<Expr> translateToExprs(List<Expression> expressions, PlanTranslatorContext context) {
         List<Expr> exprs = Lists.newArrayListWithCapacity(expressions.size());
         for (Expression expression : expressions) {
@@ -2378,8 +2405,10 @@ public class PhysicalPlanTranslator extends DefaultPlanVisitor<PlanFragment, Pla
             if (inputPlanNode instanceof OlapScanNode) {
                 ((OlapScanNode) inputPlanNode).updateRequiredSlots(context, requiredByProjectSlotIdSet);
             }
-            updateScanSlotsMaterialization((ScanNode) inputPlanNode, requiredSlotIdSet,
-                    requiredByProjectSlotIdSet, context);
+            if (!(inputPlanNode instanceof RecursiveCteScanNode)) {
+                updateScanSlotsMaterialization((ScanNode) inputPlanNode, requiredSlotIdSet,
+                        requiredByProjectSlotIdSet, context);
+            }
         } else {
             if (project.child() instanceof PhysicalDeferMaterializeTopN) {
                 inputFragment.setOutputExprs(allProjectionExprs);
@@ -2392,6 +2421,78 @@ public class PhysicalPlanTranslator extends DefaultPlanVisitor<PlanFragment, Pla
         return inputFragment;
     }
 
+    @Override
+    public PlanFragment visitPhysicalRecursiveCte(PhysicalRecursiveCte recursiveCte, PlanTranslatorContext context) {
+        List<PlanFragment> childrenFragments = new ArrayList<>();
+        for (Plan plan : recursiveCte.children()) {
+            childrenFragments.add(plan.accept(this, context));
+        }
+
+        TupleDescriptor setTuple = generateTupleDesc(recursiveCte.getOutput(), null, context);
+        List<SlotDescriptor> outputSlotDescs = new ArrayList<>(setTuple.getSlots());
+
+        RecursiveCteNode recursiveCteNode = new RecursiveCteNode(context.nextPlanNodeId(), setTuple.getId(),
+                    recursiveCte.getCteName(), recursiveCte.isUnionAll());
+        List<List<Expr>> distributeExprLists = getDistributeExprs(recursiveCte.children().toArray(new Plan[0]));
+        recursiveCteNode.setChildrenDistributeExprLists(distributeExprLists);
+        recursiveCteNode.setNereidsId(recursiveCte.getId());
+        List<List<Expression>> resultExpressionLists = Lists.newArrayList();
+        context.getNereidsIdToPlanNodeIdMap().put(recursiveCte.getId(), recursiveCteNode.getId());
+        for (List<SlotReference> regularChildrenOutput : recursiveCte.getRegularChildrenOutputs()) {
+            resultExpressionLists.add(new ArrayList<>(regularChildrenOutput));
+        }
+
+        for (PlanFragment childFragment : childrenFragments) {
+            recursiveCteNode.addChild(childFragment.getPlanRoot());
+        }
+
+        List<List<Expr>> materializedResultExprLists = Lists.newArrayList();
+        for (int i = 0; i < resultExpressionLists.size(); ++i) {
+            List<Expression> resultExpressionList = resultExpressionLists.get(i);
+            List<Expr> exprList = Lists.newArrayList();
+            Preconditions.checkState(resultExpressionList.size() == outputSlotDescs.size());
+            for (int j = 0; j < resultExpressionList.size(); ++j) {
+                exprList.add(ExpressionTranslator.translate(resultExpressionList.get(j), context));
+                // TODO: reconsider this, we may change nullable info in previous nereids rules not here.
+                outputSlotDescs.get(j)
+                        .setIsNullable(outputSlotDescs.get(j).getIsNullable() || exprList.get(j).isNullable());
+            }
+            materializedResultExprLists.add(exprList);
+        }
+        recursiveCteNode.setMaterializedResultExprLists(materializedResultExprLists);
+        Preconditions.checkState(recursiveCteNode.getMaterializedResultExprLists().size()
+                == recursiveCteNode.getChildren().size());
+
+        PlanFragment recursiveCteFragment;
+        if (childrenFragments.isEmpty()) {
+            recursiveCteFragment = createPlanFragment(recursiveCteNode,
+                    DataPartition.UNPARTITIONED, recursiveCte);
+            context.addPlanFragment(recursiveCteFragment);
+        } else {
+            int childrenSize = childrenFragments.size();
+            recursiveCteFragment = childrenFragments.get(childrenSize - 1);
+            for (int i = childrenSize - 2; i >= 0; i--) {
+                context.mergePlanFragment(childrenFragments.get(i), recursiveCteFragment);
+                for (PlanFragment child : childrenFragments.get(i).getChildren()) {
+                    recursiveCteFragment.addChild(child);
+                }
+            }
+            setPlanRoot(recursiveCteFragment, recursiveCteNode, recursiveCte);
+        }
+
+        recursiveCteFragment.updateDataPartition(DataPartition.UNPARTITIONED);
+        recursiveCteFragment.setOutputPartition(DataPartition.UNPARTITIONED);
+
+        return recursiveCteFragment;
+    }
+
+    @Override
+    public PlanFragment visitPhysicalRecursiveCteRecursiveChild(
+            PhysicalRecursiveCteRecursiveChild<? extends Plan> recursiveChild,
+            PlanTranslatorContext context) {
+        return recursiveChild.child().accept(this, context);
+    }
+
     /**
      * Returns a new fragment with a UnionNode as its root. The data partition of the
      * returned fragment and how the data of the child fragments is consumed depends on the
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index f493f8aa97a..71ea167ce08 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -1163,6 +1163,8 @@ public class LogicalPlanBuilder extends DorisParserBaseVisitor<Object> {
 
     private final Map<Integer, ParserRuleContext> selectHintMap;
 
+    private boolean isInRecursiveCteContext = false;
+
     public LogicalPlanBuilder(Map<Integer, ParserRuleContext> selectHintMap) {
         this.selectHintMap = selectHintMap;
     }
@@ -2376,7 +2378,11 @@ public class LogicalPlanBuilder extends DorisParserBaseVisitor<Object> {
         if (ctx == null) {
             return plan;
         }
-        return new LogicalCTE<>((List) visit(ctx.aliasQuery(), LogicalSubQueryAlias.class), plan);
+        isInRecursiveCteContext = ctx.RECURSIVE() != null;
+        LogicalCTE<Plan> logicalCTE = new LogicalCTE<>(isInRecursiveCteContext,
+                (List) visit(ctx.aliasQuery(), LogicalSubQueryAlias.class), plan);
+        isInRecursiveCteContext = false;
+        return logicalCTE;
     }
 
     /**
@@ -2570,7 +2576,7 @@ public class LogicalPlanBuilder extends DorisParserBaseVisitor<Object> {
     public LogicalPlan visitSetOperation(SetOperationContext ctx) {
         return ParserUtils.withOrigin(ctx, () -> {
 
-            if (ctx.UNION() != null) {
+            if (ctx.UNION() != null && !isInRecursiveCteContext) {
                 Qualifier qualifier = getQualifier(ctx);
                 List<QueryTermContext> contexts = Lists.newArrayList(ctx.right);
                 QueryTermContext current = ctx.left;
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/pattern/MemoPatterns.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/pattern/MemoPatterns.java
index fa9d191f5e2..ec53deafaad 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/pattern/MemoPatterns.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/pattern/MemoPatterns.java
@@ -28,6 +28,7 @@ import org.apache.doris.nereids.trees.plans.logical.LogicalExcept;
 import org.apache.doris.nereids.trees.plans.logical.LogicalIntersect;
 import org.apache.doris.nereids.trees.plans.logical.LogicalLeaf;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCte;
 import org.apache.doris.nereids.trees.plans.logical.LogicalRelation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSetOperation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalUnary;
@@ -205,6 +206,29 @@ public interface MemoPatterns extends Patterns {
                 defaultPromise());
     }
 
+    /**
+     * create a LogicalRecursiveCte pattern.
+     */
+    default PatternDescriptor<LogicalRecursiveCte>
+            logicalRecursiveCte(
+                PatternDescriptor... children) {
+        return new PatternDescriptor(
+                new TypePattern(LogicalRecursiveCte.class,
+                        Arrays.stream(children)
+                                .map(PatternDescriptor::getPattern)
+                                .toArray(Pattern[]::new)),
+                defaultPromise());
+    }
+
+    /**
+     * create a logicalRecursiveCte group.
+     */
+    default PatternDescriptor<LogicalRecursiveCte> logicalRecursiveCte() {
+        return new PatternDescriptor(
+                new TypePattern(LogicalRecursiveCte.class, multiGroup().pattern),
+                defaultPromise());
+    }
+
     /**
      * create a logicalExcept pattern.
      */
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/RuntimeFilterPruner.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/RuntimeFilterPruner.java
index 51a9003b78f..d7e4e5b67cb 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/RuntimeFilterPruner.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/RuntimeFilterPruner.java
@@ -33,6 +33,7 @@ import org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalIntersect;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalLimit;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalNestedLoopJoin;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCte;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalRelation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalSetOperation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalTopN;
@@ -76,6 +77,14 @@ public class RuntimeFilterPruner extends PlanPostProcessor {
         return plan;
     }
 
+    @Override
+    public PhysicalRecursiveCte visitPhysicalRecursiveCte(PhysicalRecursiveCte recursiveCte, CascadesContext context) {
+        for (Plan child : recursiveCte.children()) {
+            child.accept(this, context);
+        }
+        return recursiveCte;
+    }
+
     @Override
     public PhysicalSetOperation visitPhysicalSetOperation(PhysicalSetOperation setOperation, CascadesContext context) {
         for (Plan child : setOperation.children()) {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/pre/PullUpSubqueryAliasToCTE.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/pre/PullUpSubqueryAliasToCTE.java
index 18ac63dc600..6af9bc5e091 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/pre/PullUpSubqueryAliasToCTE.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/pre/PullUpSubqueryAliasToCTE.java
@@ -42,25 +42,25 @@ public class PullUpSubqueryAliasToCTE extends PlanPreprocessor {
 
     @Override
     public Plan visitUnboundResultSink(UnboundResultSink<? extends Plan> unboundResultSink,
-                                       StatementContext context) {
+            StatementContext context) {
         return createCteForRootNode(unboundResultSink, context);
     }
 
     @Override
     public Plan visitUnboundTableSink(UnboundTableSink<? extends Plan> unboundTableSink,
-                                      StatementContext context) {
+            StatementContext context) {
         return createCteForRootNode(unboundTableSink, context);
     }
 
     @Override
     public Plan visitLogicalFileSink(LogicalFileSink<? extends Plan> logicalFileSink,
-                                     StatementContext context) {
+            StatementContext context) {
         return createCteForRootNode(logicalFileSink, context);
     }
 
     @Override
     public Plan visitLogicalSubQueryAlias(LogicalSubQueryAlias<? extends Plan> alias,
-                                          StatementContext context) {
+            StatementContext context) {
         if (findLeadingHintIgnoreSortAndLimit(alias.child())) {
             aliasQueries.add((LogicalSubQueryAlias<Plan>) alias);
             List<String> tableName = new ArrayList<>();
@@ -83,7 +83,8 @@ public class PullUpSubqueryAliasToCTE extends PlanPreprocessor {
             subQueryAliasesOfCte.addAll(logicalCTE.getAliasQueries());
             subQueryAliasesOfCte.addAll(aliasQueries);
             aliasQueries = new ArrayList<>();
-            return new LogicalCTE<>(subQueryAliasesOfCte, (LogicalPlan) newLogicalCTE.child());
+            return new LogicalCTE<>(newLogicalCTE.isRecursiveCte(), subQueryAliasesOfCte,
+                    (LogicalPlan) newLogicalCTE.child());
         }
         return cte;
     }
@@ -97,10 +98,11 @@ public class PullUpSubqueryAliasToCTE extends PlanPreprocessor {
                 subQueryAliases.addAll(logicalCTE.getAliasQueries());
                 subQueryAliases.addAll(aliasQueries);
                 return topPlan.withChildren(
-                        new LogicalCTE<>(subQueryAliases, (LogicalPlan) topPlan.child(0)));
+                        new LogicalCTE<>(logicalCTE.isRecursiveCte(), subQueryAliases,
+                                (LogicalPlan) topPlan.child(0)));
             }
             return topPlan.withChildren(
-                    new LogicalCTE<>(aliasQueries, (LogicalPlan) topPlan.child(0)));
+                    new LogicalCTE<>(false, aliasQueries, (LogicalPlan) topPlan.child(0)));
         }
         return topPlan;
     }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/ChildOutputPropertyDeriver.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/ChildOutputPropertyDeriver.java
index 2a1a54435a6..4c2cbfaf0bc 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/ChildOutputPropertyDeriver.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/ChildOutputPropertyDeriver.java
@@ -52,6 +52,9 @@ import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalOneRowRelation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPartitionTopN;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCte;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCteRecursiveChild;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCteScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalRepeat;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalSetOperation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalSink;
@@ -147,6 +150,11 @@ public class ChildOutputPropertyDeriver extends PlanVisitor<PhysicalProperties,
         return PhysicalProperties.STORAGE_ANY;
     }
 
+    @Override
+    public PhysicalProperties visitPhysicalRecursiveCteScan(PhysicalRecursiveCteScan cteScan, PlanContext context) {
+        return PhysicalProperties.ANY;
+    }
+
     /**
      * TODO return ANY after refactor coordinator
      * return STORAGE_ANY not ANY, in order to generate distribute on jdbc scan.
@@ -484,6 +492,18 @@ public class ChildOutputPropertyDeriver extends PlanVisitor<PhysicalProperties,
         return PhysicalProperties.createHash(request, firstType);
     }
 
+    @Override
+    public PhysicalProperties visitPhysicalRecursiveCte(PhysicalRecursiveCte recursiveCte, PlanContext context) {
+        return PhysicalProperties.GATHER;
+    }
+
+    @Override
+    public PhysicalProperties visitPhysicalRecursiveCteRecursiveChild(
+            PhysicalRecursiveCteRecursiveChild<? extends Plan> recursiveChild,
+            PlanContext context) {
+        return PhysicalProperties.MUST_SHUFFLE;
+    }
+
     @Override
     public PhysicalProperties visitPhysicalUnion(PhysicalUnion union, PlanContext context) {
         if (union.getConstantExprsList().isEmpty()) {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/RequestPropertyDeriver.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/RequestPropertyDeriver.java
index 1b3c07a9c2b..60239acadbc 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/RequestPropertyDeriver.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/properties/RequestPropertyDeriver.java
@@ -57,6 +57,7 @@ import org.apache.doris.nereids.trees.plans.physical.PhysicalNestedLoopJoin;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapTableSink;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPartitionTopN;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCte;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalResultSink;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalSetOperation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalTVFTableSink;
@@ -370,6 +371,16 @@ public class RequestPropertyDeriver extends PlanVisitor<Void, PlanContext> {
         return null;
     }
 
+    @Override
+    public Void visitPhysicalRecursiveCte(PhysicalRecursiveCte recursiveCte, PlanContext context) {
+        List<PhysicalProperties> requestGather = Lists.newArrayListWithCapacity(context.arity());
+        for (int i = context.arity(); i > 0; --i) {
+            requestGather.add(PhysicalProperties.GATHER);
+        }
+        addRequestPropertyToChildren(requestGather);
+        return null;
+    }
+
     @Override
     public Void visitAbstractPhysicalSort(AbstractPhysicalSort<? extends Plan> sort, PlanContext context) {
         if (!sort.getSortPhase().isLocal()) {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleSet.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleSet.java
index d1c9166179c..5b7e23c60f6 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleSet.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleSet.java
@@ -92,6 +92,9 @@ import org.apache.doris.nereids.rules.implementation.LogicalOlapTableSinkToPhysi
 import org.apache.doris.nereids.rules.implementation.LogicalOneRowRelationToPhysicalOneRowRelation;
 import org.apache.doris.nereids.rules.implementation.LogicalPartitionTopNToPhysicalPartitionTopN;
 import org.apache.doris.nereids.rules.implementation.LogicalProjectToPhysicalProject;
+import org.apache.doris.nereids.rules.implementation.LogicalRecursiveCteRecursiveChildToPhysicalRecursiveCteRecursiveChild;
+import org.apache.doris.nereids.rules.implementation.LogicalRecursiveCteScanToPhysicalRecursiveCteScan;
+import org.apache.doris.nereids.rules.implementation.LogicalRecursiveCteToPhysicalRecursiveCte;
 import org.apache.doris.nereids.rules.implementation.LogicalRepeatToPhysicalRepeat;
 import org.apache.doris.nereids.rules.implementation.LogicalResultSinkToPhysicalResultSink;
 import org.apache.doris.nereids.rules.implementation.LogicalSchemaScanToPhysicalSchemaScan;
@@ -209,6 +212,7 @@ public class RuleSet {
             .add(new LogicalJdbcScanToPhysicalJdbcScan())
             .add(new LogicalOdbcScanToPhysicalOdbcScan())
             .add(new LogicalEsScanToPhysicalEsScan())
+            .add(new LogicalRecursiveCteScanToPhysicalRecursiveCteScan())
             .add(new LogicalProjectToPhysicalProject())
             .add(new LogicalLimitToPhysicalLimit())
             .add(new LogicalWindowToPhysicalWindow())
@@ -224,6 +228,8 @@ public class RuleSet {
             .add(SplitAggWithoutDistinct.INSTANCE)
             .add(SplitAggMultiPhase.INSTANCE)
             .add(SplitAggMultiPhaseWithoutGbyKey.INSTANCE)
+            .add(new LogicalRecursiveCteToPhysicalRecursiveCte())
+            .add(new LogicalRecursiveCteRecursiveChildToPhysicalRecursiveCteRecursiveChild())
             .add(new LogicalUnionToPhysicalUnion())
             .add(new LogicalExceptToPhysicalExcept())
             .add(new LogicalIntersectToPhysicalIntersect())
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleType.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleType.java
index 6d7f8f25b23..139b02a23fa 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/RuleType.java
@@ -555,6 +555,7 @@ public enum RuleType {
     LOGICAL_JDBC_SCAN_TO_PHYSICAL_JDBC_SCAN_RULE(RuleTypeClass.IMPLEMENTATION),
     LOGICAL_ODBC_SCAN_TO_PHYSICAL_ODBC_SCAN_RULE(RuleTypeClass.IMPLEMENTATION),
     LOGICAL_ES_SCAN_TO_PHYSICAL_ES_SCAN_RULE(RuleTypeClass.IMPLEMENTATION),
+    LOGICAL_RECURSIVE_CTE_SCAN_TO_PHYSICAL_RECUSIVE_CTE_SCAN_RULE(RuleTypeClass.IMPLEMENTATION),
     LOGICAL_BLACKHOLE_SINK_TO_PHYSICAL_BLACKHOLE_SINK_RULE(RuleTypeClass.IMPLEMENTATION),
     LOGICAL_OLAP_TABLE_SINK_TO_PHYSICAL_OLAP_TABLE_SINK_RULE(RuleTypeClass.IMPLEMENTATION),
     LOGICAL_HIVE_TABLE_SINK_TO_PHYSICAL_HIVE_TABLE_SINK_RULE(RuleTypeClass.IMPLEMENTATION),
@@ -580,6 +581,8 @@ public enum RuleType {
     COUNT_ON_INDEX_WITHOUT_PROJECT(RuleTypeClass.IMPLEMENTATION),
     TWO_PHASE_AGGREGATE_WITH_DISTINCT(RuleTypeClass.IMPLEMENTATION),
     LOGICAL_UNION_TO_PHYSICAL_UNION(RuleTypeClass.IMPLEMENTATION),
+    LOGICAL_RECURSIVE_CTE_TO_PHYSICAL_RECURSIVE_CTE(RuleTypeClass.IMPLEMENTATION),
+    LOGICAL_RECURSIVE_CTE_RECURSIVE_CHILD_TO_PHYSICAL_RECURSIVE_CTE_RECURSIVE_CHILD(RuleTypeClass.IMPLEMENTATION),
     LOGICAL_EXCEPT_TO_PHYSICAL_EXCEPT(RuleTypeClass.IMPLEMENTATION),
     LOGICAL_INTERSECT_TO_PHYSICAL_INTERSECT(RuleTypeClass.IMPLEMENTATION),
     LOGICAL_GENERATE_TO_PHYSICAL_GENERATE(RuleTypeClass.IMPLEMENTATION),
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/AnalyzeCTE.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/AnalyzeCTE.java
index c49b3f01c51..0c65c8af560 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/AnalyzeCTE.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/AnalyzeCTE.java
@@ -23,22 +23,36 @@ import org.apache.doris.nereids.CascadesContext;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.rules.Rule;
 import org.apache.doris.nereids.rules.RuleType;
+import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.CTEId;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
 import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
 import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Nullable;
 import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.algebra.SetOperation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalCTE;
 import org.apache.doris.nereids.trees.plans.logical.LogicalCTEAnchor;
 import org.apache.doris.nereids.trees.plans.logical.LogicalCTEProducer;
 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.LogicalRecursiveCte;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCteRecursiveChild;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCteScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias;
+import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
+import org.apache.doris.nereids.trees.plans.logical.ProjectProcessor;
+import org.apache.doris.nereids.types.DataType;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -70,7 +84,7 @@ public class AnalyzeCTE extends OneAnalysisRuleFactory {
             // step 1. analyzed all cte plan
             Pair<CTEContext, List<LogicalCTEProducer<Plan>>> result = analyzeCte(logicalCTE, ctx.cascadesContext);
             CascadesContext outerCascadesCtx = CascadesContext.newContextWithCteContext(
-                    ctx.cascadesContext, logicalCTE.child(), result.first);
+                    ctx.cascadesContext, logicalCTE.child(), result.first, Optional.empty(), ImmutableList.of());
             outerCascadesCtx.withPlanProcess(ctx.cascadesContext.showPlanProcess(), () -> {
                 outerCascadesCtx.newAnalyzer().analyze();
             });
@@ -95,27 +109,146 @@ public class AnalyzeCTE extends OneAnalysisRuleFactory {
         List<LogicalCTEProducer<Plan>> cteProducerPlans = new ArrayList<>();
         for (LogicalSubQueryAlias<Plan> aliasQuery : aliasQueries) {
             // we should use a chain to ensure visible of cte
-            LogicalPlan parsedCtePlan = (LogicalPlan) aliasQuery.child();
-            CascadesContext innerCascadesCtx = CascadesContext.newContextWithCteContext(
-                    cascadesContext, parsedCtePlan, outerCteCtx);
-            innerCascadesCtx.withPlanProcess(cascadesContext.showPlanProcess(), () -> {
-                innerCascadesCtx.newAnalyzer().analyze();
-            });
-            cascadesContext.addPlanProcesses(innerCascadesCtx.getPlanProcesses());
-            LogicalPlan analyzedCtePlan = (LogicalPlan) innerCascadesCtx.getRewritePlan();
-            checkColumnAlias(aliasQuery, analyzedCtePlan.getOutput());
-            CTEId cteId = StatementScopeIdGenerator.newCTEId();
-            LogicalSubQueryAlias<Plan> logicalSubQueryAlias =
-                    aliasQuery.withChildren(ImmutableList.of(analyzedCtePlan));
-            outerCteCtx = new CTEContext(cteId, logicalSubQueryAlias, outerCteCtx);
-            outerCteCtx.setAnalyzedPlan(logicalSubQueryAlias);
-            LogicalCTEProducer<Plan> cteProducer = new LogicalCTEProducer<>(cteId, logicalSubQueryAlias);
-            cascadesContext.getStatementContext().setCteProducer(cteId, cteProducer);
-            cteProducerPlans.add(cteProducer);
+            if (aliasQuery.isRecursiveCte() && logicalCTE.isRecursiveCte()) {
+                Pair<CTEContext, LogicalCTEProducer<Plan>> result = analyzeRecursiveCte(aliasQuery, outerCteCtx,
+                        cascadesContext);
+                outerCteCtx = result.first;
+                cteProducerPlans.add(result.second);
+            } else {
+                LogicalPlan parsedCtePlan = (LogicalPlan) aliasQuery.child();
+                CascadesContext innerCascadesCtx = CascadesContext.newContextWithCteContext(
+                        cascadesContext, parsedCtePlan, outerCteCtx, Optional.empty(), ImmutableList.of());
+                innerCascadesCtx.withPlanProcess(cascadesContext.showPlanProcess(), () -> {
+                    innerCascadesCtx.newAnalyzer().analyze();
+                });
+                cascadesContext.addPlanProcesses(innerCascadesCtx.getPlanProcesses());
+                LogicalPlan analyzedCtePlan = (LogicalPlan) innerCascadesCtx.getRewritePlan();
+                checkColumnAlias(aliasQuery, analyzedCtePlan.getOutput());
+                CTEId cteId = StatementScopeIdGenerator.newCTEId();
+                LogicalSubQueryAlias<Plan> logicalSubQueryAlias = aliasQuery
+                        .withChildren(ImmutableList.of(analyzedCtePlan));
+                outerCteCtx = new CTEContext(cteId, logicalSubQueryAlias, outerCteCtx);
+                outerCteCtx.setAnalyzedPlan(logicalSubQueryAlias);
+                cteProducerPlans.add(new LogicalCTEProducer<>(cteId, logicalSubQueryAlias));
+            }
         }
         return Pair.of(outerCteCtx, cteProducerPlans);
     }
 
+    private Pair<CTEContext, LogicalCTEProducer<Plan>> analyzeRecursiveCte(LogicalSubQueryAlias<Plan> aliasQuery,
+            CTEContext outerCteCtx, CascadesContext cascadesContext) {
+        Preconditions.checkArgument(aliasQuery.isRecursiveCte(), "alias query must be recursive cte");
+        LogicalPlan parsedCtePlan = (LogicalPlan) aliasQuery.child();
+        if (!(parsedCtePlan instanceof LogicalUnion) || parsedCtePlan.children().size() != 2) {
+            throw new AnalysisException(String.format("recursive cte must be union, don't support %s",
+                    parsedCtePlan.getClass().getSimpleName()));
+        }
+        // analyze anchor child, its output list will be recursive cte temp table's schema
+        LogicalPlan anchorChild = (LogicalPlan) parsedCtePlan.child(0);
+        CascadesContext innerAnchorCascadesCtx = CascadesContext.newContextWithCteContext(
+                cascadesContext, anchorChild, outerCteCtx, Optional.of(aliasQuery.getAlias()), ImmutableList.of());
+        innerAnchorCascadesCtx.withPlanProcess(cascadesContext.showPlanProcess(), () -> {
+            innerAnchorCascadesCtx.newAnalyzer().analyze();
+        });
+        cascadesContext.addPlanProcesses(innerAnchorCascadesCtx.getPlanProcesses());
+        LogicalPlan analyzedAnchorChild = (LogicalPlan) innerAnchorCascadesCtx.getRewritePlan();
+        Set<LogicalRecursiveCteScan> recursiveCteScans = analyzedAnchorChild
+                .collect(LogicalRecursiveCteScan.class::isInstance);
+        for (LogicalRecursiveCteScan cteScan : recursiveCteScans) {
+            if (cteScan.getTable().getName().equalsIgnoreCase(aliasQuery.getAlias())) {
+                throw new AnalysisException(
+                        String.format("recursive reference to query %s must not appear within its non-recursive term",
+                                aliasQuery.getAlias()));
+            }
+        }
+        checkColumnAlias(aliasQuery, analyzedAnchorChild.getOutput());
+        // make all output nullable
+        analyzedAnchorChild = forceOutputNullable(analyzedAnchorChild,
+                aliasQuery.getColumnAliases().orElse(ImmutableList.of()));
+        // analyze recursive child
+        LogicalPlan recursiveChild = (LogicalPlan) parsedCtePlan.child(1);
+        CascadesContext innerRecursiveCascadesCtx = CascadesContext.newContextWithCteContext(
+                cascadesContext, recursiveChild, outerCteCtx, Optional.of(aliasQuery.getAlias()),
+                analyzedAnchorChild.getOutput());
+        innerRecursiveCascadesCtx.withPlanProcess(cascadesContext.showPlanProcess(), () -> {
+            innerRecursiveCascadesCtx.newAnalyzer().analyze();
+        });
+        cascadesContext.addPlanProcesses(innerRecursiveCascadesCtx.getPlanProcesses());
+        LogicalPlan analyzedRecursiveChild = (LogicalPlan) innerRecursiveCascadesCtx.getRewritePlan();
+        List<LogicalRecursiveCteScan> recursiveCteScanList = analyzedRecursiveChild
+                .collectToList(LogicalRecursiveCteScan.class::isInstance);
+        if (recursiveCteScanList.size() > 1) {
+            throw new AnalysisException(String.format("recursive reference to query %s must not appear more than once",
+                    aliasQuery.getAlias()));
+        }
+        List<Slot> anchorChildOutputs = analyzedAnchorChild.getOutput();
+        List<DataType> anchorChildOutputTypes = new ArrayList<>(anchorChildOutputs.size());
+        for (Slot slot : anchorChildOutputs) {
+            anchorChildOutputTypes.add(slot.getDataType());
+        }
+        List<Slot> recursiveChildOutputs = analyzedRecursiveChild.getOutput();
+        for (int i = 0; i < recursiveChildOutputs.size(); ++i) {
+            if (!recursiveChildOutputs.get(i).getDataType().equals(anchorChildOutputTypes.get(i))) {
+                throw new AnalysisException(String.format("%s recursive child's %d column's datatype in select list %s "
+                                + "is different from anchor child's output datatype %s, please add cast manually "
+                                + "to get expect datatype", aliasQuery.getAlias(), i + 1,
+                        recursiveChildOutputs.get(i).getDataType(), anchorChildOutputTypes.get(i)));
+            }
+        }
+        analyzedRecursiveChild = new LogicalRecursiveCteRecursiveChild<>(aliasQuery.getAlias(),
+                forceOutputNullable(analyzedRecursiveChild, ImmutableList.of()));
+
+        // create LogicalRecursiveCte
+        LogicalUnion logicalUnion = (LogicalUnion) parsedCtePlan;
+        LogicalRecursiveCte analyzedCtePlan = new LogicalRecursiveCte(aliasQuery.getAlias(),
+                logicalUnion.getQualifier() == SetOperation.Qualifier.ALL,
+                ImmutableList.of(analyzedAnchorChild, analyzedRecursiveChild));
+        List<List<NamedExpression>> childrenProjections = analyzedCtePlan.collectChildrenProjections();
+        int childrenProjectionSize = childrenProjections.size();
+        ImmutableList.Builder<List<SlotReference>> childrenOutputs = ImmutableList
+                .builderWithExpectedSize(childrenProjectionSize);
+        ImmutableList.Builder<Plan> newChildren = ImmutableList.builderWithExpectedSize(childrenProjectionSize);
+        for (int i = 0; i < childrenProjectionSize; i++) {
+            Plan newChild;
+            Plan child = analyzedCtePlan.child(i);
+            if (childrenProjections.get(i).stream().allMatch(SlotReference.class::isInstance)) {
+                newChild = child;
+            } else {
+                List<NamedExpression> parentProject = childrenProjections.get(i);
+                newChild = ProjectProcessor.tryProcessProject(parentProject, child)
+                        .orElseGet(() -> new LogicalProject<>(parentProject, child));
+            }
+            newChildren.add(newChild);
+            childrenOutputs.add((List<SlotReference>) (List) newChild.getOutput());
+        }
+        analyzedCtePlan = analyzedCtePlan.withChildrenAndTheirOutputs(newChildren.build(), childrenOutputs.build());
+        List<NamedExpression> newOutputs = analyzedCtePlan.buildNewOutputs();
+        analyzedCtePlan = analyzedCtePlan.withNewOutputs(newOutputs);
+
+        CTEId cteId = StatementScopeIdGenerator.newCTEId();
+        LogicalSubQueryAlias<Plan> logicalSubQueryAlias = aliasQuery.withChildren(ImmutableList.of(analyzedCtePlan));
+        outerCteCtx = new CTEContext(cteId, logicalSubQueryAlias, outerCteCtx);
+        outerCteCtx.setAnalyzedPlan(logicalSubQueryAlias);
+        LogicalCTEProducer<Plan> cteProducer = new LogicalCTEProducer<>(cteId, logicalSubQueryAlias);
+        return Pair.of(outerCteCtx, cteProducer);
+    }
+
+    private LogicalPlan forceOutputNullable(LogicalPlan logicalPlan, List<String> aliasNames) {
+        List<Slot> oldOutputs = logicalPlan.getOutput();
+        int size = oldOutputs.size();
+        List<NamedExpression> newOutputs = new ArrayList<>(oldOutputs.size());
+        if (!aliasNames.isEmpty()) {
+            for (int i = 0; i < size; ++i) {
+                newOutputs.add(new Alias(new Nullable(oldOutputs.get(i)), aliasNames.get(i)));
+            }
+        } else {
+            for (Slot slot : oldOutputs) {
+                newOutputs.add(new Alias(new Nullable(slot), slot.getName()));
+            }
+        }
+        return new LogicalProject<>(newOutputs, logicalPlan);
+    }
+
     /**
      * check columnAliases' size and name
      */
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindRelation.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindRelation.java
index 53a9e24ad50..fc9883a9711 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindRelation.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindRelation.java
@@ -27,6 +27,7 @@ import org.apache.doris.catalog.FunctionRegistry;
 import org.apache.doris.catalog.KeysType;
 import org.apache.doris.catalog.OlapTable;
 import org.apache.doris.catalog.Partition;
+import org.apache.doris.catalog.RecursiveCteTempTable;
 import org.apache.doris.catalog.SchemaTable;
 import org.apache.doris.catalog.SchemaTable.SchemaColumn;
 import org.apache.doris.catalog.TableIf;
@@ -91,6 +92,7 @@ import org.apache.doris.nereids.trees.plans.logical.LogicalJdbcScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOdbcScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCteScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSchemaScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSubQueryAlias;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTVFRelation;
@@ -172,16 +174,32 @@ public class BindRelation extends OneAnalysisRuleFactory {
                 return consumer;
             }
         }
-        List<String> tableQualifier = RelationUtil.getQualifierName(
-                cascadesContext.getConnectContext(), unboundRelation.getNameParts());
-        TableIf table = cascadesContext.getStatementContext().getAndCacheTable(tableQualifier, TableFrom.QUERY,
-                Optional.of(unboundRelation));
+        LogicalPlan scan;
+        if (tableName.equalsIgnoreCase(cascadesContext.getCurrentRecursiveCteName().orElse(""))) {
+            if (cascadesContext.isAnalyzingRecursiveCteAnchorChild()) {
+                throw new AnalysisException(
+                        String.format("recursive reference to query %s must not appear within its non-recursive term",
+                                cascadesContext.getCurrentRecursiveCteName().get()));
+            }
+            ImmutableList.Builder<Column> schema = new ImmutableList.Builder<>();
+            for (Slot slot : cascadesContext.getRecursiveCteOutputs()) {
+                schema.add(new Column(slot.getName(), slot.getDataType().toCatalogDataType(), slot.nullable()));
+            }
+            RecursiveCteTempTable cteTempTable = new RecursiveCteTempTable(tableName, schema.build());
+            scan = new LogicalRecursiveCteScan(cascadesContext.getStatementContext().getNextRelationId(),
+                    cteTempTable, unboundRelation.getNameParts());
+        } else {
+            List<String> tableQualifier = RelationUtil.getQualifierName(
+                    cascadesContext.getConnectContext(), unboundRelation.getNameParts());
+            TableIf table = cascadesContext.getStatementContext().getAndCacheTable(tableQualifier, TableFrom.QUERY,
+                    Optional.of(unboundRelation));
 
-        LogicalPlan scan = getLogicalPlan(table, unboundRelation, tableQualifier, cascadesContext);
-        if (cascadesContext.isLeadingJoin()) {
-            LeadingHint leading = (LeadingHint) cascadesContext.getHintMap().get("Leading");
-            leading.putRelationIdAndTableName(Pair.of(unboundRelation.getRelationId(), tableName));
-            leading.getRelationIdToScanMap().put(unboundRelation.getRelationId(), scan);
+            scan = getLogicalPlan(table, unboundRelation, tableQualifier, cascadesContext);
+            if (cascadesContext.isLeadingJoin()) {
+                LeadingHint leading = (LeadingHint) cascadesContext.getHintMap().get("Leading");
+                leading.putRelationIdAndTableName(Pair.of(unboundRelation.getRelationId(), tableName));
+                leading.getRelationIdToScanMap().put(unboundRelation.getRelationId(), scan);
+            }
         }
         return scan;
     }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CollectRelation.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CollectRelation.java
index 0e15e4dbec0..52418a85f5f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CollectRelation.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CollectRelation.java
@@ -108,8 +108,10 @@ public class CollectRelation implements AnalysisRuleFactory {
         for (LogicalSubQueryAlias<Plan> aliasQuery : aliasQueries) {
             // we should use a chain to ensure visible of cte
             LogicalPlan parsedCtePlan = (LogicalPlan) aliasQuery.child();
+            // 看起来需要在CascadesContext中添加当前CTE的name,以便判断自引用
             CascadesContext innerCascadesCtx = CascadesContext.newContextWithCteContext(
-                    cascadesContext, parsedCtePlan, outerCteCtx);
+                    cascadesContext, parsedCtePlan, outerCteCtx, aliasQuery.isRecursiveCte()
+                            ? Optional.of(aliasQuery.getAlias()) : Optional.empty(), ImmutableList.of());
             innerCascadesCtx.newTableCollector(true).collect();
             LogicalPlan analyzedCtePlan = (LogicalPlan) innerCascadesCtx.getRewritePlan();
             // cteId is not used in CollectTable stage
@@ -128,7 +130,8 @@ public class CollectRelation implements AnalysisRuleFactory {
                 if (e instanceof SubqueryExpr) {
                     SubqueryExpr subqueryExpr = (SubqueryExpr) e;
                     CascadesContext subqueryContext = CascadesContext.newContextWithCteContext(
-                            ctx.cascadesContext, subqueryExpr.getQueryPlan(), ctx.cteContext);
+                            ctx.cascadesContext, subqueryExpr.getQueryPlan(), ctx.cteContext, Optional.empty(),
+                            ImmutableList.of());
                     subqueryContext.keepOrShowPlanProcess(ctx.cascadesContext.showPlanProcess(),
                             () -> subqueryContext.newTableCollector(true).collect());
                     ctx.cascadesContext.addPlanProcesses(subqueryContext.getPlanProcesses());
@@ -184,6 +187,10 @@ public class CollectRelation implements AnalysisRuleFactory {
             List<String> nameParts, TableFrom tableFrom, Optional<UnboundRelation> unboundRelation) {
         if (nameParts.size() == 1) {
             String tableName = nameParts.get(0);
+            if (cascadesContext.getCurrentRecursiveCteName().isPresent()
+                    && tableName.equalsIgnoreCase(cascadesContext.getCurrentRecursiveCteName().get())) {
+                return;
+            }
             // check if it is a CTE's name
             CTEContext cteContext = cascadesContext.getCteContext().findCTEContext(tableName).orElse(null);
             if (cteContext != null) {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SubExprAnalyzer.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SubExprAnalyzer.java
index 69d79934992..05a35dc6ed8 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SubExprAnalyzer.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SubExprAnalyzer.java
@@ -52,6 +52,7 @@ import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -227,7 +228,8 @@ class SubExprAnalyzer<T> extends DefaultExpressionRewriter<T> {
             throw new IllegalStateException("Missing CascadesContext");
         }
         CascadesContext subqueryContext = CascadesContext.newContextWithCteContext(
-                cascadesContext, expr.getQueryPlan(), cascadesContext.getCteContext());
+                cascadesContext, expr.getQueryPlan(), cascadesContext.getCteContext(), Optional.empty(),
+                ImmutableList.of());
         // don't use `getScope()` because we only need `getScope().getOuterScope()` and `getScope().getSlots()`
         // otherwise unexpected errors may occur
         Scope subqueryScope = new Scope(getScope().getOuterScope(),
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalRecursiveCteRecursiveChildToPhysicalRecursiveCteRecursiveChild.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalRecursiveCteRecursiveChildToPhysicalRecursiveCteRecursiveChild.java
new file mode 100644
index 00000000000..689e550fa3a
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalRecursiveCteRecursiveChildToPhysicalRecursiveCteRecursiveChild.java
@@ -0,0 +1,37 @@
+// 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.
+
+package org.apache.doris.nereids.rules.implementation;
+
+import org.apache.doris.nereids.rules.Rule;
+import org.apache.doris.nereids.rules.RuleType;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCteRecursiveChild;
+
+/**
+ * Implementation rule that convert logical recursive cte's recursive child to physical recursive child.
+ */
+public class LogicalRecursiveCteRecursiveChildToPhysicalRecursiveCteRecursiveChild
+        extends OneImplementationRuleFactory {
+    @Override
+    public Rule build() {
+        return logicalRecursiveCteRecursiveChild().then(recursiveCte -> new PhysicalRecursiveCteRecursiveChild(
+                recursiveCte.getCteName(),
+                recursiveCte.getLogicalProperties(),
+                recursiveCte.child()))
+                .toRule(RuleType.LOGICAL_RECURSIVE_CTE_RECURSIVE_CHILD_TO_PHYSICAL_RECURSIVE_CTE_RECURSIVE_CHILD);
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalRecursiveCteScanToPhysicalRecursiveCteScan.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalRecursiveCteScanToPhysicalRecursiveCteScan.java
new file mode 100644
index 00000000000..8714c280bb9
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalRecursiveCteScanToPhysicalRecursiveCteScan.java
@@ -0,0 +1,42 @@
+// 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.
+
+package org.apache.doris.nereids.rules.implementation;
+
+import org.apache.doris.nereids.rules.Rule;
+import org.apache.doris.nereids.rules.RuleType;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCteScan;
+
+import java.util.Optional;
+
+/**
+ * Implementation rule that convert logical Recursive Cte Scan to physical Recursive Cte Scan.
+ */
+public class LogicalRecursiveCteScanToPhysicalRecursiveCteScan extends OneImplementationRuleFactory {
+    @Override
+    public Rule build() {
+        return logicalRecursiveCteScan().then(recursiveCteScan ->
+            new PhysicalRecursiveCteScan(
+                    recursiveCteScan.getRelationId(),
+                    recursiveCteScan.getTable(),
+                    recursiveCteScan.getQualifier(),
+                    Optional.empty(),
+                    recursiveCteScan.getLogicalProperties(),
+                    recursiveCteScan.getOperativeSlots())
+        ).toRule(RuleType.LOGICAL_RECURSIVE_CTE_SCAN_TO_PHYSICAL_RECUSIVE_CTE_SCAN_RULE);
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalRecursiveCteToPhysicalRecursiveCte.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalRecursiveCteToPhysicalRecursiveCte.java
new file mode 100644
index 00000000000..12ac89df19c
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalRecursiveCteToPhysicalRecursiveCte.java
@@ -0,0 +1,39 @@
+// 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.
+
+package org.apache.doris.nereids.rules.implementation;
+
+import org.apache.doris.nereids.rules.Rule;
+import org.apache.doris.nereids.rules.RuleType;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCte;
+
+/**
+ * Implementation rule that convert logical Recursive Cte to Physical Recursive Cte.
+ */
+public class LogicalRecursiveCteToPhysicalRecursiveCte extends OneImplementationRuleFactory {
+    @Override
+    public Rule build() {
+        return logicalRecursiveCte().then(recursiveCte ->
+            new PhysicalRecursiveCte(recursiveCte.getCteName(),
+                    recursiveCte.isUnionAll(),
+                    recursiveCte.getOutputs(),
+                    recursiveCte.getRegularChildrenOutputs(),
+                    recursiveCte.getLogicalProperties(),
+                    recursiveCte.children())
+        ).toRule(RuleType.LOGICAL_RECURSIVE_CTE_TO_PHYSICAL_RECURSIVE_CTE);
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AdjustNullable.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AdjustNullable.java
index c8016a493c6..24d70ca3bef 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AdjustNullable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AdjustNullable.java
@@ -39,6 +39,7 @@ import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPartitionTopN;
 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.LogicalRecursiveCte;
 import org.apache.doris.nereids.trees.plans.logical.LogicalRepeat;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSetOperation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSink;
@@ -288,6 +289,53 @@ public class AdjustNullable extends DefaultPlanRewriter<Map<ExprId, Slot>> imple
         return repeat.withGroupSetsAndOutput(repeat.getGroupingSets(), newOutputs).recomputeLogicalProperties();
     }
 
+    @Override
+    public Plan visitLogicalRecursiveCte(LogicalRecursiveCte recursiveCte, Map<ExprId, Slot> replaceMap) {
+        recursiveCte = (LogicalRecursiveCte) super.visit(recursiveCte, replaceMap);
+        ImmutableList.Builder<List<SlotReference>> newChildrenOutputs = ImmutableList.builder();
+        List<Boolean> inputNullable = null;
+        if (!recursiveCte.children().isEmpty()) {
+            inputNullable = Lists.newArrayListWithCapacity(recursiveCte.getOutputs().size());
+            for (int i = 0; i < recursiveCte.getOutputs().size(); i++) {
+                inputNullable.add(false);
+            }
+            for (int i = 0; i < recursiveCte.arity(); i++) {
+                List<Slot> childOutput = recursiveCte.child(i).getOutput();
+                List<SlotReference> setChildOutput = recursiveCte.getRegularChildOutput(i);
+                ImmutableList.Builder<SlotReference> newChildOutputs = ImmutableList.builder();
+                for (int j = 0; j < setChildOutput.size(); j++) {
+                    for (Slot slot : childOutput) {
+                        if (slot.getExprId().equals(setChildOutput.get(j).getExprId())) {
+                            inputNullable.set(j, slot.nullable() || inputNullable.get(j));
+                            newChildOutputs.add((SlotReference) slot);
+                            break;
+                        }
+                    }
+                }
+                newChildrenOutputs.add(newChildOutputs.build());
+            }
+        }
+        if (inputNullable == null) {
+            // this is a fail-safe
+            // means there is no children and having no getConstantExprsList
+            // no way to update the nullable flag, so just do nothing
+            return recursiveCte;
+        }
+        List<NamedExpression> outputs = recursiveCte.getOutputs();
+        List<NamedExpression> newOutputs = Lists.newArrayListWithCapacity(outputs.size());
+        for (int i = 0; i < inputNullable.size(); i++) {
+            NamedExpression ne = outputs.get(i);
+            Slot slot = ne instanceof Alias ? (Slot) ((Alias) ne).child() : (Slot) ne;
+            slot = slot.withNullable(inputNullable.get(i));
+            NamedExpression newOutput = ne instanceof Alias ? (NamedExpression) ne.withChildren(slot) : slot;
+            newOutputs.add(newOutput);
+            replaceMap.put(newOutput.getExprId(), newOutput.toSlot());
+        }
+        return recursiveCte.withNewOutputs(newOutputs)
+                .withChildrenAndTheirOutputs(recursiveCte.children(), newChildrenOutputs.build())
+                .recomputeLogicalProperties();
+    }
+
     @Override
     public Plan visitLogicalSetOperation(LogicalSetOperation setOperation, Map<ExprId, Slot> replaceMap) {
         setOperation = (LogicalSetOperation) super.visit(setOperation, replaceMap);
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInline.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInline.java
index 22ec72c99c5..9bbcb2e1e8d 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInline.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CTEInline.java
@@ -31,15 +31,19 @@ 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.LogicalPlan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCteRecursiveChild;
 import org.apache.doris.nereids.trees.plans.visitor.CustomRewriter;
 import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanRewriter;
 import org.apache.doris.qe.ConnectContext;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * pull up LogicalCteAnchor to the top of plan to avoid CteAnchor break other rewrite rules pattern
@@ -48,9 +52,13 @@ import java.util.List;
  * and put all of them to the top of plan depends on dependency tree of them.
  */
 public class CTEInline extends DefaultPlanRewriter<LogicalCTEProducer<?>> implements CustomRewriter {
+    // all cte used by recursive cte's recursive child should be inline
+    private Set<LogicalCTEConsumer> mustInlineCteConsumers = new HashSet<>();
 
     @Override
     public Plan rewriteRoot(Plan plan, JobContext jobContext) {
+        collectMustInlineCteConsumers(plan, false, mustInlineCteConsumers);
+
         Plan root = plan.accept(this, null);
         // collect cte id to consumer
         root.foreach(p -> {
@@ -78,17 +86,24 @@ public class CTEInline extends DefaultPlanRewriter<LogicalCTEProducer<?>> implem
                 }
                 return false;
             });
-            ConnectContext connectContext = ConnectContext.get();
-            if (connectContext.getSessionVariable().enableCTEMaterialize
-                    && consumers.size() > connectContext.getSessionVariable().inlineCTEReferencedThreshold) {
-                // not inline
-                Plan right = cteAnchor.right().accept(this, null);
-                return cteAnchor.withChildren(cteAnchor.left(), right);
-            } else {
+            if (!Sets.intersection(mustInlineCteConsumers, Sets.newHashSet(consumers)).isEmpty()) {
                 // should inline
                 Plan root = cteAnchor.right().accept(this, (LogicalCTEProducer<?>) cteAnchor.left());
                 // process child
                 return root.accept(this, null);
+            } else {
+                ConnectContext connectContext = ConnectContext.get();
+                if (connectContext.getSessionVariable().enableCTEMaterialize
+                        && consumers.size() > connectContext.getSessionVariable().inlineCTEReferencedThreshold) {
+                    // not inline
+                    Plan right = cteAnchor.right().accept(this, null);
+                    return cteAnchor.withChildren(cteAnchor.left(), right);
+                } else {
+                    // should inline
+                    Plan root = cteAnchor.right().accept(this, (LogicalCTEProducer<?>) cteAnchor.left());
+                    // process child
+                    return root.accept(this, null);
+                }
             }
         }
     }
@@ -113,4 +128,19 @@ public class CTEInline extends DefaultPlanRewriter<LogicalCTEProducer<?>> implem
         }
         return cteConsumer;
     }
+
+    private void collectMustInlineCteConsumers(Plan planNode, boolean needCollect,
+            Set<LogicalCTEConsumer> cteConsumers) {
+        if (planNode instanceof LogicalCTEConsumer) {
+            if (needCollect) {
+                cteConsumers.add((LogicalCTEConsumer) planNode);
+            }
+        } else if (planNode instanceof LogicalRecursiveCteRecursiveChild) {
+            collectMustInlineCteConsumers(planNode.child(0), true, cteConsumers);
+        } else {
+            for (Plan child : planNode.children()) {
+                collectMustInlineCteConsumers(child, needCollect, cteConsumers);
+            }
+        }
+    }
 }
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..7b4a8d9480f 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
@@ -44,6 +44,7 @@ import org.apache.doris.nereids.trees.plans.logical.LogicalExcept;
 import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
 import org.apache.doris.nereids.trees.plans.logical.LogicalIntersect;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCte;
 import org.apache.doris.nereids.trees.plans.logical.LogicalRepeat;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSink;
 import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
@@ -211,6 +212,12 @@ public class ColumnPruning extends DefaultPlanRewriter<PruneContext> implements
         return pruneChildren(plan, new RoaringBitmap());
     }
 
+    @Override
+    public Plan visitLogicalRecursiveCte(LogicalRecursiveCte recursiveCte, PruneContext context) {
+        // keep LogicalRecursiveCte's output unchanged
+        return skipPruneThis(recursiveCte);
+    }
+
     // union can not prune children by the common logic, we must override visit method to write special code.
     @Override
     public Plan visitLogicalUnion(LogicalUnion union, PruneContext context) {
@@ -388,6 +395,54 @@ public class ColumnPruning extends DefaultPlanRewriter<PruneContext> implements
         }
     }
 
+    private LogicalRecursiveCte pruneRecursiveCteOutput(LogicalRecursiveCte recursiveCte, PruneContext context) {
+        List<NamedExpression> originOutput = recursiveCte.getOutputs();
+        if (originOutput.isEmpty()) {
+            return recursiveCte;
+        }
+        List<NamedExpression> prunedOutputs = Lists.newArrayList();
+        List<List<SlotReference>> regularChildrenOutputs = recursiveCte.getRegularChildrenOutputs();
+        List<Plan> children = recursiveCte.children();
+        List<Integer> extractColumnIndex = Lists.newArrayList();
+        for (int i = 0; i < originOutput.size(); i++) {
+            NamedExpression output = originOutput.get(i);
+            if (context.requiredSlotsIds.contains(output.getExprId().asInt())) {
+                prunedOutputs.add(output);
+                extractColumnIndex.add(i);
+            }
+        }
+
+        if (prunedOutputs.isEmpty()) {
+            // process prune all columns
+            NamedExpression originSlot = originOutput.get(0);
+            prunedOutputs = ImmutableList.of(new SlotReference(originSlot.getExprId(), originSlot.getName(),
+                    TinyIntType.INSTANCE, false, originSlot.getQualifier()));
+            regularChildrenOutputs = Lists.newArrayListWithCapacity(regularChildrenOutputs.size());
+            children = Lists.newArrayListWithCapacity(children.size());
+            for (int i = 0; i < recursiveCte.children().size(); i++) {
+                Plan child = recursiveCte.child(i);
+                List<NamedExpression> newProjectOutput = ImmutableList.of(new Alias(new TinyIntLiteral((byte) 1)));
+                LogicalProject<?> project;
+                if (child instanceof LogicalProject) {
+                    LogicalProject<Plan> childProject = (LogicalProject<Plan>) child;
+                    List<NamedExpression> mergeProjections = PlanUtils.mergeProjections(
+                            childProject.getProjects(), newProjectOutput);
+                    project = new LogicalProject<>(mergeProjections, childProject.child());
+                } else {
+                    project = new LogicalProject<>(newProjectOutput, child);
+                }
+                regularChildrenOutputs.add((List) project.getOutput());
+                children.add(project);
+            }
+        }
+
+        if (prunedOutputs.equals(originOutput) && !context.requiredSlotsIds.isEmpty()) {
+            return recursiveCte;
+        } else {
+            return recursiveCte.withNewOutputsAndChildren(prunedOutputs, children, regularChildrenOutputs);
+        }
+    }
+
     private LogicalUnion pruneUnionOutput(LogicalUnion union, PruneContext context) {
         List<NamedExpression> originOutput = union.getOutputs();
         if (originOutput.isEmpty()) {
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/stats/StatsCalculator.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/stats/StatsCalculator.java
index fc95e3a4078..7f112c4b4f8 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/stats/StatsCalculator.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/stats/StatsCalculator.java
@@ -60,6 +60,7 @@ import org.apache.doris.nereids.trees.plans.algebra.Limit;
 import org.apache.doris.nereids.trees.plans.algebra.OlapScan;
 import org.apache.doris.nereids.trees.plans.algebra.PartitionTopN;
 import org.apache.doris.nereids.trees.plans.algebra.Project;
+import org.apache.doris.nereids.trees.plans.algebra.RecursiveCte;
 import org.apache.doris.nereids.trees.plans.algebra.Relation;
 import org.apache.doris.nereids.trees.plans.algebra.Repeat;
 import org.apache.doris.nereids.trees.plans.algebra.SetOperation;
@@ -90,6 +91,9 @@ import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPartitionTopN;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCte;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCteRecursiveChild;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCteScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalRepeat;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSchemaScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSink;
@@ -123,6 +127,9 @@ import org.apache.doris.nereids.trees.plans.physical.PhysicalOneRowRelation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPartitionTopN;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalQuickSort;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCte;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCteRecursiveChild;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCteScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalRelation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalRepeat;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalSchemaScan;
@@ -876,6 +883,12 @@ public class StatsCalculator extends DefaultPlanVisitor<Statistics, Void> {
         return computeCatalogRelation(esScan);
     }
 
+    @Override
+    public Statistics visitLogicalRecursiveCteScan(LogicalRecursiveCteScan recursiveCteScan, Void context) {
+        recursiveCteScan.getExpressions();
+        return computeCatalogRelation(recursiveCteScan);
+    }
+
     @Override
     public Statistics visitLogicalProject(LogicalProject<? extends Plan> project, Void context) {
         return computeProject(project, groupExpression.childStatistics(0));
@@ -919,6 +932,20 @@ public class StatsCalculator extends DefaultPlanVisitor<Statistics, Void> {
         return computeAssertNumRows(assertNumRows.getAssertNumRowsElement(), groupExpression.childStatistics(0));
     }
 
+    @Override
+    public Statistics visitLogicalRecursiveCte(
+            LogicalRecursiveCte recursiveCte, Void context) {
+        return computeRecursiveCte(recursiveCte,
+                groupExpression.children()
+                        .stream().map(Group::getStatistics).collect(Collectors.toList()));
+    }
+
+    @Override
+    public Statistics visitLogicalRecursiveCteRecursiveChild(LogicalRecursiveCteRecursiveChild recursiveChild,
+            Void context) {
+        return groupExpression.childStatistics(0);
+    }
+
     @Override
     public Statistics visitLogicalUnion(
             LogicalUnion union, Void context) {
@@ -1004,6 +1031,11 @@ public class StatsCalculator extends DefaultPlanVisitor<Statistics, Void> {
         return computeCatalogRelation(schemaScan);
     }
 
+    @Override
+    public Statistics visitPhysicalRecursiveCteScan(PhysicalRecursiveCteScan recursiveCteScan, Void context) {
+        return computeCatalogRelation(recursiveCteScan);
+    }
+
     @Override
     public Statistics visitPhysicalFileScan(PhysicalFileScan fileScan, Void context) {
         return computeCatalogRelation(fileScan);
@@ -1090,6 +1122,18 @@ public class StatsCalculator extends DefaultPlanVisitor<Statistics, Void> {
         return computeAssertNumRows(assertNumRows.getAssertNumRowsElement(), groupExpression.childStatistics(0));
     }
 
+    @Override
+    public Statistics visitPhysicalRecursiveCte(PhysicalRecursiveCte recursiveCte, Void context) {
+        return computeRecursiveCte(recursiveCte, groupExpression.children()
+                .stream().map(Group::getStatistics).collect(Collectors.toList()));
+    }
+
+    @Override
+    public Statistics visitPhysicalRecursiveCteRecursiveChild(PhysicalRecursiveCteRecursiveChild recursiveChild,
+            Void context) {
+        return groupExpression.childStatistics(0);
+    }
+
     @Override
     public Statistics visitPhysicalUnion(PhysicalUnion union, Void context) {
         return computeUnion(union, groupExpression.children()
@@ -1471,6 +1515,71 @@ public class StatsCalculator extends DefaultPlanVisitor<Statistics, Void> {
         return new Statistics(rowCount, 1, columnStatsMap);
     }
 
+    /**
+     * computeRecursiveCte
+     */
+    public Statistics computeRecursiveCte(RecursiveCte recursiveCte, List<Statistics> childStats) {
+        // TODO: refactor this for one row relation
+        List<SlotReference> head;
+        Statistics headStats;
+        List<List<SlotReference>> childOutputs = Lists.newArrayList(recursiveCte.getRegularChildrenOutputs());
+
+        head = childOutputs.get(0);
+        headStats = new StatisticsBuilder(childStats.get(0)).build();
+
+        StatisticsBuilder statisticsBuilder = new StatisticsBuilder();
+        List<NamedExpression> unionOutput = recursiveCte.getOutputs();
+        double unionRowCount = childStats.stream().mapToDouble(Statistics::getRowCount).sum();
+        statisticsBuilder.setRowCount(unionRowCount);
+
+        for (int i = 0; i < head.size(); i++) {
+            Slot headSlot = head.get(i);
+            ColumnStatisticBuilder colStatsBuilder = new ColumnStatisticBuilder(
+                    headStats.findColumnStatistics(headSlot));
+            for (int j = 1; j < childOutputs.size(); j++) {
+                Slot slot = childOutputs.get(j).get(i);
+                ColumnStatistic rightStatistic = childStats.get(j).findColumnStatistics(slot);
+                double rightRowCount = childStats.get(j).getRowCount();
+                colStatsBuilder = unionColumn(colStatsBuilder,
+                        headStats.getRowCount(), rightStatistic, rightRowCount, headSlot.getDataType());
+            }
+
+            //update hot values
+            Map<Literal, Float> unionHotValues = new HashMap<>();
+            for (int j = 0; j < childOutputs.size(); j++) {
+                Slot slot = childOutputs.get(j).get(i);
+                ColumnStatistic slotStats = childStats.get(j).findColumnStatistics(slot);
+                if (slotStats.getHotValues() != null) {
+                    for (Map.Entry<Literal, Float> entry : slotStats.getHotValues().entrySet()) {
+                        Float value = unionHotValues.get(entry.getKey());
+                        if (value == null) {
+                            unionHotValues.put(entry.getKey(),
+                                    (float) (entry.getValue() * childStats.get(j).getRowCount()));
+                        } else {
+                            unionHotValues.put(entry.getKey(),
+                                    (float) (value + entry.getValue() * childStats.get(j).getRowCount()));
+                        }
+                    }
+                }
+            }
+
+            Map<Literal, Float> resultHotValues = new LinkedHashMap<>();
+            for (Literal hot : unionHotValues.keySet()) {
+                float ratio = (float) (unionHotValues.get(hot) / unionRowCount);
+                if (ratio * colStatsBuilder.getNdv() >= SessionVariable.getSkewValueThreshold()
+                        || ratio >= SessionVariable.getHotValueThreshold()) {
+                    resultHotValues.put(hot, ratio);
+                }
+            }
+            if (!resultHotValues.isEmpty()) {
+                colStatsBuilder.setHotValues(resultHotValues);
+            }
+            statisticsBuilder.putColumnStatistics(unionOutput.get(i), colStatsBuilder.build());
+        }
+
+        return statisticsBuilder.setWidthInJoinCluster(1).build();
+    }
+
     /**
      * computeUnion
      */
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/copier/LogicalPlanDeepCopier.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/copier/LogicalPlanDeepCopier.java
index b4e13c7d4b9..a29ba438a49 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/copier/LogicalPlanDeepCopier.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/copier/LogicalPlanDeepCopier.java
@@ -53,6 +53,8 @@ import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPartitionTopN;
 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.LogicalRecursiveCte;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCteRecursiveChild;
 import org.apache.doris.nereids.trees.plans.logical.LogicalRelation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalRepeat;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSink;
@@ -371,6 +373,30 @@ public class LogicalPlanDeepCopier extends DefaultPlanRewriter<DeepCopierContext
                 constantExprsList, union.hasPushedFilter(), children);
     }
 
+    @Override
+    public Plan visitLogicalRecursiveCte(LogicalRecursiveCte recursiveCte, DeepCopierContext context) {
+        List<Plan> children = recursiveCte.children().stream()
+                .map(c -> c.accept(this, context))
+                .collect(ImmutableList.toImmutableList());
+        List<NamedExpression> outputs = recursiveCte.getOutputs().stream()
+                .map(o -> (NamedExpression) ExpressionDeepCopier.INSTANCE.deepCopy(o, context))
+                .collect(ImmutableList.toImmutableList());
+        List<List<SlotReference>> childrenOutputs = recursiveCte.getRegularChildrenOutputs().stream()
+                .map(childOutputs -> childOutputs.stream()
+                        .map(o -> (SlotReference) ExpressionDeepCopier.INSTANCE.deepCopy(o, context))
+                        .collect(ImmutableList.toImmutableList()))
+                .collect(ImmutableList.toImmutableList());
+        return new LogicalRecursiveCte(recursiveCte.getCteName(), recursiveCte.isUnionAll(), outputs,
+                childrenOutputs, children);
+    }
+
+    @Override
+    public Plan visitLogicalRecursiveCteRecursiveChild(LogicalRecursiveCteRecursiveChild<? extends Plan> recursiveChild,
+            DeepCopierContext context) {
+        Plan child = recursiveChild.child().accept(this, context);
+        return new LogicalRecursiveCteRecursiveChild<>(recursiveChild.getCteName(), child);
+    }
+
     @Override
     public Plan visitLogicalExcept(LogicalExcept except, DeepCopierContext context) {
         List<Plan> children = except.children().stream()
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
index 949283816a2..23b35776b12 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
@@ -89,6 +89,9 @@ public enum PlanType {
     LOGICAL_PARTITION_TOP_N,
     LOGICAL_PROJECT,
     LOGICAL_QUALIFY,
+    LOGICAL_RECURSIVE_CTE,
+    LOGICAL_RECURSIVE_CTE_RECURSIVE_CHILD,
+    LOGICAL_RECURSIVE_CTE_SCAN,
     LOGICAL_REPEAT,
     LOGICAL_SELECT_HINT,
     LOGICAL_SUBQUERY_ALIAS,
@@ -113,6 +116,7 @@ public enum PlanType {
     PHYSICAL_OLAP_SCAN,
     PHYSICAL_SCHEMA_SCAN,
     PHYSICAL_TVF_RELATION,
+    PHYSICAL_RECURSIVE_CTE_SCAN,
 
     // physical sinks
     PHYSICAL_FILE_SINK,
@@ -133,6 +137,8 @@ public enum PlanType {
     PHYSICAL_ASSERT_NUM_ROWS,
     PHYSICAL_CTE_PRODUCER,
     PHYSICAL_CTE_ANCHOR,
+    PHYSICAL_RECURSIVE_CTE,
+    PHYSICAL_RECURSIVE_CTE_RECURSIVE_CHILD,
     PHYSICAL_DISTRIBUTE,
     PHYSICAL_EXCEPT,
     PHYSICAL_FILTER,
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/algebra/RecursiveCte.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/algebra/RecursiveCte.java
new file mode 100644
index 00000000000..d7fd8f985a1
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/algebra/RecursiveCte.java
@@ -0,0 +1,36 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.algebra;
+
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+
+import java.util.List;
+
+/**
+ * Common interface for logical/physical recursive cte.
+ */
+public interface RecursiveCte {
+    boolean isUnionAll();
+
+    List<SlotReference> getRegularChildOutput(int i);
+
+    List<NamedExpression> getOutputs();
+
+    List<List<SlotReference>> getRegularChildrenOutputs();
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateMvByPartitionCommand.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateMvByPartitionCommand.java
index 3ec8aa36f3b..31f11a903f1 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateMvByPartitionCommand.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/UpdateMvByPartitionCommand.java
@@ -269,7 +269,8 @@ public class UpdateMvByPartitionCommand extends InsertOverwriteTableCommand {
                 );
                 rewrittenSubQueryAlias.add(subQueryAlias.withChildren(subQueryAliasChildren));
             }
-            return super.visitLogicalCTE(new LogicalCTE<>(rewrittenSubQueryAlias, cte.child()), predicates);
+            return super.visitLogicalCTE(new LogicalCTE<>(cte.isRecursiveCte(),
+                    rewrittenSubQueryAlias, cte.child()), predicates);
         }
 
         @Override
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/job/UnassignedJobBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/job/UnassignedJobBuilder.java
index bc20d3efa17..1229bc59455 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/job/UnassignedJobBuilder.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/job/UnassignedJobBuilder.java
@@ -29,6 +29,7 @@ import org.apache.doris.planner.OlapScanNode;
 import org.apache.doris.planner.PlanFragment;
 import org.apache.doris.planner.PlanFragmentId;
 import org.apache.doris.planner.PlanNodeId;
+import org.apache.doris.planner.RecursiveCteScanNode;
 import org.apache.doris.planner.ScanNode;
 import org.apache.doris.planner.SchemaScanNode;
 import org.apache.doris.thrift.TExplainLevel;
@@ -126,6 +127,10 @@ public class UnassignedJobBuilder {
                 unassignedJob = buildScanMetadataJob(
                         statementContext, planFragment, (SchemaScanNode) scanNode, scanWorkerSelector
                 );
+            } else if (scanNode instanceof RecursiveCteScanNode) {
+                unassignedJob = buildScanRecursiveCteJob(
+                        statementContext, planFragment, (RecursiveCteScanNode) scanNode, inputJobs, scanWorkerSelector
+                );
             } else {
                 // only scan external tables or cloud tables or table valued functions
                 // e,g. select * from numbers('number'='100')
@@ -196,6 +201,14 @@ public class UnassignedJobBuilder {
         return new UnassignedScanMetadataJob(statementContext, fragment, schemaScanNode, scanWorkerSelector);
     }
 
+    private UnassignedJob buildScanRecursiveCteJob(
+            StatementContext statementContext, PlanFragment fragment,
+            RecursiveCteScanNode recursiveCteScanNode,
+            ListMultimap<ExchangeNode, UnassignedJob> inputJobs, ScanWorkerSelector scanWorkerSelector) {
+        return new UnassignedRecursiveCteScanJob(statementContext, fragment, recursiveCteScanNode,
+                inputJobs, scanWorkerSelector);
+    }
+
     private UnassignedJob buildScanRemoteTableJob(
             StatementContext statementContext, PlanFragment planFragment, List<ScanNode> scanNodes,
             ListMultimap<ExchangeNode, UnassignedJob> inputJobs,
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/job/UnassignedRecursiveCteScanJob.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/job/UnassignedRecursiveCteScanJob.java
new file mode 100644
index 00000000000..cfd3ebe7b2b
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/worker/job/UnassignedRecursiveCteScanJob.java
@@ -0,0 +1,65 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.distribute.worker.job;
+
+import org.apache.doris.nereids.StatementContext;
+import org.apache.doris.nereids.trees.plans.distribute.DistributeContext;
+import org.apache.doris.nereids.trees.plans.distribute.worker.DistributedPlanWorker;
+import org.apache.doris.nereids.trees.plans.distribute.worker.DistributedPlanWorkerManager;
+import org.apache.doris.nereids.trees.plans.distribute.worker.ScanWorkerSelector;
+import org.apache.doris.planner.ExchangeNode;
+import org.apache.doris.planner.PlanFragment;
+import org.apache.doris.planner.ScanNode;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * UnassignedRecursiveCteScanJob
+ */
+public class UnassignedRecursiveCteScanJob extends AbstractUnassignedScanJob {
+    private final ScanWorkerSelector scanWorkerSelector;
+
+    public UnassignedRecursiveCteScanJob(
+            StatementContext statementContext, PlanFragment fragment, ScanNode scanNode,
+            ListMultimap<ExchangeNode, UnassignedJob> exchangeToChildJob, ScanWorkerSelector scanWorkerSelector) {
+        super(statementContext, fragment, ImmutableList.of(scanNode), exchangeToChildJob);
+        this.scanWorkerSelector = Objects.requireNonNull(scanWorkerSelector, "scanWorkerSelector is not null");
+    }
+
+    @Override
+    protected Map<DistributedPlanWorker, UninstancedScanSource> multipleMachinesParallelization(
+            DistributeContext distributeContext, ListMultimap<ExchangeNode, AssignedJob> inputJobs) {
+        return scanWorkerSelector.selectReplicaAndWorkerWithoutBucket(
+                scanNodes.get(0), statementContext.getConnectContext()
+        );
+    }
+
+    @Override
+    protected List<AssignedJob> fillUpAssignedJobs(List<AssignedJob> assignedJobs,
+            DistributedPlanWorkerManager workerManager, ListMultimap<ExchangeNode, AssignedJob> inputJobs) {
+        Preconditions.checkArgument(!assignedJobs.isEmpty(),
+                "assignedJobs is empty for UnassignedRecursiveCteScanJob");
+        return assignedJobs;
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTE.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTE.java
index 4f810c3b6de..5ceb0e55bca 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTE.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTE.java
@@ -41,21 +41,28 @@ import java.util.stream.Collectors;
 public class LogicalCTE<CHILD_TYPE extends Plan> extends LogicalUnary<CHILD_TYPE> implements PropagateFuncDeps {
 
     private final List<LogicalSubQueryAlias<Plan>> aliasQueries;
+    private final boolean isRecursiveCte;
 
-    public LogicalCTE(List<LogicalSubQueryAlias<Plan>> aliasQueries, CHILD_TYPE child) {
-        this(aliasQueries, Optional.empty(), Optional.empty(), child);
+    public LogicalCTE(boolean isRecursiveCte, List<LogicalSubQueryAlias<Plan>> aliasQueries, CHILD_TYPE child) {
+        this(isRecursiveCte, aliasQueries, Optional.empty(), Optional.empty(), child);
     }
 
-    public LogicalCTE(List<LogicalSubQueryAlias<Plan>> aliasQueries, Optional<GroupExpression> groupExpression,
-            Optional<LogicalProperties> logicalProperties, CHILD_TYPE child) {
+    public LogicalCTE(boolean isRecursiveCte, List<LogicalSubQueryAlias<Plan>> aliasQueries,
+                      Optional<GroupExpression> groupExpression, Optional<LogicalProperties> logicalProperties,
+                      CHILD_TYPE child) {
         super(PlanType.LOGICAL_CTE, groupExpression, logicalProperties, child);
         this.aliasQueries = ImmutableList.copyOf(Objects.requireNonNull(aliasQueries, "aliasQueries can not be null"));
+        this.isRecursiveCte = isRecursiveCte;
     }
 
     public List<LogicalSubQueryAlias<Plan>> getAliasQueries() {
         return aliasQueries;
     }
 
+    public boolean isRecursiveCte() {
+        return isRecursiveCte;
+    }
+
     @Override
     public List<? extends Plan> extraPlans() {
         return aliasQueries;
@@ -74,6 +81,7 @@ public class LogicalCTE<CHILD_TYPE extends Plan> extends LogicalUnary<CHILD_TYPE
     @Override
     public String toString() {
         return Utils.toSqlString("LogicalCTE",
+                "isRecursiveCte", isRecursiveCte,
                 "aliasQueries", aliasQueries
         );
     }
@@ -105,18 +113,18 @@ public class LogicalCTE<CHILD_TYPE extends Plan> extends LogicalUnary<CHILD_TYPE
             return false;
         }
         LogicalCTE that = (LogicalCTE) o;
-        return aliasQueries.equals(that.aliasQueries);
+        return aliasQueries.equals(that.aliasQueries) && isRecursiveCte == that.isRecursiveCte;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(aliasQueries);
+        return Objects.hash(aliasQueries, isRecursiveCte);
     }
 
     @Override
     public Plan withChildren(List<Plan> children) {
         Preconditions.checkArgument(aliasQueries.size() > 0);
-        return new LogicalCTE<>(aliasQueries, children.get(0));
+        return new LogicalCTE<>(isRecursiveCte, aliasQueries, children.get(0));
     }
 
     @Override
@@ -131,13 +139,14 @@ public class LogicalCTE<CHILD_TYPE extends Plan> extends LogicalUnary<CHILD_TYPE
 
     @Override
     public LogicalCTE<CHILD_TYPE> withGroupExpression(Optional<GroupExpression> groupExpression) {
-        return new LogicalCTE<>(aliasQueries, groupExpression, Optional.of(getLogicalProperties()), child());
+        return new LogicalCTE<>(isRecursiveCte, aliasQueries, groupExpression,
+                Optional.of(getLogicalProperties()), child());
     }
 
     @Override
     public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression> groupExpression,
             Optional<LogicalProperties> logicalProperties, List<Plan> children) {
         Preconditions.checkArgument(aliasQueries.size() > 0);
-        return new LogicalCTE<>(aliasQueries, groupExpression, logicalProperties, children.get(0));
+        return new LogicalCTE<>(isRecursiveCte, aliasQueries, groupExpression, logicalProperties, children.get(0));
     }
 }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalRecursiveCte.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalRecursiveCte.java
new file mode 100644
index 00000000000..bef00db5f72
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalRecursiveCte.java
@@ -0,0 +1,284 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.logical;
+
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.memo.GroupExpression;
+import org.apache.doris.nereids.properties.DataTrait;
+import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.algebra.RecursiveCte;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.util.TypeCoercionUtils;
+import org.apache.doris.nereids.util.Utils;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * LogicalRecursiveCte is basically like LogicalUnion
+ */
+public class LogicalRecursiveCte extends AbstractLogicalPlan implements RecursiveCte, OutputPrunable {
+    private final String cteName;
+    private final List<NamedExpression> outputs;
+    private final List<List<SlotReference>> regularChildrenOutputs;
+    private final boolean isUnionAll;
+
+    /** LogicalRecursiveCte */
+    public LogicalRecursiveCte(String cteName, boolean isUnionAll, List<Plan> children) {
+        this(cteName, isUnionAll, ImmutableList.of(), ImmutableList.of(), children);
+    }
+
+    /** LogicalRecursiveCte */
+    public LogicalRecursiveCte(String cteName, boolean isUnionAll, List<NamedExpression> outputs,
+            List<List<SlotReference>> childrenOutputs, List<Plan> children) {
+        this(cteName, isUnionAll, outputs, childrenOutputs, Optional.empty(),
+                Optional.empty(),
+                children);
+    }
+
+    /** LogicalRecursiveCte */
+    public LogicalRecursiveCte(String cteName, boolean isUnionAll, List<NamedExpression> outputs,
+            List<List<SlotReference>> childrenOutputs,
+            Optional<GroupExpression> groupExpression, Optional<LogicalProperties> logicalProperties,
+            List<Plan> children) {
+        super(PlanType.LOGICAL_RECURSIVE_CTE, groupExpression, logicalProperties, children);
+        this.cteName = cteName;
+        this.isUnionAll = isUnionAll;
+        this.outputs = ImmutableList.copyOf(outputs);
+        this.regularChildrenOutputs = ImmutableList.copyOf(childrenOutputs);
+    }
+
+    @Override
+    public boolean isUnionAll() {
+        return isUnionAll;
+    }
+
+    public String getCteName() {
+        return cteName;
+    }
+
+    @Override
+    public List<SlotReference> getRegularChildOutput(int i) {
+        return regularChildrenOutputs.get(i);
+    }
+
+    @Override
+    public List<List<SlotReference>> getRegularChildrenOutputs() {
+        return regularChildrenOutputs;
+    }
+
+    public List<List<NamedExpression>> collectChildrenProjections() {
+        return castCommonDataTypeOutputs();
+    }
+
+    private List<List<NamedExpression>> castCommonDataTypeOutputs() {
+        int childOutputSize = child(0).getOutput().size();
+        ImmutableList.Builder<NamedExpression> newLeftOutputs = ImmutableList.builderWithExpectedSize(
+                childOutputSize);
+        ImmutableList.Builder<NamedExpression> newRightOutputs = ImmutableList.builderWithExpectedSize(
+                childOutputSize
+        );
+        // Ensure that the output types of the left and right children are consistent and expand upward.
+        for (int i = 0; i < childOutputSize; ++i) {
+            Slot left = child(0).getOutput().get(i);
+            Slot right = child(1).getOutput().get(i);
+            DataType compatibleType;
+            try {
+                compatibleType = LogicalSetOperation.getAssignmentCompatibleType(left.getDataType(),
+                        right.getDataType());
+            } catch (Exception e) {
+                throw new AnalysisException(
+                        "Can not find compatible type for " + left + " and " + right + ", " + e.getMessage());
+            }
+            Expression newLeft = TypeCoercionUtils.castIfNotSameTypeStrict(left, compatibleType);
+            Expression newRight = TypeCoercionUtils.castIfNotSameTypeStrict(right, compatibleType);
+            if (newLeft instanceof Cast) {
+                newLeft = new Alias(newLeft, left.getName());
+            }
+            if (newRight instanceof Cast) {
+                newRight = new Alias(newRight, right.getName());
+            }
+            newLeftOutputs.add((NamedExpression) newLeft);
+            newRightOutputs.add((NamedExpression) newRight);
+        }
+
+        return ImmutableList.of(newLeftOutputs.build(), newRightOutputs.build());
+    }
+
+    /**
+     * Generate new output for Recursive Cte.
+     */
+    public List<NamedExpression> buildNewOutputs() {
+        List<Slot> slots = resetNullableForLeftOutputs();
+        ImmutableList.Builder<NamedExpression> newOutputs = ImmutableList.builderWithExpectedSize(slots.size());
+
+        for (int i = 0; i < slots.size(); i++) {
+            Slot slot = slots.get(i);
+            ExprId exprId = i < outputs.size() ? outputs.get(i).getExprId() : StatementScopeIdGenerator.newExprId();
+            newOutputs.add(
+                    new SlotReference(exprId, slot.toSql(), slot.getDataType(), slot.nullable(), ImmutableList.of())
+            );
+        }
+        return newOutputs.build();
+    }
+
+    // If the right child is nullable, need to ensure that the left child is also nullable
+    private List<Slot> resetNullableForLeftOutputs() {
+        int rightChildOutputSize = child(1).getOutput().size();
+        ImmutableList.Builder<Slot> resetNullableForLeftOutputs
+                = ImmutableList.builderWithExpectedSize(rightChildOutputSize);
+        for (int i = 0; i < rightChildOutputSize; ++i) {
+            if (child(1).getOutput().get(i).nullable() && !child(0).getOutput().get(i).nullable()) {
+                resetNullableForLeftOutputs.add(child(0).getOutput().get(i).withNullable(true));
+            } else {
+                resetNullableForLeftOutputs.add(child(0).getOutput().get(i));
+            }
+        }
+        return resetNullableForLeftOutputs.build();
+    }
+
+    @Override
+    public String toString() {
+        return Utils.toSqlStringSkipNull("LogicalRecursiveCte",
+                "cteName", cteName,
+                "isUnionAll", isUnionAll,
+                "outputs", outputs,
+                "regularChildrenOutputs", regularChildrenOutputs,
+                "stats", statistics);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        LogicalRecursiveCte that = (LogicalRecursiveCte) o;
+        return cteName.equals(that.cteName) && isUnionAll == that.isUnionAll && Objects.equals(outputs, that.outputs)
+                && Objects.equals(regularChildrenOutputs, that.regularChildrenOutputs);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(cteName, isUnionAll, outputs, regularChildrenOutputs);
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitLogicalRecursiveCte(this, context);
+    }
+
+    @Override
+    public List<? extends Expression> getExpressions() {
+        return regularChildrenOutputs.stream().flatMap(List::stream).collect(ImmutableList.toImmutableList());
+    }
+
+    @Override
+    public List<Slot> computeOutput() {
+        return outputs.stream()
+                .map(NamedExpression::toSlot)
+                .collect(ImmutableList.toImmutableList());
+    }
+
+    @Override
+    public LogicalRecursiveCte withChildren(List<Plan> children) {
+        return new LogicalRecursiveCte(cteName, isUnionAll, outputs, regularChildrenOutputs, children);
+    }
+
+    public LogicalRecursiveCte withChildrenAndTheirOutputs(List<Plan> children,
+            List<List<SlotReference>> childrenOutputs) {
+        Preconditions.checkArgument(children.size() == childrenOutputs.size(),
+                "children size %s is not equals with children outputs size %s",
+                children.size(), childrenOutputs.size());
+        return new LogicalRecursiveCte(cteName, isUnionAll, outputs, childrenOutputs, children);
+    }
+
+    @Override
+    public LogicalRecursiveCte withGroupExpression(Optional<GroupExpression> groupExpression) {
+        return new LogicalRecursiveCte(cteName, isUnionAll, outputs, regularChildrenOutputs,
+                groupExpression, Optional.of(getLogicalProperties()), children);
+    }
+
+    @Override
+    public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression> groupExpression,
+            Optional<LogicalProperties> logicalProperties, List<Plan> children) {
+        return new LogicalRecursiveCte(cteName, isUnionAll, outputs, regularChildrenOutputs,
+                groupExpression, logicalProperties, children);
+    }
+
+    public LogicalRecursiveCte withNewOutputs(List<NamedExpression> newOutputs) {
+        return new LogicalRecursiveCte(cteName, isUnionAll, newOutputs, regularChildrenOutputs,
+                Optional.empty(), Optional.empty(), children);
+    }
+
+    public LogicalRecursiveCte withNewOutputsAndChildren(List<NamedExpression> newOutputs,
+                                                         List<Plan> children,
+                                                         List<List<SlotReference>> childrenOutputs) {
+        return new LogicalRecursiveCte(cteName, isUnionAll, newOutputs, childrenOutputs,
+                Optional.empty(), Optional.empty(), children);
+    }
+
+    @Override
+    public List<NamedExpression> getOutputs() {
+        return outputs;
+    }
+
+    @Override
+    public LogicalRecursiveCte pruneOutputs(List<NamedExpression> prunedOutputs) {
+        return withNewOutputs(prunedOutputs);
+    }
+
+    @Override
+    public void computeUnique(DataTrait.Builder builder) {
+    }
+
+    @Override
+    public void computeUniform(DataTrait.Builder builder) {
+    }
+
+    @Override
+    public boolean hasUnboundExpression() {
+        return outputs.isEmpty();
+    }
+
+    @Override
+    public void computeEqualSet(DataTrait.Builder builder) {
+    }
+
+    @Override
+    public void computeFd(DataTrait.Builder builder) {
+        // don't generate
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalRecursiveCteRecursiveChild.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalRecursiveCteRecursiveChild.java
new file mode 100644
index 00000000000..d2766a86f95
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalRecursiveCteRecursiveChild.java
@@ -0,0 +1,117 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.logical;
+
+import org.apache.doris.nereids.memo.GroupExpression;
+import org.apache.doris.nereids.properties.DataTrait;
+import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.nereids.util.Utils;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * LogicalRecursiveCteRecursiveChild is sentinel plan for must_shuffle
+ */
+public class LogicalRecursiveCteRecursiveChild<CHILD_TYPE extends Plan> extends LogicalUnary<CHILD_TYPE> {
+    private final String cteName;
+
+    public LogicalRecursiveCteRecursiveChild(String cteName, CHILD_TYPE child) {
+        this(cteName, Optional.empty(), Optional.empty(), child);
+    }
+
+    public LogicalRecursiveCteRecursiveChild(String cteName, Optional<GroupExpression> groupExpression,
+            Optional<LogicalProperties> logicalProperties, CHILD_TYPE child) {
+        this(cteName, groupExpression, logicalProperties, ImmutableList.of(child));
+    }
+
+    public LogicalRecursiveCteRecursiveChild(String cteName, Optional<GroupExpression> groupExpression,
+            Optional<LogicalProperties> logicalProperties, List<Plan> child) {
+        super(PlanType.LOGICAL_RECURSIVE_CTE_RECURSIVE_CHILD, groupExpression, logicalProperties, child);
+        this.cteName = cteName;
+    }
+
+    public String getCteName() {
+        return cteName;
+    }
+
+    @Override
+    public Plan withChildren(List<Plan> children) {
+        return new LogicalRecursiveCteRecursiveChild<>(cteName, Optional.empty(), Optional.empty(), children);
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitLogicalRecursiveCteRecursiveChild(this, context);
+    }
+
+    @Override
+    public List<? extends Expression> getExpressions() {
+        return ImmutableList.of();
+    }
+
+    @Override
+    public Plan withGroupExpression(Optional<GroupExpression> groupExpression) {
+        return new LogicalRecursiveCteRecursiveChild<>(cteName, groupExpression,
+                Optional.of(getLogicalProperties()), children);
+    }
+
+    @Override
+    public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression> groupExpression,
+            Optional<LogicalProperties> logicalProperties, List<Plan> children) {
+        return new LogicalRecursiveCteRecursiveChild<>(cteName, groupExpression, logicalProperties, children);
+    }
+
+    @Override
+    public String toString() {
+        return Utils.toSqlStringSkipNull("LogicalRecursiveCteRecursiveChild",
+                "cteName", cteName);
+    }
+
+    @Override
+    public void computeUnique(DataTrait.Builder builder) {
+
+    }
+
+    @Override
+    public void computeUniform(DataTrait.Builder builder) {
+
+    }
+
+    @Override
+    public void computeEqualSet(DataTrait.Builder builder) {
+
+    }
+
+    @Override
+    public void computeFd(DataTrait.Builder builder) {
+
+    }
+
+    @Override
+    public List<Slot> computeOutput() {
+        return child().getOutput();
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalRecursiveCteScan.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalRecursiveCteScan.java
new file mode 100644
index 00000000000..5d1ed487847
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalRecursiveCteScan.java
@@ -0,0 +1,73 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.logical;
+
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.nereids.memo.GroupExpression;
+import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.RelationId;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.nereids.util.Utils;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * LogicalRecursiveCteScan.
+ */
+public class LogicalRecursiveCteScan extends LogicalCatalogRelation {
+    public LogicalRecursiveCteScan(RelationId relationId, TableIf table, List<String> qualifier) {
+        this(relationId, table, qualifier, Optional.empty(), Optional.empty());
+    }
+
+    private LogicalRecursiveCteScan(RelationId relationId, TableIf table, List<String> qualifier,
+            Optional<GroupExpression> groupExpression, Optional<LogicalProperties> logicalProperties) {
+        super(relationId, PlanType.LOGICAL_RECURSIVE_CTE_SCAN, table, qualifier, groupExpression, logicalProperties);
+    }
+
+    @Override
+    public String toString() {
+        return Utils.toSqlString("LogicalRecursiveCteScan",
+                "cteName", table.getName());
+    }
+
+    @Override
+    public Plan withGroupExpression(Optional<GroupExpression> groupExpression) {
+        return new LogicalRecursiveCteScan(relationId, table, qualifier,
+                groupExpression, Optional.ofNullable(getLogicalProperties()));
+    }
+
+    @Override
+    public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression> groupExpression,
+            Optional<LogicalProperties> logicalProperties, List<Plan> children) {
+        return new LogicalRecursiveCteScan(relationId, table, qualifier, groupExpression, logicalProperties);
+    }
+
+    @Override
+    public LogicalCatalogRelation withRelationId(RelationId relationId) {
+        return new LogicalRecursiveCteScan(relationId, table, qualifier,
+                groupExpression, Optional.ofNullable(getLogicalProperties()));
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitLogicalRecursiveCteScan(this, context);
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalSubQueryAlias.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalSubQueryAlias.java
index 2f49dc1ce85..8b18d3ce49d 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalSubQueryAlias.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalSubQueryAlias.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.nereids.trees.plans.logical;
 
+import org.apache.doris.nereids.analyzer.UnboundRelation;
 import org.apache.doris.nereids.memo.GroupExpression;
 import org.apache.doris.nereids.properties.DataTrait;
 import org.apache.doris.nereids.properties.LogicalProperties;
@@ -27,6 +28,7 @@ import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.PlanType;
 import org.apache.doris.nereids.trees.plans.RelationId;
 import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.nereids.util.LazyCompute;
 import org.apache.doris.nereids.util.Utils;
 
 import com.google.common.base.Preconditions;
@@ -42,6 +44,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 /**
@@ -55,6 +58,7 @@ public class LogicalSubQueryAlias<CHILD_TYPE extends Plan> extends LogicalUnary<
     protected RelationId relationId;
     private final List<String> qualifier;
     private final Optional<List<String>> columnAliases;
+    private final Supplier<Boolean> isRecursiveCte;
 
     public LogicalSubQueryAlias(String tableAlias, CHILD_TYPE child) {
         this(ImmutableList.of(tableAlias), Optional.empty(), Optional.empty(), Optional.empty(), child);
@@ -78,6 +82,7 @@ public class LogicalSubQueryAlias<CHILD_TYPE extends Plan> extends LogicalUnary<
         super(PlanType.LOGICAL_SUBQUERY_ALIAS, groupExpression, logicalProperties, child);
         this.qualifier = ImmutableList.copyOf(Objects.requireNonNull(qualifier, "qualifier is null"));
         this.columnAliases = columnAliases;
+        this.isRecursiveCte = computeIsRecursiveCte();
     }
 
     @Override
@@ -121,6 +126,23 @@ public class LogicalSubQueryAlias<CHILD_TYPE extends Plan> extends LogicalUnary<
         return currentOutput.build();
     }
 
+    private Supplier<Boolean> computeIsRecursiveCte() {
+        return LazyCompute.of(() -> {
+            List<UnboundRelation> relationList = collectToList(UnboundRelation.class::isInstance);
+            for (UnboundRelation relation : relationList) {
+                List<String> nameParts = relation.getNameParts();
+                if (nameParts.size() == 1 && nameParts.get(0).equalsIgnoreCase(getAlias())) {
+                    return true;
+                }
+            }
+            return false;
+        });
+    }
+
+    public boolean isRecursiveCte() {
+        return isRecursiveCte.get();
+    }
+
     public String getAlias() {
         return qualifier.get(qualifier.size() - 1);
     }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalRecursiveCte.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalRecursiveCte.java
new file mode 100644
index 00000000000..44aab38fc30
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalRecursiveCte.java
@@ -0,0 +1,292 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.physical;
+
+import org.apache.doris.nereids.memo.GroupExpression;
+import org.apache.doris.nereids.properties.DataTrait;
+import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.algebra.RecursiveCte;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.nereids.util.Utils;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.statistics.Statistics;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * PhysicalRecursiveCte is basically like PhysicalUnion
+ */
+public class PhysicalRecursiveCte extends AbstractPhysicalPlan implements RecursiveCte {
+    private final String cteName;
+    private final List<NamedExpression> outputs;
+    private final List<List<SlotReference>> regularChildrenOutputs;
+    private final boolean isUnionAll;
+
+    /** PhysicalRecursiveCte */
+    public PhysicalRecursiveCte(String cteName, boolean isUnionAll,
+            List<NamedExpression> outputs,
+            List<List<SlotReference>> childrenOutputs,
+            LogicalProperties logicalProperties,
+            List<Plan> children) {
+        this(cteName, isUnionAll, outputs, childrenOutputs, Optional.empty(), logicalProperties, children);
+    }
+
+    /** PhysicalRecursiveCte */
+    public PhysicalRecursiveCte(String cteName, boolean isUnionAll,
+            List<NamedExpression> outputs,
+            List<List<SlotReference>> childrenOutputs,
+            Optional<GroupExpression> groupExpression,
+            LogicalProperties logicalProperties,
+            List<Plan> children) {
+        this(cteName, isUnionAll, outputs, childrenOutputs, groupExpression, logicalProperties,
+                PhysicalProperties.ANY, null, children);
+    }
+
+    /** PhysicalRecursiveCte */
+    public PhysicalRecursiveCte(String cteName, boolean isUnionAll, List<NamedExpression> outputs,
+            List<List<SlotReference>> childrenOutputs,
+            Optional<GroupExpression> groupExpression, LogicalProperties logicalProperties,
+            PhysicalProperties physicalProperties, Statistics statistics, List<Plan> children) {
+        super(PlanType.PHYSICAL_RECURSIVE_CTE, groupExpression, logicalProperties, physicalProperties,
+                statistics, children.toArray(new Plan[0]));
+        this.cteName = cteName;
+        this.isUnionAll = isUnionAll;
+        this.outputs = ImmutableList.copyOf(outputs);
+        this.regularChildrenOutputs = ImmutableList.copyOf(childrenOutputs);
+    }
+
+    @Override
+    public boolean isUnionAll() {
+        return isUnionAll;
+    }
+
+    public String getCteName() {
+        return cteName;
+    }
+
+    @Override
+    public List<SlotReference> getRegularChildOutput(int i) {
+        return regularChildrenOutputs.get(i);
+    }
+
+    @Override
+    public List<NamedExpression> getOutputs() {
+        return outputs;
+    }
+
+    @Override
+    public List<Slot> computeOutput() {
+        return outputs.stream()
+                .map(NamedExpression::toSlot)
+                .collect(ImmutableList.toImmutableList());
+    }
+
+    @Override
+    public List<List<SlotReference>> getRegularChildrenOutputs() {
+        return regularChildrenOutputs;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        PhysicalRecursiveCte that = (PhysicalRecursiveCte) o;
+        return cteName.equals(that.cteName) && isUnionAll == that.isUnionAll && Objects.equals(outputs, that.outputs)
+                && Objects.equals(regularChildrenOutputs, that.regularChildrenOutputs);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(cteName, isUnionAll, outputs, regularChildrenOutputs);
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitPhysicalRecursiveCte(this, context);
+    }
+
+    @Override
+    public List<? extends Expression> getExpressions() {
+        return regularChildrenOutputs.stream().flatMap(List::stream).collect(ImmutableList.toImmutableList());
+    }
+
+    @Override
+    public String toString() {
+        return Utils.toSqlString("PhysicalRecursiveCte" + "[" + id.asInt() + "]" + getGroupIdWithPrefix(),
+                "stats", statistics,
+                "cteName", cteName,
+                "isUnionAll", isUnionAll,
+                "outputs", outputs,
+                "regularChildrenOutputs", regularChildrenOutputs);
+    }
+
+    @Override
+    public String shapeInfo() {
+        ConnectContext context = ConnectContext.get();
+        if (context != null
+                && context.getSessionVariable().getDetailShapePlanNodesSet().contains(getClass().getSimpleName())) {
+            StringBuilder builder = new StringBuilder();
+            builder.append(getClass().getSimpleName());
+            builder.append(")");
+            return builder.toString();
+        } else {
+            return super.shapeInfo();
+        }
+    }
+
+    @Override
+    public PhysicalRecursiveCte withChildren(List<Plan> children) {
+        return new PhysicalRecursiveCte(cteName, isUnionAll, outputs, regularChildrenOutputs, groupExpression,
+                getLogicalProperties(), children);
+    }
+
+    @Override
+    public PhysicalRecursiveCte withGroupExpression(Optional<GroupExpression> groupExpression) {
+        return new PhysicalRecursiveCte(cteName, isUnionAll, outputs, regularChildrenOutputs,
+                groupExpression, getLogicalProperties(), children);
+    }
+
+    @Override
+    public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression> groupExpression,
+            Optional<LogicalProperties> logicalProperties, List<Plan> children) {
+        return new PhysicalRecursiveCte(cteName, isUnionAll, outputs, regularChildrenOutputs,
+                groupExpression, logicalProperties.get(), children);
+    }
+
+    @Override
+    public PhysicalRecursiveCte withPhysicalPropertiesAndStats(
+            PhysicalProperties physicalProperties, Statistics statistics) {
+        return new PhysicalRecursiveCte(cteName, isUnionAll, outputs, regularChildrenOutputs,
+                groupExpression, getLogicalProperties(), physicalProperties, statistics, children);
+    }
+
+    @Override
+    public PhysicalRecursiveCte resetLogicalProperties() {
+        return new PhysicalRecursiveCte(cteName, isUnionAll, outputs, regularChildrenOutputs,
+                Optional.empty(), null, physicalProperties, statistics, children);
+    }
+
+    @Override
+    public void computeUnique(DataTrait.Builder builder) {
+        if (!isUnionAll) {
+            builder.addUniqueSlot(ImmutableSet.copyOf(getOutput()));
+        }
+    }
+
+    @Override
+    public void computeUniform(DataTrait.Builder builder) {
+        // don't propagate uniform slots
+    }
+
+    private List<BitSet> mapSlotToIndex(Plan plan, List<Set<Slot>> equalSlotsList) {
+        Map<Slot, Integer> slotToIndex = new HashMap<>();
+        for (int i = 0; i < plan.getOutput().size(); i++) {
+            slotToIndex.put(plan.getOutput().get(i), i);
+        }
+        List<BitSet> equalSlotIndicesList = new ArrayList<>();
+        for (Set<Slot> equalSlots : equalSlotsList) {
+            BitSet equalSlotIndices = new BitSet();
+            for (Slot slot : equalSlots) {
+                if (slotToIndex.containsKey(slot)) {
+                    equalSlotIndices.set(slotToIndex.get(slot));
+                }
+            }
+            if (equalSlotIndices.cardinality() > 1) {
+                equalSlotIndicesList.add(equalSlotIndices);
+            }
+        }
+        return equalSlotIndicesList;
+    }
+
+    @Override
+    public void computeEqualSet(DataTrait.Builder builder) {
+        if (children.isEmpty()) {
+            return;
+        }
+
+        // Get the list of equal slot sets and their corresponding index mappings for the first child
+        List<Set<Slot>> childEqualSlotsList = child(0).getLogicalProperties()
+                .getTrait().calAllEqualSet();
+        List<BitSet> childEqualSlotsIndicesList = mapSlotToIndex(child(0), childEqualSlotsList);
+        List<BitSet> unionEqualSlotIndicesList = new ArrayList<>(childEqualSlotsIndicesList);
+
+        // Traverse all children and find the equal sets that exist in all children
+        for (int i = 1; i < children.size(); i++) {
+            Plan child = children.get(i);
+
+            // Get the equal slot sets for the current child
+            childEqualSlotsList = child.getLogicalProperties().getTrait().calAllEqualSet();
+
+            // Map slots to indices for the current child
+            childEqualSlotsIndicesList = mapSlotToIndex(child, childEqualSlotsList);
+
+            // Only keep the equal pairs that exist in all children of the union
+            // This is done by calculating the intersection of all children's equal slot indices
+            for (BitSet unionEqualSlotIndices : unionEqualSlotIndicesList) {
+                BitSet intersect = new BitSet();
+                for (BitSet childEqualSlotIndices : childEqualSlotsIndicesList) {
+                    if (unionEqualSlotIndices.intersects(childEqualSlotIndices)) {
+                        intersect = childEqualSlotIndices;
+                        break;
+                    }
+                }
+                unionEqualSlotIndices.and(intersect);
+            }
+        }
+
+        // Build the functional dependencies for the output slots
+        List<Slot> outputList = getOutput();
+        for (BitSet equalSlotIndices : unionEqualSlotIndicesList) {
+            if (equalSlotIndices.cardinality() <= 1) {
+                continue;
+            }
+            int first = equalSlotIndices.nextSetBit(0);
+            int next = equalSlotIndices.nextSetBit(first + 1);
+            while (next > 0) {
+                builder.addEqualPair(outputList.get(first), outputList.get(next));
+                next = equalSlotIndices.nextSetBit(next + 1);
+            }
+        }
+    }
+
+    @Override
+    public void computeFd(DataTrait.Builder builder) {
+        // don't generate
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalRecursiveCteRecursiveChild.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalRecursiveCteRecursiveChild.java
new file mode 100644
index 00000000000..9aef71e7ee9
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalRecursiveCteRecursiveChild.java
@@ -0,0 +1,121 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.physical;
+
+import org.apache.doris.nereids.memo.GroupExpression;
+import org.apache.doris.nereids.properties.DataTrait;
+import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.nereids.util.Utils;
+import org.apache.doris.statistics.Statistics;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * PhysicalRecursiveCteRecursiveChild is sentinel plan for must_shuffle
+ */
+public class PhysicalRecursiveCteRecursiveChild<CHILD_TYPE extends Plan> extends PhysicalUnary<CHILD_TYPE> {
+    private final String cteName;
+
+    public PhysicalRecursiveCteRecursiveChild(String cteName, LogicalProperties logicalProperties, CHILD_TYPE child) {
+        this(cteName, Optional.empty(), logicalProperties, child);
+    }
+
+    public PhysicalRecursiveCteRecursiveChild(String cteName, Optional<GroupExpression> groupExpression,
+            LogicalProperties logicalProperties, CHILD_TYPE child) {
+        this(cteName, groupExpression, logicalProperties, PhysicalProperties.ANY, null, child);
+    }
+
+    public PhysicalRecursiveCteRecursiveChild(String cteName, Optional<GroupExpression> groupExpression,
+            LogicalProperties logicalProperties, @Nullable PhysicalProperties physicalProperties, Statistics statistics,
+            CHILD_TYPE child) {
+        super(PlanType.PHYSICAL_RECURSIVE_CTE_RECURSIVE_CHILD, groupExpression, logicalProperties, physicalProperties,
+                statistics, child);
+        this.cteName = cteName;
+    }
+
+    @Override
+    public String toString() {
+        return Utils.toSqlStringSkipNull("PhysicalRecursiveCteRecursiveChild",
+                "cteName", cteName);
+    }
+
+    @Override
+    public Plan withChildren(List<Plan> children) {
+        Preconditions.checkArgument(children.size() == 1);
+        return new PhysicalRecursiveCteRecursiveChild<>(cteName, groupExpression, getLogicalProperties(),
+                children.get(0));
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitPhysicalRecursiveCteRecursiveChild(this, context);
+    }
+
+    @Override
+    public List<? extends Expression> getExpressions() {
+        return ImmutableList.of();
+    }
+
+    @Override
+    public Plan withGroupExpression(Optional<GroupExpression> groupExpression) {
+        return new PhysicalRecursiveCteRecursiveChild<>(cteName, groupExpression, getLogicalProperties(), child());
+    }
+
+    @Override
+    public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression> groupExpression,
+            Optional<LogicalProperties> logicalProperties, List<Plan> children) {
+        Preconditions.checkArgument(children.size() == 1);
+        return new PhysicalRecursiveCteRecursiveChild<>(cteName, groupExpression, logicalProperties.get(), child());
+    }
+
+    @Override
+    public void computeUnique(DataTrait.Builder builder) {
+
+    }
+
+    @Override
+    public void computeUniform(DataTrait.Builder builder) {
+
+    }
+
+    @Override
+    public void computeEqualSet(DataTrait.Builder builder) {
+
+    }
+
+    @Override
+    public void computeFd(DataTrait.Builder builder) {
+
+    }
+
+    @Override
+    public PhysicalPlan withPhysicalPropertiesAndStats(PhysicalProperties physicalProperties, Statistics statistics) {
+        return new PhysicalRecursiveCteRecursiveChild<>(cteName, groupExpression, getLogicalProperties(),
+                physicalProperties, statistics, child());
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalRecursiveCteScan.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalRecursiveCteScan.java
new file mode 100644
index 00000000000..3450ae0de18
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalRecursiveCteScan.java
@@ -0,0 +1,85 @@
+// 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.
+
+package org.apache.doris.nereids.trees.plans.physical;
+
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.nereids.memo.GroupExpression;
+import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.properties.PhysicalProperties;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.RelationId;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.nereids.util.Utils;
+import org.apache.doris.statistics.Statistics;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * PhysicalRecursiveCteScan.
+ */
+public class PhysicalRecursiveCteScan extends PhysicalCatalogRelation {
+    public PhysicalRecursiveCteScan(RelationId relationId, TableIf table, List<String> qualifier,
+            Optional<GroupExpression> groupExpression, LogicalProperties logicalProperties,
+            Collection<Slot> operativeSlots) {
+        this(relationId, table, qualifier, groupExpression, logicalProperties, PhysicalProperties.ANY, null,
+                operativeSlots);
+    }
+
+    public PhysicalRecursiveCteScan(RelationId relationId, TableIf table, List<String> qualifier,
+            Optional<GroupExpression> groupExpression, LogicalProperties logicalProperties,
+            PhysicalProperties physicalProperties, Statistics statistics, Collection<Slot> operativeSlots) {
+        super(relationId, PlanType.PHYSICAL_RECURSIVE_CTE_SCAN, table, qualifier, groupExpression, logicalProperties,
+                physicalProperties, statistics, operativeSlots);
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitPhysicalRecursiveCteScan(this, context);
+    }
+
+    @Override
+    public Plan withGroupExpression(Optional<GroupExpression> groupExpression) {
+        return new PhysicalRecursiveCteScan(relationId, table, qualifier, groupExpression, getLogicalProperties(),
+                physicalProperties, statistics, operativeSlots);
+    }
+
+    @Override
+    public Plan withGroupExprLogicalPropChildren(Optional<GroupExpression> groupExpression,
+            Optional<LogicalProperties> logicalProperties, List<Plan> children) {
+        return new PhysicalRecursiveCteScan(relationId, table, qualifier, groupExpression, getLogicalProperties(),
+                physicalProperties, statistics, operativeSlots);
+    }
+
+    @Override
+    public PhysicalPlan withPhysicalPropertiesAndStats(PhysicalProperties physicalProperties, Statistics statistics) {
+        return new PhysicalRecursiveCteScan(relationId, table, qualifier, groupExpression, getLogicalProperties(),
+                physicalProperties, statistics, operativeSlots);
+    }
+
+    @Override
+    public String toString() {
+        return Utils.toSqlString("PhysicalRecursiveCteScan[" + table.getName() + "]" + getGroupIdWithPrefix(),
+                "stats", statistics,
+                "qualified", Utils.qualifiedName(qualifier, table.getName()),
+                "operativeCols", getOperativeSlots());
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/PlanVisitor.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/PlanVisitor.java
index 2a1c8c4dc59..f2c9ec69bde 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/PlanVisitor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/PlanVisitor.java
@@ -45,6 +45,8 @@ import org.apache.doris.nereids.trees.plans.logical.LogicalPreAggOnHint;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPreFilter;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalQualify;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCte;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCteRecursiveChild;
 import org.apache.doris.nereids.trees.plans.logical.LogicalRelation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalRepeat;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSelectHint;
@@ -79,6 +81,8 @@ import org.apache.doris.nereids.trees.plans.physical.PhysicalNestedLoopJoin;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPartitionTopN;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalQuickSort;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCte;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCteRecursiveChild;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalRelation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalRepeat;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalSetOperation;
@@ -212,6 +216,15 @@ public abstract class PlanVisitor<R, C> implements CommandVisitor<R, C>, Relatio
         return visit(join, context);
     }
 
+    public R visitLogicalRecursiveCte(LogicalRecursiveCte recursiveCte, C context) {
+        return visit(recursiveCte, context);
+    }
+
+    public R visitLogicalRecursiveCteRecursiveChild(LogicalRecursiveCteRecursiveChild<? extends Plan> recursiveChild,
+            C context) {
+        return visit(recursiveChild, context);
+    }
+
     public R visitLogicalLimit(LogicalLimit<? extends Plan> limit, C context) {
         return visit(limit, context);
     }
@@ -381,6 +394,15 @@ public abstract class PlanVisitor<R, C> implements CommandVisitor<R, C>, Relatio
         return visitPhysicalSetOperation(union, context);
     }
 
+    public R visitPhysicalRecursiveCte(PhysicalRecursiveCte recursiveCte, C context) {
+        return visit(recursiveCte, context);
+    }
+
+    public R visitPhysicalRecursiveCteRecursiveChild(PhysicalRecursiveCteRecursiveChild<? extends Plan> recursiveChild,
+            C context) {
+        return visit(recursiveChild, context);
+    }
+
     public R visitAbstractPhysicalSort(AbstractPhysicalSort<? extends Plan> sort, C context) {
         return visit(sort, context);
     }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/RelationVisitor.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/RelationVisitor.java
index fef94ff52f9..b325849b1b1 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/RelationVisitor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/RelationVisitor.java
@@ -30,6 +30,7 @@ import org.apache.doris.nereids.trees.plans.logical.LogicalJdbcScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOdbcScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
+import org.apache.doris.nereids.trees.plans.logical.LogicalRecursiveCteScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalRelation;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSchemaScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTVFRelation;
@@ -45,6 +46,7 @@ import org.apache.doris.nereids.trees.plans.physical.PhysicalJdbcScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalOdbcScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalOneRowRelation;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalRecursiveCteScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalRelation;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalSchemaScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalTVFRelation;
@@ -139,6 +141,10 @@ public interface RelationVisitor<R, C> {
         return visitLogicalCatalogRelation(testScan, context);
     }
 
+    default R visitLogicalRecursiveCteScan(LogicalRecursiveCteScan recursiveCteScan, C context) {
+        return visitLogicalCatalogRelation(recursiveCteScan, context);
+    }
+
     // *******************************
     // physical relations
     // *******************************
@@ -176,6 +182,10 @@ public interface RelationVisitor<R, C> {
         return visitPhysicalCatalogRelation(deferMaterializeOlapScan, context);
     }
 
+    default R visitPhysicalRecursiveCteScan(PhysicalRecursiveCteScan recursiveCteScan, C context) {
+        return visitPhysicalCatalogRelation(recursiveCteScan, context);
+    }
+
     default R visitPhysicalOneRowRelation(PhysicalOneRowRelation oneRowRelation, C context) {
         return visitPhysicalRelation(oneRowRelation, context);
     }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/planner/RecursiveCteNode.java b/fe/fe-core/src/main/java/org/apache/doris/planner/RecursiveCteNode.java
new file mode 100644
index 00000000000..c1531813d3c
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/RecursiveCteNode.java
@@ -0,0 +1,87 @@
+// 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.
+
+package org.apache.doris.planner;
+
+import org.apache.doris.analysis.Expr;
+import org.apache.doris.analysis.TupleId;
+import org.apache.doris.statistics.StatisticalType;
+import org.apache.doris.thrift.TExplainLevel;
+import org.apache.doris.thrift.TPlanNode;
+import org.apache.doris.thrift.TPlanNodeType;
+import org.apache.doris.thrift.TRecCTENode;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+public class RecursiveCteNode extends PlanNode {
+    private String cteName;
+    private boolean isUnionAll;
+    private List<List<Expr>> materializedResultExprLists = Lists.newArrayList();
+    private TRecCTENode tRecCTENode;
+
+    public RecursiveCteNode(PlanNodeId id, TupleId tupleId, String cteName, boolean isUnionAll) {
+        super(id, tupleId.asList(), "RECURSIVE_CTE", StatisticalType.REC_CTE_NODE);
+        this.cteName = cteName;
+        this.isUnionAll = isUnionAll;
+    }
+
+    public boolean isUnionAll() {
+        return isUnionAll;
+    }
+
+    public void setMaterializedResultExprLists(List<List<Expr>> exprs) {
+        this.materializedResultExprLists = exprs;
+    }
+
+    public List<List<Expr>> getMaterializedResultExprLists() {
+        return materializedResultExprLists;
+    }
+
+    public void settRecCTENode(TRecCTENode tRecCTENode) {
+        this.tRecCTENode = tRecCTENode;
+    }
+
+    @Override
+    protected void toThrift(TPlanNode msg) {
+        msg.node_type = TPlanNodeType.REC_CTE_NODE;
+        msg.rec_cte_node = tRecCTENode;
+    }
+
+    @Override
+    public String getNodeExplainString(String prefix, TExplainLevel detailLevel) {
+        StringBuilder output = new StringBuilder();
+        output.append(prefix).append("Recursive Cte: ").append(cteName).append("\n");
+        output.append(prefix).append("isUnionAll: ").append(isUnionAll).append("\n");
+        if (!conjuncts.isEmpty()) {
+            Expr expr = convertConjunctsToAndCompoundPredicate(conjuncts);
+            output.append(prefix).append("PREDICATES: ").append(expr.toSql()).append("\n");
+        }
+        return output.toString();
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("name", cteName)
+                .add("id", getId().asInt())
+                .add("tid", tupleIds.get(0).asInt())
+                .add("isUnionAll", isUnionAll).toString();
+    }
+}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/planner/RecursiveCteScanNode.java b/fe/fe-core/src/main/java/org/apache/doris/planner/RecursiveCteScanNode.java
new file mode 100644
index 00000000000..97aedcb1f9d
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/RecursiveCteScanNode.java
@@ -0,0 +1,123 @@
+// 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.
+
+package org.apache.doris.planner;
+
+import org.apache.doris.analysis.Expr;
+import org.apache.doris.analysis.TupleDescriptor;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.UserException;
+import org.apache.doris.statistics.StatisticalType;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.TExplainLevel;
+import org.apache.doris.thrift.TNetworkAddress;
+import org.apache.doris.thrift.TPlanNode;
+import org.apache.doris.thrift.TPlanNodeType;
+import org.apache.doris.thrift.TScanRange;
+import org.apache.doris.thrift.TScanRangeLocation;
+import org.apache.doris.thrift.TScanRangeLocations;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Lists;
+
+import java.util.Collections;
+import java.util.List;
+
+// Full scan of recursive cte temp table
+public class RecursiveCteScanNode extends ScanNode {
+    private final String recursiveCteName;
+
+    public RecursiveCteScanNode(String recursiveCteName, PlanNodeId id, TupleDescriptor desc,
+            ScanContext scanContext) {
+        super(id, desc, "RECURSIVE_CTE_SCAN", scanContext, StatisticalType.REC_CTE_SCAN_NODE);
+        this.recursiveCteName = recursiveCteName;
+    }
+
+    public void initScanRangeLocations() throws UserException {
+        createScanRangeLocations();
+    }
+
+    @Override
+    protected void createScanRangeLocations() throws UserException {
+        scanRangeLocations = Lists.newArrayList();
+        // randomly select 1 backend
+        List<Backend> backendList = Lists.newArrayList();
+        for (Backend be : Env.getCurrentSystemInfo().getBackendsByCurrentCluster().values()) {
+            if (be.isAlive()) {
+                backendList.add(be);
+            }
+        }
+        if (backendList.isEmpty()) {
+            throw new UserException("No Alive backends");
+        }
+        Collections.shuffle(backendList);
+        Backend selectedBackend = backendList.get(0);
+
+        // create scan range locations
+        TScanRangeLocation location = new TScanRangeLocation();
+        location.setBackendId(selectedBackend.getId());
+        location.setServer(new TNetworkAddress(selectedBackend.getHost(), selectedBackend.getBePort()));
+        TScanRangeLocations locations = new TScanRangeLocations();
+        locations.addToLocations(location);
+        locations.setScanRange(new TScanRange());
+        scanRangeLocations.add(locations);
+    }
+
+    @Override
+    public List<TScanRangeLocations> getScanRangeLocations(long maxScanRangeLength) {
+        return scanRangeLocations;
+    }
+
+    @Override
+    public int getNumInstances() {
+        return 1;
+    }
+
+    @Override
+    public int getScanRangeNum() {
+        return 1;
+    }
+
+    @Override
+    public String getNodeExplainString(String prefix, TExplainLevel detailLevel) {
+        StringBuilder output = new StringBuilder();
+        output.append(prefix).append("Recursive Cte: ").append(recursiveCteName).append("\n");
+        if (!conjuncts.isEmpty()) {
+            Expr expr = convertConjunctsToAndCompoundPredicate(conjuncts);
+            output.append(prefix).append("PREDICATES: ").append(expr.toSql()).append("\n");
+        }
+        return output.toString();
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("recursiveCteName", recursiveCteName)
+                .add("id", getId().asInt())
+                .add("tid", desc.getId().asInt()).toString();
+    }
+
+    @Override
+    protected void toThrift(TPlanNode msg) {
+        msg.node_type = TPlanNodeType.REC_CTE_SCAN_NODE;
+    }
+
+    @Override
+    public boolean isSerialOperator() {
+        return true;
+    }
+}
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 e1114a3f8f4..1fd733e6db9 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
@@ -839,6 +839,8 @@ public class SessionVariable implements Serializable, Writable {
 
     public static final String READ_HIVE_JSON_IN_ONE_COLUMN = "read_hive_json_in_one_column";
 
+    public static final String CTE_MAX_RECURSION_DEPTH = "cte_max_recursion_depth";
+
     /**
      * Inserting overwrite for auto partition table allows creating partition for
      * datas which cannot find partition to overwrite.
@@ -1079,6 +1081,11 @@ public class SessionVariable implements Serializable, Writable {
     }, varType = VariableAnnotation.DEPRECATED)
     public int minScanSchedulerConcurrency = 0;
 
+    @VariableMgr.VarAttr(name = CTE_MAX_RECURSION_DEPTH, needForward = true, description = {
+            "CTE递归的最大深度,默认值100",
+            "The maximum depth of CTE recursion. Default is 100" })
+    public int cteMaxRecursionDepth = 100;
+
     // By default, the number of Limit items after OrderBy is changed from 65535 items
     // before v1.2.0 (not included), to return all items by default
     @VariableMgr.VarAttr(name = DEFAULT_ORDER_BY_LIMIT, affectQueryResultInExecution = true)
@@ -5266,6 +5273,7 @@ public class SessionVariable implements Serializable, Writable {
         tResult.setInvertedIndexCompatibleRead(invertedIndexCompatibleRead);
         tResult.setEnableInvertedIndexWandQuery(enableInvertedIndexWandQuery);
 
+        tResult.setCteMaxRecursionDepth(cteMaxRecursionDepth);
         tResult.setEnableParallelScan(enableParallelScan);
         tResult.setEnableLeftSemiDirectReturnOpt(enableLeftSemiDirectReturnOpt);
         tResult.setEnableAggregateFunctionNullV2(enableAggregateFunctionNullV2);
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/ThriftPlansBuilder.java b/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/ThriftPlansBuilder.java
index 13391013f52..0738876282f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/ThriftPlansBuilder.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/ThriftPlansBuilder.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.qe.runtime;
 
+import org.apache.doris.analysis.Expr;
 import org.apache.doris.catalog.AIResource;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.catalog.Resource;
@@ -41,6 +42,11 @@ import org.apache.doris.planner.MultiCastDataSink;
 import org.apache.doris.planner.OlapScanNode;
 import org.apache.doris.planner.OlapTableSink;
 import org.apache.doris.planner.PlanFragment;
+import org.apache.doris.planner.PlanFragmentId;
+import org.apache.doris.planner.PlanNode;
+import org.apache.doris.planner.RecursiveCteNode;
+import org.apache.doris.planner.RecursiveCteScanNode;
+import org.apache.doris.planner.RuntimeFilter;
 import org.apache.doris.planner.ScanNode;
 import org.apache.doris.planner.SortNode;
 import org.apache.doris.qe.ConnectContext;
@@ -48,6 +54,7 @@ import org.apache.doris.qe.CoordinatorContext;
 import org.apache.doris.thrift.PaloInternalServiceVersion;
 import org.apache.doris.thrift.TAIResource;
 import org.apache.doris.thrift.TDataSinkType;
+import org.apache.doris.thrift.TExpr;
 import org.apache.doris.thrift.TFileScanRangeParams;
 import org.apache.doris.thrift.TNetworkAddress;
 import org.apache.doris.thrift.TPipelineFragmentParams;
@@ -56,10 +63,14 @@ import org.apache.doris.thrift.TPipelineInstanceParams;
 import org.apache.doris.thrift.TPlanFragment;
 import org.apache.doris.thrift.TPlanFragmentDestination;
 import org.apache.doris.thrift.TQueryOptions;
+import org.apache.doris.thrift.TRecCTENode;
+import org.apache.doris.thrift.TRecCTEResetInfo;
+import org.apache.doris.thrift.TRecCTETarget;
 import org.apache.doris.thrift.TRuntimeFilterInfo;
 import org.apache.doris.thrift.TRuntimeFilterParams;
 import org.apache.doris.thrift.TScanRangeParams;
 import org.apache.doris.thrift.TTopnFilterDesc;
+import org.apache.doris.thrift.TUniqueId;
 
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ArrayListMultimap;
@@ -75,10 +86,13 @@ import org.apache.logging.log4j.Logger;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.function.BiConsumer;
 import java.util.function.Supplier;
 
@@ -89,6 +103,8 @@ public class ThriftPlansBuilder {
             CoordinatorContext coordinatorContext) {
 
         List<PipelineDistributedPlan> distributedPlans = coordinatorContext.distributedPlans;
+        Set<Integer> fragmentToNotifyClose = setParamsForRecursiveCteNode(distributedPlans,
+                coordinatorContext.runtimeFilters);
 
         // Determine whether this query is assigned to a single backend and propagate it to
         // TQueryOptions so that BE can apply more appropriate optimization strategies (e.g.
@@ -119,7 +135,7 @@ public class ThriftPlansBuilder {
                 TPipelineFragmentParams currentFragmentParam = fragmentToThriftIfAbsent(
                         currentFragmentPlan, instanceJob, workerToCurrentFragment,
                         instancesPerWorker, exchangeSenderNum, sharedFileScanRangeParams,
-                        workerProcessInstanceNum, coordinatorContext);
+                        workerProcessInstanceNum, fragmentToNotifyClose, coordinatorContext);
 
                 TPipelineInstanceParams instanceParam = instanceToThrift(
                         currentFragmentParam, instanceJob, currentInstanceIndex++);
@@ -330,6 +346,7 @@ public class ThriftPlansBuilder {
             Map<Integer, Integer> exchangeSenderNum,
             Map<Integer, TFileScanRangeParams> fileScanRangeParamsMap,
             Multiset<DistributedPlanWorker> workerProcessInstanceNum,
+            Set<Integer> fragmentToNotifyClose,
             CoordinatorContext coordinatorContext) {
         DistributedPlanWorker worker = assignedJob.getAssignedWorker();
         return workerToFragmentParams.computeIfAbsent(worker, w -> {
@@ -343,6 +360,9 @@ public class ThriftPlansBuilder {
             params.setDescTbl(coordinatorContext.descriptorTable);
             params.setQueryId(coordinatorContext.queryId);
             params.setFragmentId(fragment.getFragmentId().asInt());
+            if (fragmentToNotifyClose.contains(params.getFragmentId())) {
+                params.setNeedNotifyClose(true);
+            }
 
             // Each tParam will set the total number of Fragments that need to be executed on the same BE,
             // and the BE will determine whether all Fragments have been executed based on this information.
@@ -581,6 +601,215 @@ public class ThriftPlansBuilder {
         }
     }
 
+    private static Set<Integer> setParamsForRecursiveCteNode(List<PipelineDistributedPlan> distributedPlans,
+            List<RuntimeFilter> runtimeFilters) {
+        /*
+         * Populate and attach recursive-CTE related Thrift structures used by
+         * backends (BE) to coordinate recursive Common Table Expression (CTE)
+         * execution across fragments and fragment instances.
+         *
+         * This method performs the following responsibilities:
+         * - Traverse the provided `distributedPlans` in bottom-up order (this
+         *   ordering is expected by callers) and collect the set of network
+         *   addresses (host + brpcPort) for every fragment. These addresses are
+         *   used to reset and control recursive CTE child fragments from the
+         *   producer side.
+         * - Detect `RecursiveCteScanNode` within fragments and build a
+         *   `TRecCTETarget` for each such scan. A `TRecCTETarget` captures the
+         *   network address and a representative fragment instance id and the
+         *   scan node id that the recursive producer should send data to.
+         *   Exactly one `RecursiveCteScanNode` is expected per fragment that
+         *   contains a scan for a recursive CTE; otherwise an
+         *   IllegalStateException is thrown.
+         * - For every `RecursiveCteNode` (producer/union node), collect its
+         *   child fragments that implement the recursive side. For each child
+         *   fragment, add the corresponding `TRecCTETarget` (if present) to
+         *   the producer's target list and create `TRecCTEResetInfo` entries
+         *   for all instances of that fragment. `TRecCTEResetInfo` entries
+         *   carry the fragment id and addresses to be reset by the producer
+         *   when a new recursion iteration begins.
+         * - Populate the `TRecCTENode` object attached to the
+         *   `RecursiveCteNode`, including: whether it's `UNION ALL`, result
+         *   expression lists (materialized result expressions converted to
+         *   Thrift `TExpr`), list of targets, fragments-to-reset, runtime
+         *   filter ids that must be reset on the recursive side, and a flag
+         *   indicating whether this recursive CTE node is used by other
+         *   recursive CTEs.
+         *
+         * How runtime filters are handled:
+         * - Build `runtimeFiltersToReset` by scanning provided
+         *   `runtimeFilters`. A filter id is added if the filter has remote
+         *   targets and if the recursive side (right child) contains the
+         *   runtime-filter builder node. These filter ids are attached to the
+         *   `TRecCTENode` so BE can reset the corresponding runtime filters
+         *   between recursive iterations.
+         *
+         * Important assumptions and invariants:
+         * - `distributedPlans` must be ordered bottom-up so that child
+         *   fragments (containing `RecursiveCteScanNode`) are visited before
+         *   their producers. The implementation relies on this to pop
+         *   consumed `TRecCTETarget` entries from `fragmentIdToRecCteTargetMap`
+         *   to avoid a parent producer incorrectly picking up grandchild
+         *   scan nodes.
+         * - Each fragment containing a `RecursiveCteScanNode` must have at
+         *   least one assigned job (instance). If not, an
+         *   IllegalStateException is thrown.
+         * - At most one `RecursiveCteScanNode` per fragment is supported; if
+         *   more than one is found an IllegalStateException is thrown.
+         *
+         * @param distributedPlans ordered list of PipelineDistributedPlan in
+         *                         bottom-up traversal order
+         * @param runtimeFilters   list of runtime filters to consider for reset
+         * @return set of fragment ids (as integers) that need to be notified
+         *         to close for recursive CTE handling
+         */
+        // fragments whose child recursive fragments need to be notified to close
+        Set<Integer> fragmentToNotifyClose = new HashSet<>();
+        // mapping from fragment id -> TRecCTETarget (the scan node target info)
+        Map<PlanFragmentId, List<TRecCTETarget>> fragmentIdToRecCteTargetMap = new TreeMap<>();
+        // mapping from fragment id -> set of network addresses for all instances
+        Map<PlanFragmentId, Set<TNetworkAddress>> fragmentIdToNetworkAddressMap = new TreeMap<>();
+        // distributedPlans is ordered in bottom up way, so does the fragments
+        for (PipelineDistributedPlan plan : distributedPlans) {
+            // collect all assigned instance network addresses for this fragment
+            List<AssignedJob> fragmentAssignedJobs = plan.getInstanceJobs();
+            Set<TNetworkAddress> networkAddresses = new TreeSet<>();
+            Map<TNetworkAddress, TUniqueId> addressTUniqueIdMap = new TreeMap<>();
+            for (AssignedJob assignedJob : fragmentAssignedJobs) {
+                DistributedPlanWorker distributedPlanWorker = assignedJob.getAssignedWorker();
+                // use brpc port + host as the address used by BE for control/reset
+                TNetworkAddress networkAddress = new TNetworkAddress(distributedPlanWorker.host(),
+                        distributedPlanWorker.brpcPort());
+                if (networkAddresses.add(networkAddress)) {
+                    addressTUniqueIdMap.put(networkAddress, assignedJob.instanceId());
+                }
+            }
+            PlanFragment planFragment = plan.getFragmentJob().getFragment();
+            // remember addresses for later when building reset infos
+            fragmentIdToNetworkAddressMap.put(planFragment.getFragmentId(), networkAddresses);
+
+            // find RecursiveCteScanNode in this fragment (scan side of recursive CTE)
+            List<RecursiveCteScanNode> recursiveCteScanNodes = planFragment.getPlanRoot()
+                    .collectInCurrentFragment(RecursiveCteScanNode.class::isInstance);
+            if (!recursiveCteScanNodes.isEmpty()) {
+                // validate there is exactly one scan node per fragment
+                if (recursiveCteScanNodes.size() != 1) {
+                    throw new IllegalStateException(
+                            String.format("one fragment can only have 1 recursive cte scan node, but there is %d",
+                                    recursiveCteScanNodes.size()));
+                }
+                // scan fragments must have at least one assigned instance
+                if (fragmentAssignedJobs.isEmpty()) {
+                    throw new IllegalStateException(
+                            "fragmentAssignedJobs is empty for recursive cte scan node");
+                }
+                // Build a TRecCTETargets
+                List<TRecCTETarget> recCTETargets = new ArrayList<>(addressTUniqueIdMap.size());
+                for (Entry<TNetworkAddress, TUniqueId> entry : addressTUniqueIdMap.entrySet()) {
+                    TRecCTETarget tRecCTETarget = new TRecCTETarget();
+                    tRecCTETarget.setAddr(entry.getKey());
+                    tRecCTETarget.setFragmentInstanceId(entry.getValue());
+                    tRecCTETarget.setNodeId(recursiveCteScanNodes.get(0).getId().asInt());
+                    recCTETargets.add(tRecCTETarget);
+                }
+                // store the target for producers to reference later
+                fragmentIdToRecCteTargetMap.put(planFragment.getFragmentId(), recCTETargets);
+            }
+
+            List<RecursiveCteNode> recursiveCteNodes = planFragment.getPlanRoot()
+                    .collectInCurrentFragment(RecursiveCteNode.class::isInstance);
+            for (RecursiveCteNode recursiveCteNode : recursiveCteNodes) {
+                // list of scan targets this producer should send recursive rows to
+                List<TRecCTETarget> targets = new ArrayList<>();
+                // reset infos for all instances of child fragments (used to reset state)
+                List<TRecCTEResetInfo> fragmentsToReset = new ArrayList<>();
+                // The recursive side is under the right child; collect all fragments
+                List<PlanFragment> childFragments = new ArrayList<>();
+                recursiveCteNode.getChild(1).getChild(0).getFragment().collectAll(PlanFragment.class::isInstance,
+                        childFragments);
+                for (PlanFragment child : childFragments) {
+                    PlanFragmentId childFragmentId = child.getFragmentId();
+                    // mark this child fragment id so it will be notified to close
+                    fragmentToNotifyClose.add(childFragmentId.asInt());
+                    // add target if a matching RecursiveCteScanNode was recorded
+                    List<TRecCTETarget> recCTETargets = fragmentIdToRecCteTargetMap.getOrDefault(childFragmentId, null);
+                    if (recCTETargets != null) {
+                        // each producer can only map to one scan node target per child
+                        targets.addAll(recCTETargets);
+                        // remove the entry so ancestor producers won't reuse a grandchild scan node
+                        fragmentIdToRecCteTargetMap.remove(childFragmentId);
+                    }
+                    // get all instance addresses for this child fragment and build reset infos
+                    Set<TNetworkAddress> tNetworkAddresses = fragmentIdToNetworkAddressMap.get(childFragmentId);
+                    if (tNetworkAddresses == null) {
+                        throw new IllegalStateException(
+                                String.format("can't find TNetworkAddress for fragment %d", childFragmentId));
+                    }
+                    for (TNetworkAddress address : tNetworkAddresses) {
+                        TRecCTEResetInfo tRecCTEResetInfo = new TRecCTEResetInfo();
+                        tRecCTEResetInfo.setFragmentId(childFragmentId.asInt());
+                        tRecCTEResetInfo.setAddr(address);
+                        fragmentsToReset.add(tRecCTEResetInfo);
+                    }
+                }
+
+                // convert materialized result expression lists to Thrift TExpr lists
+                List<List<Expr>> materializedResultExprLists = recursiveCteNode.getMaterializedResultExprLists();
+                List<List<TExpr>> texprLists = new ArrayList<>(materializedResultExprLists.size());
+                for (List<Expr> exprList : materializedResultExprLists) {
+                    texprLists.add(Expr.treesToThrift(exprList));
+                }
+                // the recursive side's rf need to be reset
+                // determine which runtime filters on the recursive side must be reset
+                List<Integer> runtimeFiltersToReset = new ArrayList<>(runtimeFilters.size());
+                for (RuntimeFilter rf : runtimeFilters) {
+                    // only consider filters that have remote targets and whose builder
+                    // node is present in the recursive side (right child)
+                    if (rf.hasRemoteTargets()
+                            && recursiveCteNode.getChild(1).contains(node -> node == rf.getBuilderNode())) {
+                        runtimeFiltersToReset.add(rf.getFilterId().asInt());
+                    }
+                }
+                // find recursiveCte used by other recursive cte
+                // detect whether this recursive CTE node is referenced by other
+                // recursive CTEs in the recursive side; needed to correctly
+                // indicate sharing/usage across recursive nodes
+                Set<RecursiveCteNode> recursiveCteNodesInRecursiveSide = new HashSet<>();
+                PlanNode rootPlan = distributedPlans.get(distributedPlans.size() - 1)
+                        .getFragmentJob().getFragment().getPlanRoot();
+                collectAllRecursiveCteNodesInRecursiveSide(rootPlan, false, recursiveCteNodesInRecursiveSide);
+                boolean isUsedByOtherRecCte = recursiveCteNodesInRecursiveSide.contains(recursiveCteNode);
+
+                // build the Thrift TRecCTENode and attach it to the RecursiveCteNode
+                TRecCTENode tRecCTENode = new TRecCTENode();
+                tRecCTENode.setIsUnionAll(recursiveCteNode.isUnionAll());
+                tRecCTENode.setTargets(targets);
+                tRecCTENode.setFragmentsToReset(fragmentsToReset);
+                tRecCTENode.setResultExprLists(texprLists);
+                tRecCTENode.setRecSideRuntimeFilterIds(runtimeFiltersToReset);
+                tRecCTENode.setIsUsedByOtherRecCte(isUsedByOtherRecCte);
+                // attach Thrift node to plan node for BE consumption
+                recursiveCteNode.settRecCTENode(tRecCTENode);
+            }
+        }
+        return fragmentToNotifyClose;
+    }
+
+    private static void collectAllRecursiveCteNodesInRecursiveSide(PlanNode planNode, boolean needCollect,
+            Set<RecursiveCteNode> recursiveCteNodes) {
+        if (planNode instanceof RecursiveCteNode) {
+            if (needCollect) {
+                recursiveCteNodes.add((RecursiveCteNode) planNode);
+            }
+            collectAllRecursiveCteNodesInRecursiveSide(planNode.getChild(0), needCollect, recursiveCteNodes);
+            collectAllRecursiveCteNodesInRecursiveSide(planNode.getChild(1), true, recursiveCteNodes);
+        } else {
+            for (PlanNode child : planNode.getChildren()) {
+                collectAllRecursiveCteNodesInRecursiveSide(child, needCollect, recursiveCteNodes);
+            }
+        }
+    }
+
     private static class PerNodeScanParams {
         Map<Integer, List<TScanRangeParams>> perNodeScanRanges;
 
diff --git a/fe/fe-core/src/main/java/org/apache/doris/statistics/StatisticalType.java b/fe/fe-core/src/main/java/org/apache/doris/statistics/StatisticalType.java
index 0ec7a518078..a14cabd6a4a 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/statistics/StatisticalType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/statistics/StatisticalType.java
@@ -42,6 +42,8 @@ public enum StatisticalType {
     OLAP_SCAN_NODE,
     PARTITION_TOPN_NODE,
     REPEAT_NODE,
+    REC_CTE_NODE,
+    REC_CTE_SCAN_NODE,
     SELECT_NODE,
     SET_OPERATION_NODE,
     SCHEMA_SCAN_NODE,
diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/RuntimeFilterTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/RuntimeFilterTest.java
index 3d6964d8d7e..f1164ece13e 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/RuntimeFilterTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/RuntimeFilterTest.java
@@ -18,13 +18,17 @@
 package org.apache.doris.nereids.postprocess;
 
 import org.apache.doris.common.Pair;
+import org.apache.doris.nereids.NereidsPlanner;
+import org.apache.doris.nereids.StatementContext;
 import org.apache.doris.nereids.datasets.ssb.SSBTestBase;
 import org.apache.doris.nereids.datasets.ssb.SSBUtils;
 import org.apache.doris.nereids.glue.translator.PhysicalPlanTranslator;
 import org.apache.doris.nereids.glue.translator.PlanTranslatorContext;
 import org.apache.doris.nereids.hint.DistributeHint;
+import org.apache.doris.nereids.parser.NereidsParser;
 import org.apache.doris.nereids.processor.post.PlanPostProcessors;
 import org.apache.doris.nereids.processor.post.RuntimeFilterContext;
+import org.apache.doris.nereids.properties.PhysicalProperties;
 import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.EqualTo;
 import org.apache.doris.nereids.trees.expressions.ExprId;
@@ -34,13 +38,17 @@ import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
 import org.apache.doris.nereids.trees.plans.DistributeType;
 import org.apache.doris.nereids.trees.plans.JoinType;
 import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.commands.ExplainCommand;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalPlan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalOlapScan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
 import org.apache.doris.nereids.trees.plans.physical.RuntimeFilter;
+import org.apache.doris.nereids.util.MemoTestUtils;
 import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.qe.OriginStatement;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
@@ -420,4 +428,25 @@ public class RuntimeFilterTest extends SSBTestBase {
         Assertions.assertEquals(0, ((AbstractPhysicalPlan) plan.child(0).child(1).child(0))
                 .getAppliedRuntimeFilters().size());
     }
+
+    @Test
+    public void testRuntimeFilterBlockByRecCte() {
+        String sql = new StringBuilder().append("with recursive xx as (\n").append("  select\n")
+                .append("    c_custkey as c1\n").append("  from\n").append("    customer\n").append("  union\n")
+                .append("  select\n").append("    xx.c1 as c1\n").append("  from\n").append("    xx\n").append(")\n")
+                .append("select\n").append("    xx.c1\n").append("  from\n").append("    xx\n")
+                .append("    join lineorder on lineorder.lo_custkey = xx.c1").toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                ExplainCommand.ExplainLevel.OPTIMIZED_PLAN);
+        MemoTestUtils.initMemoAndValidState(planner.getCascadesContext());
+        new PhysicalPlanTranslator(new PlanTranslatorContext(planner.getCascadesContext()))
+                .translatePlan((PhysicalPlan) planner.getOptimizedPlan());
+        RuntimeFilterContext context = planner.getCascadesContext().getRuntimeFilterContext();
+        List<RuntimeFilter> filters = context.getNereidsRuntimeFilter();
+        Assertions.assertEquals(0, filters.size());
+    }
 }
diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/AnalyzeCTETest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/AnalyzeCTETest.java
index a91c0dd4712..1921ae9025d 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/AnalyzeCTETest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/AnalyzeCTETest.java
@@ -33,10 +33,14 @@ import org.apache.doris.nereids.rules.rewrite.InApplyToJoin;
 import org.apache.doris.nereids.rules.rewrite.PullUpProjectUnderApply;
 import org.apache.doris.nereids.rules.rewrite.UnCorrelatedApplyFilter;
 import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Nullable;
+import org.apache.doris.nereids.trees.plans.commands.ExplainCommand;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
 import org.apache.doris.nereids.util.MemoPatternMatchSupported;
 import org.apache.doris.nereids.util.MemoTestUtils;
 import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.qe.OriginStatement;
 import org.apache.doris.utframe.TestWithFeService;
 
 import com.google.common.collect.ImmutableList;
@@ -264,6 +268,92 @@ public class AnalyzeCTETest extends TestWithFeService implements MemoPatternMatc
                 );
     }
 
+    @Test
+    public void testRecCteOutputNullable() {
+        String sql = new StringBuilder()
+                .append("WITH RECURSIVE test_table AS (\n")
+                .append("    SELECT 1 UNION ALL\n")
+                .append("    SELECT 2 FROM test_table\n")
+                .append(")\n")
+                .append("SELECT * FROM test_table;")
+                .toString();
+        PlanChecker.from(connectContext)
+                .analyze(sql)
+                .matches(
+                        logicalRecursiveCte(
+                                logicalProject(
+                                        logicalOneRowRelation(
+                                        )
+                                ).when(project -> project.getProjects().get(0).child(0) instanceof Nullable),
+                                logicalRecursiveCteRecursiveChild(
+                                        logicalProject(
+                                                logicalProject(
+                                                        logicalCTEConsumer()
+                                                )
+                                        ).when(project -> project.getProjects().get(0).child(0) instanceof Nullable)
+                                )
+                        )
+                );
+    }
+
+    @Test
+    public void testRecCteWithoutRecKeyword() {
+        String sql = new StringBuilder()
+                .append("WITH RECURSIVE t1 AS (\n")
+                .append("    SELECT 1\n")
+                .append("UNION ALL\n")
+                .append("    SELECT 2 FROM t1\n")
+                .append("),\n").append("t2 AS (\n")
+                .append("    SELECT 3\n")
+                .append("UNION ALL\n")
+                .append("    SELECT 4 FROM t1, t2\n")
+                .append(")\n")
+                .append("SELECT * FROM t2;")
+                .toString();
+        PlanChecker.from(connectContext)
+                .analyze(sql)
+                .matches(
+                        logicalRecursiveCte(
+                                logicalProject(
+                                        logicalOneRowRelation(
+                                        )
+                                ),
+                                logicalRecursiveCteRecursiveChild(
+                                        logicalProject(
+                                                logicalProject(
+                                                        logicalJoin()
+                                                )
+                                        )
+                                )
+                        ).when(cte -> cte.getCteName().equals("t2"))
+                );
+    }
+
+    @Test
+    public void testRecCteMultipleUnion() {
+        String sql = new StringBuilder().append("with recursive t1 as (\n").append("    select\n")
+                .append("        1 as c1,\n").append("        1 as c2\n").append("),\n").append("t2 as (\n")
+                .append("    select\n").append("        2 as c1,\n").append("        2 as c2\n").append("),\n")
+                .append("xx as (\n").append("    select\n").append("        c1,\n").append("        c2\n")
+                .append("    from\n").append("        t1\n").append("    union\n").append("    select\n")
+                .append("        c1,\n").append("        c2\n").append("    from\n").append("        t2\n")
+                .append("    union\n").append("    select\n").append("        c1,\n").append("        c2\n")
+                .append("    from\n").append("        xx\n").append(")\n").append("select\n").append("    *\n")
+                .append("from\n").append("    xx;").toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                ExplainCommand.ExplainLevel.ANALYZED_PLAN);
+        MemoTestUtils.initMemoAndValidState(planner.getCascadesContext());
+        PlanChecker.from(planner.getCascadesContext()).matches(
+                logicalRecursiveCte(
+                        logicalProject(
+                                logicalUnion()),
+                        logicalRecursiveCteRecursiveChild()).when(cte -> cte.getCteName().equals("xx")));
+    }
+
 
     /* ********************************************************************************************
      * Test CTE Exceptions
@@ -333,4 +423,133 @@ public class AnalyzeCTETest extends TestWithFeService implements MemoPatternMatc
                 () -> PlanChecker.from(connectContext).analyze(sql), "Not throw expected exception.");
         Assertions.assertTrue(exception.getMessage().contains("Table [cte2] does not exist in database"));
     }
+
+    @Test
+    public void testRecCteWithoutRecKeywordException() {
+        String sql = new StringBuilder()
+                .append("WITH t1 AS (\n")
+                .append("    SELECT 1 UNION ALL\n")
+                .append("    SELECT 2 FROM t1\n")
+                .append(")\n")
+                .append("SELECT * FROM t1;")
+                .toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        AnalysisException exception = Assertions.assertThrows(AnalysisException.class,
+                () -> planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                        ExplainCommand.ExplainLevel.ANALYZED_PLAN), "Not throw expected exception.");
+        Assertions.assertTrue(exception.getMessage().contains("Table [t1] does not exist in database"));
+    }
+
+    @Test
+    public void testRecCteDatatypeException() {
+        String sql = new StringBuilder().append("WITH RECURSIVE t1 AS (\n").append("    SELECT 1 AS number\n")
+                .append("UNION ALL\n").append("    SELECT number + 1 FROM t1 WHERE number < 100\n").append(")\n")
+                .append("SELECT number FROM t1;").toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        AnalysisException exception = Assertions.assertThrows(AnalysisException.class,
+                () -> planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                        ExplainCommand.ExplainLevel.ANALYZED_PLAN),
+                "Not throw expected exception.");
+        Assertions.assertTrue(exception.getMessage().contains("please add cast manually to get expect datatype"));
+    }
+
+    @Test
+    public void testRecCteMultipleUnionException() {
+        String sql = new StringBuilder().append("with recursive t1 as (\n").append("    select\n")
+                .append("        1 as c1,\n").append("        1 as c2\n").append("),\n").append("t2 as (\n")
+                .append("    select\n").append("        2 as c1,\n").append("        2 as c2\n").append("),\n")
+                .append("xx as (\n").append("    select\n").append("        c1,\n").append("        c2\n")
+                .append("    from\n").append("        t1\n").append("    union\n").append("    select\n")
+                .append("        c1,\n").append("        c2\n").append("    from\n").append("        xx\n")
+                .append("    union\n").append("    select\n").append("        c1,\n").append("        c2\n")
+                .append("    from\n").append("        t2\n").append(")\n").append("select\n").append("    *\n")
+                .append("from\n").append("    xx").toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        AnalysisException exception = Assertions.assertThrows(AnalysisException.class,
+                () -> planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                        ExplainCommand.ExplainLevel.ANALYZED_PLAN),
+                "Not throw expected exception.");
+        Assertions.assertTrue(exception.getMessage()
+                .contains("recursive reference to query xx must not appear within its non-recursive term"));
+    }
+
+    @Test
+    public void testRecCteNoUnionException() {
+        String sql = new StringBuilder().append("with recursive t1 as (\n").append("    select 1 \n")
+                .append("        intersect\n").append("    select 2 from t1\n").append(")\n").append("select\n")
+                .append("    *\n").append("from\n").append("    t1").toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        AnalysisException exception = Assertions.assertThrows(AnalysisException.class,
+                () -> planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                        ExplainCommand.ExplainLevel.ANALYZED_PLAN),
+                "Not throw expected exception.");
+        Assertions.assertTrue(exception.getMessage().contains("recursive cte must be union"));
+    }
+
+    @Test
+    public void testRecCteAnchorException() {
+        String sql = new StringBuilder().append("with recursive t1 as (\n").append("    select 1 from t1\n")
+                .append("        union\n").append("    select 2 from t1\n").append(")\n").append("select\n")
+                .append("    *\n").append("from\n").append("    t1;").toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        AnalysisException exception = Assertions.assertThrows(AnalysisException.class,
+                () -> planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                        ExplainCommand.ExplainLevel.ANALYZED_PLAN),
+                "Not throw expected exception.");
+        Assertions.assertTrue(exception.getMessage()
+                .contains("recursive reference to query t1 must not appear within its non-recursive term"));
+    }
+
+    @Test
+    public void testRecCteMoreThanOnceException() {
+        String sql = new StringBuilder().append("with recursive t1 as (\n").append("    select 1\n")
+                .append("        union\n").append("    select 2 from t1 x, t1 y\n").append(")\n").append("select\n")
+                .append("    *\n").append("from\n").append("    t1").toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        AnalysisException exception = Assertions.assertThrows(AnalysisException.class,
+                () -> planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                        ExplainCommand.ExplainLevel.ANALYZED_PLAN),
+                "Not throw expected exception.");
+        Assertions.assertTrue(
+                exception.getMessage().contains("recursive reference to query t1 must not appear more than once"));
+    }
+
+    @Test
+    public void testRecCteInSubqueryException() {
+        String sql = new StringBuilder().append("with recursive t1 as (\n").append("    select\n")
+                .append("        1 as c1,\n").append("        1 as c2\n").append("),\n").append("xx as (\n")
+                .append("    select\n").append("        2 as c1,\n").append("        2 as c2\n").append("    from\n")
+                .append("        t1\n").append("    union\n").append("    select\n").append("        3 as c1,\n")
+                .append("        3 as c2\n").append("    from\n")
+                .append("        t1 where t1.c1 in (select c1 from xx)\n").append(")\n").append("select\n")
+                .append("    *\n").append("from\n").append("    xx").toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        AnalysisException exception = Assertions.assertThrows(AnalysisException.class,
+                () -> planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                        ExplainCommand.ExplainLevel.ANALYZED_PLAN),
+                "Not throw expected exception.");
+        Assertions.assertTrue(
+                exception.getMessage().contains("Table [xx] does not exist in database"));
+    }
 }
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
new file mode 100644
index 00000000000..0a0efc8b5db
--- /dev/null
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CTEInlineTest.java
@@ -0,0 +1,84 @@
+// 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.
+
+package org.apache.doris.nereids.rules.rewrite;
+
+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.plans.commands.ExplainCommand;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.util.MemoPatternMatchSupported;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.qe.OriginStatement;
+import org.apache.doris.utframe.TestWithFeService;
+
+import org.junit.jupiter.api.Test;
+
+public class CTEInlineTest extends TestWithFeService implements MemoPatternMatchSupported {
+    @Override
+    protected void runBeforeAll() throws Exception {
+        createDatabase("test");
+        connectContext.setDatabase("test");
+    }
+
+    @Test
+    public void recCteInline() {
+        String sql = new StringBuilder().append("with recursive t1 as (\n").append("    select\n")
+                .append("        1 as c1,\n").append("        1 as c2\n").append("),\n").append("t2 as (\n")
+                .append("    select\n").append("        2 as c1,\n").append("        2 as c2\n").append("),\n")
+                .append("t3 as (\n").append("    select\n").append("        3 as c1,\n").append("        3 as c2\n")
+                .append("),\n").append("xx as (\n").append("    select\n").append("        c1,\n")
+                .append("        c2\n").append("    from\n").append("        t1\n").append("    union\n")
+                .append("    select\n").append("        t2.c1,\n").append("        t2.c2\n").append("    from\n")
+                .append("        t2,\n").append("        xx\n").append("    where\n").append("        t2.c1 = xx.c1\n")
+                .append("),\n").append("yy as (\n").append("    select\n").append("        c1,\n")
+                .append("        c2\n").append("    from\n").append("        t3\n").append("    union\n")
+                .append("    select\n").append("        t3.c1,\n").append("        t3.c2\n").append("    from\n")
+                .append("        t3,\n").append("        yy,\n").append("        xx\n").append("    where\n")
+                .append("        t3.c1 = yy.c1\n").append("        and t3.c2 = xx.c1\n").append(")\n")
+                .append("select\n").append("    *\n").append("from\n").append("    yy y1,\n").append("    yy y2;")
+                .toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                ExplainCommand.ExplainLevel.REWRITTEN_PLAN);
+        MemoTestUtils.initMemoAndValidState(planner.getCascadesContext());
+        PlanChecker.from(planner.getCascadesContext()).matches(
+                logicalRecursiveCte(
+                        any(
+                        ),
+                        logicalRecursiveCteRecursiveChild(
+                                logicalProject(
+                                        logicalJoin(
+                                                any(),
+                                                logicalProject(
+                                                        logicalFilter(
+                                                                logicalRecursiveCte().when(cte -> cte.getCteName().equals("xx"))
+                                                        )
+                                                )
+                                        )
+                                )
+                        )
+                ).when(cte -> cte.getCteName().equals("yy"))
+        );
+    }
+}
diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ColumnPruningTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ColumnPruningTest.java
index 12e9a1ad381..353f0c13863 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ColumnPruningTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ColumnPruningTest.java
@@ -17,15 +17,23 @@
 
 package org.apache.doris.nereids.rules.rewrite;
 
+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.NamedExpression;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
 import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
 import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.commands.ExplainCommand;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.types.DoubleType;
 import org.apache.doris.nereids.types.TinyIntType;
 import org.apache.doris.nereids.util.MemoPatternMatchSupported;
+import org.apache.doris.nereids.util.MemoTestUtils;
 import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.qe.OriginStatement;
 import org.apache.doris.utframe.TestWithFeService;
 
 import com.google.common.collect.ImmutableList;
@@ -328,6 +336,28 @@ public class ColumnPruningTest extends TestWithFeService implements MemoPatternM
                 );
     }
 
+    @Test
+    public void pruneRecCte() {
+        String sql = new StringBuilder().append("WITH RECURSIVE t1(col1, col2, col3) AS (\n")
+                .append("        SELECT 1, 1, 1\n").append("    UNION ALL\n").append("        SELECT 2, 2, 2\n")
+                .append("        FROM student, t1\n").append("        WHERE t1.col1 = student.id\n").append("    )\n")
+                .append("SELECT col1\n").append("FROM t1\n").append("WHERE col2 = 2;").toString();
+        LogicalPlan unboundPlan = new NereidsParser().parseSingle(sql);
+        StatementContext statementContext = new StatementContext(connectContext,
+                new OriginStatement(sql, 0));
+        NereidsPlanner planner = new NereidsPlanner(statementContext);
+        planner.planWithLock(unboundPlan, PhysicalProperties.ANY,
+                ExplainCommand.ExplainLevel.REWRITTEN_PLAN);
+        MemoTestUtils.initMemoAndValidState(planner.getCascadesContext());
+        PlanChecker.from(planner.getCascadesContext()).matches(
+                logicalProject(
+                        logicalFilter(
+                                logicalRecursiveCte().when(cte -> cte.getOutput().size() == 3)
+                        )
+                ).when(project -> project.getOutputs().size() == 1)
+        );
+    }
+
     private List<String> getOutputQualifiedNames(LogicalProject<? extends Plan> p) {
         return getOutputQualifiedNames(p.getOutputs());
     }
diff --git a/regression-test/data/rec_cte_p0/rec_cte/rec_cte.out b/regression-test/data/rec_cte_p0/rec_cte/rec_cte.out
new file mode 100644
index 00000000000..ba843a71ee5
--- /dev/null
+++ b/regression-test/data/rec_cte_p0/rec_cte/rec_cte.out
@@ -0,0 +1,953 @@
+-- This file is automatically generated. You should know what you did if you want to edit this
+-- !sql --
+0.5403023058681398
+0.6542897904977791
+0.7013687736227565
+0.7221024250267077
+0.7314040424225098
+0.7356047404363474
+0.7375068905132428
+0.7383692041223232
+0.7387603198742113
+0.7389377567153445
+0.7390182624274122
+0.7390547907469174
+0.7390713652989449
+0.7390788859949921
+0.7390822985224024
+0.7390838469650002
+0.7390845495752126
+0.7390848683867142
+0.7390850130484203
+0.739085078689123
+0.7390851084737987
+0.7390851219886894
+0.7390851281211138
+0.7390851309037207
+0.7390851321663374
+0.7390851327392538
+0.7390851329992164
+0.7390851331171753
+0.7390851331706995
+0.7390851331949863
+0.7390851332060064
+0.7390851332110069
+0.7390851332132758
+0.7390851332143055
+0.7390851332147726
+0.7390851332149846
+0.7390851332150807
+0.7390851332151244
+0.7390851332151441
+0.7390851332151531
+0.7390851332151572
+0.7390851332151591
+0.7390851332151599
+0.7390851332151603
+0.7390851332151605
+0.7390851332151606
+0.7390851332151607
+0.7390851332151608
+0.7390851332151609
+0.7390851332151611
+0.7390851332151617
+0.7390851332151629
+0.7390851332151657
+0.7390851332151718
+0.7390851332151851
+0.7390851332152145
+0.7390851332152792
+0.739085133215422
+0.7390851332157367
+0.7390851332164302
+0.7390851332179587
+0.7390851332213271
+0.7390851332287504
+0.7390851332451103
+0.7390851332811648
+0.7390851333606233
+0.7390851335357372
+0.7390851339216605
+0.7390851347721744
+0.7390851366465718
+0.7390851407774467
+0.7390851498812394
+0.7390851699445544
+0.7390852141609171
+0.7390853116067619
+0.7390855263619245
+0.7390859996481299
+0.7390870426953322
+0.7390893414033927
+0.7390944073790913
+0.7391055719265363
+0.7391301765296711
+0.7391843997714936
+0.7393038923969059
+0.7395672022122561
+0.7401473355678757
+0.7414250866101092
+0.7442373549005569
+0.7504177617637605
+0.7639596829006542
+0.7934803587425656
+0.8575532158463934
+1
+
+-- !sql --
+55
+
+-- !sql --
+1	3
+1	5
+1	8
+2	4
+2	5
+2	10
+2	19
+3	1
+3	5
+3	8
+3	10
+3	24
+5	3
+5	4
+5	8
+5	15
+6	3
+6	4
+6	7
+7	4
+8	1
+9	4
+
+-- !sql --
+1	3
+1	5
+2	4
+2	5
+2	10
+3	1
+3	5
+3	8
+3	10
+5	3
+5	4
+5	8
+5	10
+6	3
+6	4
+6	5
+7	4
+8	1
+8	8
+9	4
+11	1
+12	3
+29	4
+
+-- !sql --
+1	3
+1	5
+2	4
+2	5
+2	10
+3	1
+3	5
+3	8
+3	10
+5	3
+5	4
+5	8
+6	3
+6	4
+7	4
+7	10
+8	1
+8	10
+9	4
+9	5
+9	10
+10	5
+10	8
+10	10
+11	5
+11	8
+11	10
+12	5
+12	8
+12	10
+13	1
+13	5
+13	8
+13	10
+14	1
+14	5
+14	8
+14	10
+15	1
+15	3
+15	5
+15	8
+15	10
+16	1
+16	3
+16	5
+16	8
+16	10
+17	1
+17	3
+17	5
+17	8
+17	10
+18	1
+18	3
+18	5
+18	8
+18	10
+19	1
+19	3
+19	5
+19	8
+19	10
+20	1
+20	3
+20	5
+20	8
+20	10
+21	1
+21	3
+21	5
+21	8
+21	10
+22	1
+22	3
+22	5
+22	8
+22	10
+23	1
+23	3
+23	5
+23	8
+23	10
+24	1
+24	3
+24	5
+24	8
+24	10
+25	1
+25	3
+25	5
+25	8
+25	10
+26	1
+26	3
+26	5
+26	8
+26	10
+27	1
+27	3
+27	5
+27	8
+27	10
+28	1
+28	3
+28	5
+28	8
+28	10
+29	1
+29	3
+29	5
+29	8
+29	10
+30	1
+30	3
+30	5
+30	8
+30	10
+31	1
+31	3
+31	5
+31	8
+31	10
+32	1
+32	3
+32	5
+32	8
+32	10
+33	1
+33	3
+33	5
+33	8
+33	10
+34	1
+34	3
+34	4
+34	5
+34	8
+34	10
+35	1
+35	3
+35	4
+35	5
+35	8
+35	10
+36	1
+36	3
+36	4
+36	5
+36	8
+36	10
+37	1
+37	3
+37	4
+37	5
+37	8
+37	10
+38	1
+38	3
+38	4
+38	5
+38	8
+38	10
+39	1
+39	3
+39	4
+39	5
+39	8
+39	10
+40	1
+40	3
+40	4
+40	5
+40	8
+40	10
+41	1
+41	3
+41	4
+41	5
+41	8
+41	10
+42	1
+42	3
+42	4
+42	5
+42	8
+42	10
+43	1
+43	3
+43	4
+43	5
+43	8
+43	10
+44	1
+44	3
+44	4
+44	5
+44	8
+44	10
+45	1
+45	3
+45	4
+45	5
+45	8
+45	10
+46	1
+46	3
+46	4
+46	5
+46	8
+46	10
+47	1
+47	3
+47	4
+47	5
+47	8
+47	10
+48	1
+48	3
+48	4
+48	5
+48	8
+48	10
+49	1
+49	3
+49	4
+49	5
+49	8
+49	10
+50	1
+50	3
+50	4
+50	5
+50	8
+50	10
+51	1
+51	3
+51	4
+51	5
+51	8
+51	10
+52	1
+52	3
+52	4
+52	5
+52	8
+52	10
+53	1
+53	3
+53	4
+53	5
+53	8
+53	10
+54	1
+54	3
+54	4
+54	5
+54	8
+54	10
+55	1
+55	3
+55	4
+55	5
+55	8
+55	10
+56	1
+56	3
+56	4
+56	5
+56	8
+56	10
+57	1
+57	3
+57	4
+57	5
+57	8
+57	10
+58	1
+58	3
+58	4
+58	5
+58	8
+58	10
+59	1
+59	3
+59	4
+59	5
+59	8
+59	10
+60	1
+60	3
+60	4
+60	5
+60	8
+60	10
+61	1
+61	3
+61	4
+61	5
+61	8
+61	10
+62	1
+62	3
+62	4
+62	5
+62	8
+62	10
+63	1
+63	3
+63	4
+63	5
+63	8
+63	10
+64	1
+64	3
+64	4
+64	5
+64	8
+64	10
+65	1
+65	3
+65	4
+65	5
+65	8
+65	10
+66	1
+66	3
+66	4
+66	5
+66	8
+66	10
+67	1
+67	3
+67	4
+67	5
+67	8
+67	10
+68	1
+68	3
+68	4
+68	5
+68	8
+68	10
+69	1
+69	3
+69	4
+69	5
+69	8
+69	10
+70	1
+70	3
+70	4
+70	5
+70	8
+70	10
+71	1
+71	3
+71	4
+71	5
+71	8
+71	10
+72	1
+72	3
+72	4
+72	5
+72	8
+72	10
+73	1
+73	3
+73	4
+73	5
+73	8
+73	10
+74	1
+74	3
+74	4
+74	5
+74	8
+74	10
+75	1
+75	3
+75	4
+75	5
+75	8
+75	10
+76	1
+76	3
+76	4
+76	5
+76	8
+76	10
+77	1
+77	3
+77	4
+77	5
+77	8
+77	10
+78	1
+78	3
+78	4
+78	5
+78	8
+78	10
+79	1
+79	3
+79	4
+79	5
+79	8
+79	10
+80	1
+80	3
+80	4
+80	5
+80	8
+80	10
+81	1
+81	3
+81	4
+81	5
+81	8
+81	10
+82	1
+82	3
+82	4
+82	5
+82	8
+82	10
+83	1
+83	3
+83	4
+83	5
+83	8
+83	10
+84	1
+84	3
+84	4
+84	5
+84	8
+84	10
+85	1
+85	3
+85	4
+85	5
+85	8
+85	10
+86	1
+86	3
+86	4
+86	5
+86	8
+86	10
+87	1
+87	3
+87	4
+87	5
+87	8
+87	10
+88	1
+88	3
+88	4
+88	5
+88	8
+88	10
+89	1
+89	3
+89	4
+89	5
+89	8
+89	10
+90	1
+90	3
+90	4
+90	5
+90	8
+90	10
+91	1
+91	3
+91	4
+91	5
+91	8
+91	10
+92	1
+92	3
+92	4
+92	5
+92	8
+92	10
+93	1
+93	3
+93	4
+93	5
+93	8
+93	10
+94	1
+94	3
+94	4
+94	5
+94	8
+94	10
+95	1
+95	3
+95	4
+95	5
+95	8
+95	10
+96	1
+96	3
+96	4
+96	5
+96	8
+96	10
+97	1
+97	3
+97	4
+97	5
+97	8
+97	10
+98	1
+98	3
+98	4
+98	5
+98	8
+98	10
+99	1
+99	3
+99	4
+99	5
+99	8
+99	10
+100	1
+100	3
+100	4
+100	5
+100	8
+100	10
+
+-- !sql --
+1	3
+1	5
+1	8
+2	\N
+2	4
+2	5
+2	9
+2	10
+2	15
+2	19
+2	28
+2	34
+2	43
+2	71
+2	77
+2	105
+2	176
+2	182
+2	253
+2	429
+2	435
+2	611
+2	1040
+2	1046
+2	1475
+2	2515
+2	2521
+2	3561
+2	6076
+2	6082
+2	8597
+2	14673
+2	14679
+2	20755
+2	35428
+2	35434
+2	50107
+2	85535
+2	85541
+2	120969
+2	206504
+2	206510
+2	292045
+2	498549
+2	498555
+2	705059
+2	1203608
+2	1203614
+2	1702163
+2	2905771
+2	2905777
+2	4109385
+2	7015156
+2	7015162
+2	9920933
+2	16936089
+2	16936095
+2	23951251
+2	40887340
+2	40887346
+2	57823435
+2	98710775
+2	98710781
+2	139598121
+2	238308896
+2	238308902
+2	337019677
+2	575328573
+2	575328579
+2	813637475
+2	1388966048
+2	1388966054
+2	1964294627
+3	\N
+3	1
+3	5
+3	6
+3	8
+3	10
+3	14
+3	18
+3	20
+3	23
+3	41
+3	43
+3	55
+3	63
+3	96
+3	118
+3	139
+3	181
+3	235
+3	320
+3	353
+3	501
+3	588
+3	854
+3	908
+3	1355
+3	1496
+3	2263
+3	2350
+3	3618
+3	3846
+3	5968
+3	6109
+3	9586
+3	9955
+3	15695
+3	15923
+3	25281
+3	25878
+3	41204
+3	41573
+3	66485
+3	67451
+3	108058
+3	108655
+3	174543
+3	176106
+3	283198
+3	284164
+3	457741
+3	460270
+3	741905
+3	743468
+3	1199646
+3	1203738
+3	1943114
+3	1945643
+3	3142760
+3	3149381
+3	5088403
+3	5092495
+3	8231163
+3	8241876
+3	13323658
+3	13330279
+3	21554821
+3	21572155
+3	34885100
+3	34895813
+3	56439921
+3	56467968
+3	91335734
+3	91353068
+3	147775655
+3	147821036
+3	239128723
+3	239156770
+3	386904378
+3	386977806
+3	626061148
+3	626106529
+3	1012965526
+3	1013084335
+3	1639072055
+3	1639145483
+5	\N
+5	3
+5	4
+5	7
+5	8
+5	12
+5	15
+5	22
+5	27
+5	34
+5	56
+5	61
+5	83
+5	139
+5	144
+5	200
+5	339
+5	344
+5	483
+5	822
+5	827
+5	1166
+5	1988
+5	1993
+5	2815
+5	4803
+5	4808
+5	6796
+5	11599
+5	11604
+5	16407
+5	28006
+5	28011
+5	39610
+5	67616
+5	67621
+5	95627
+5	163243
+5	163248
+5	230864
+5	394107
+5	394112
+5	557355
+5	951462
+5	951467
+5	1345574
+5	2297036
+5	2297041
+5	3248503
+5	5545539
+5	5545544
+5	7842580
+5	13388119
+5	13388124
+5	18933663
+5	32321782
+5	32321787
+5	45709906
+5	78031688
+5	78031693
+5	110353475
+5	188385163
+5	188385168
+5	266416856
+5	454802019
+5	454802024
+5	643187187
+5	1097989206
+5	1097989211
+5	1552791230
+6	3
+6	4
+6	7
+7	4
+8	1
+9	4
+
+-- !sql --
+1	2
+
+-- !sql --
+1	2
+3	4
+
+-- !sql --
+1	2
+3	4
+11	22
+33	44
+
+-- !sql --
+1	2
+3	4
+11	22
+
+-- !sql --
+1	22
+3	22
+11	22
+
+-- !sql --
+1	2
+2	3
+
+-- !sql --
+1	2
+3	4
+11	22
+
diff --git a/regression-test/data/rec_cte_p0/rec_cte_from_ck_doc/rec_cte_from_ck_doc.out b/regression-test/data/rec_cte_p0/rec_cte_from_ck_doc/rec_cte_from_ck_doc.out
new file mode 100644
index 00000000000..3a055b01acc
--- /dev/null
+++ b/regression-test/data/rec_cte_p0/rec_cte_from_ck_doc/rec_cte_from_ck_doc.out
@@ -0,0 +1,29 @@
+-- This file is automatically generated. You should know what you did if you want to edit this
+-- !q1 --
+5050
+
+-- !q2 --
+0	\N	ROOT
+1	0	Child_1
+2	0	Child_2
+3	1	Child_1_1
+
+-- !q3 --
+0	\N	ROOT	[0]
+1	0	Child_1	[0, 1]
+3	1	Child_1_1	[0, 1, 3]
+2	0	Child_2	[0, 2]
+
+-- !q4 --
+0	\N	ROOT	[0]	0
+1	0	Child_1	[0, 1]	1
+2	0	Child_2	[0, 2]	1
+3	1	Child_1_1	[0, 1, 3]	2
+
+-- !q5 --
+1	2	1 -> 2
+1	3	1 -> 3
+1	4	1 -> 4
+2	3	2 -> 3
+4	5	4 -> 5
+
diff --git a/regression-test/data/rec_cte_p0/rec_cte_from_duckdb_doc/rec_cte_from_duckdb_doc.out b/regression-test/data/rec_cte_p0/rec_cte_from_duckdb_doc/rec_cte_from_duckdb_doc.out
new file mode 100644
index 00000000000..958b6848838
--- /dev/null
+++ b/regression-test/data/rec_cte_p0/rec_cte_from_duckdb_doc/rec_cte_from_duckdb_doc.out
@@ -0,0 +1,30 @@
+-- This file is automatically generated. You should know what you did if you want to edit this
+-- !q1 --
+0	0	1
+1	1	1
+2	1	2
+3	2	3
+4	3	5
+5	5	8
+6	8	13
+7	13	21
+8	21	34
+9	34	55
+
+-- !q2 --
+["Oasis", "Rock", "Music", "Art"]
+
+-- !q3 --
+1	3	[1, 3]
+1	5	[1, 5]
+1	5	[1, 3, 5]
+1	8	[1, 3, 8]
+1	10	[1, 3, 10]
+1	3	[1, 5, 3]
+1	4	[1, 5, 4]
+1	8	[1, 5, 8]
+1	4	[1, 3, 5, 4]
+1	8	[1, 3, 5, 8]
+1	8	[1, 5, 3, 8]
+1	10	[1, 5, 3, 10]
+
diff --git a/regression-test/data/rec_cte_p0/rec_cte_from_mysql_doc/rec_cte_from_mysql_doc.out b/regression-test/data/rec_cte_p0/rec_cte_from_mysql_doc/rec_cte_from_mysql_doc.out
new file mode 100644
index 00000000000..3bc4cf27785
--- /dev/null
+++ b/regression-test/data/rec_cte_p0/rec_cte_from_mysql_doc/rec_cte_from_mysql_doc.out
@@ -0,0 +1,42 @@
+-- This file is automatically generated. You should know what you did if you want to edit this
+-- !q1 --
+1	abc
+2	abcabc
+3	abcabcabcabc
+
+-- !q2 --
+1	1	-1
+2	-2	2
+3	4	-4
+4	-8	8
+5	16	-16
+
+-- !q4 --
+2017-01-03
+2017-01-04
+2017-01-05
+2017-01-06
+2017-01-07
+2017-01-08
+2017-01-09
+2017-01-10
+
+-- !q5 --
+2017-01-03	300
+2017-01-04	0
+2017-01-05	0
+2017-01-06	50
+2017-01-07	0
+2017-01-08	180
+2017-01-09	0
+2017-01-10	5
+
+-- !q6 --
+333	Yasmina	333
+198	John	333,198
+29	Pedro	333,198,29
+4610	Sarah	333,198,29,4610
+72	Pierre	333,198,29,72
+692	Tarek	333,692
+123	Adil	333,692,123
+
diff --git a/regression-test/suites/rec_cte_p0/rec_cte/rec_cte.groovy b/regression-test/suites/rec_cte_p0/rec_cte/rec_cte.groovy
new file mode 100644
index 00000000000..d456f6ba998
--- /dev/null
+++ b/regression-test/suites/rec_cte_p0/rec_cte/rec_cte.groovy
@@ -0,0 +1,271 @@
+// 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.
+
+import org.codehaus.groovy.runtime.IOGroovyMethods
+
+suite ("rec_cte") {
+    qt_sql """
+    WITH RECURSIVE test_table AS (
+    SELECT
+        cast(1.0 as double) AS number
+    UNION
+    SELECT
+        cos(number)
+    FROM
+        test_table
+    )
+    SELECT
+        number
+    FROM
+        test_table order by number;
+    """
+
+    qt_sql """
+    WITH RECURSIVE test_table AS (
+        SELECT cast(10 as int) AS number
+    UNION ALL
+        SELECT cast(number - 1 as int) FROM test_table WHERE number > 0
+    )
+    SELECT sum(number) FROM test_table;
+    """
+
+
+    sql "DROP TABLE IF EXISTS edge;"
+    sql """
+    CREATE TABLE edge
+    (
+        node1id int,
+        node2id int
+    ) DUPLICATE KEY (node1id)
+    DISTRIBUTED BY HASH(node1id) BUCKETS 1 PROPERTIES ('replication_num' = '1');
+    """
+    sql """
+    INSERT INTO edge VALUES
+    (1, 3), (1, 5), (2, 4), (2, 5), (2, 10), (3, 1),
+    (3, 5), (3, 8), (3, 10), (5, 3), (5, 4), (5, 8),
+    (6, 3), (6, 4), (7, 4), (8, 1), (9, 4);
+    """
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            node1id AS k1,
+            node2id AS k2
+        FROM edge
+        UNION
+        SELECT
+            k1,
+            cast(sum(k2) as int)
+        FROM t1 GROUP BY k1
+    )
+    SELECT * FROM t1 ORDER BY 1,2;
+    """
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            node1id AS k1,
+            node2id AS k2
+        FROM edge
+        UNION
+        SELECT
+            cast(sum(k1) as int),
+            k2
+        FROM t1 GROUP BY k2
+    )
+    SELECT * FROM t1 ORDER BY 1,2;
+    """
+
+    test {
+        sql """
+            WITH RECURSIVE t1(k1, k2) AS (
+                SELECT
+                    node1id AS k1,
+                    node2id AS k2
+                FROM edge
+                UNION
+                SELECT
+                    cast(sum(k1 + 1) as int),
+                    k2
+                FROM t1 GROUP BY k2
+            )
+            SELECT * FROM t1 ORDER BY 1,2;
+        """
+        exception "ABORTED"
+    }  
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            node1id AS k1,
+            node2id AS k2
+        FROM edge
+        UNION
+        SELECT
+            cast(sum(k1 + 1) as int),
+            k2
+        FROM t1 WHERE k1 < 100 GROUP BY k2
+    )
+    SELECT * FROM t1 ORDER BY 1,2;
+    """
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            node1id AS k1,
+            node2id AS k2
+        FROM edge
+        UNION
+        SELECT
+            k1,
+            cast(sum(k2) OVER (PARTITION BY k1 ORDER BY k1 ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) as int)
+        FROM t1
+    )
+    SELECT * FROM t1 ORDER BY 1,2;
+    """
+
+    test {
+        sql """
+            WITH RECURSIVE t1(k1, k2) AS (
+                SELECT
+                    1,2
+                UNION ALL
+                SELECT
+                    1,2
+                FROM t1 GROUP BY k1
+            )
+            SELECT * FROM t1 ORDER BY 1,2;
+        """
+        exception "ABORTED"
+    }  
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            1,2
+        UNION
+        SELECT
+            1,2
+        FROM t1 GROUP BY k1
+    )
+    SELECT * FROM t1 ORDER BY 1,2;
+    """
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            1,2
+        UNION
+        SELECT
+            3,4
+        FROM t1 GROUP BY k1
+    )
+    SELECT * FROM t1 ORDER BY 1,2;
+    """
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            1,2
+        UNION
+        SELECT
+            3,4
+        FROM t1 GROUP BY k1
+    ),
+    t2(k1, k2) AS (
+        SELECT
+            11,22
+        UNION
+        SELECT
+            33,44
+        FROM t2 GROUP BY k1
+    )
+    SELECT * FROM t1 UNION  select * from t2 ORDER BY 1,2;
+    """
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            1,2
+        UNION
+        SELECT
+            3,4
+        FROM t1 GROUP BY k1
+    ),
+    t2(k1, k2) AS (
+        SELECT
+            11,22
+        UNION
+        SELECT t2.k1, t2.k2 FROM t1,t2
+    )
+    SELECT * FROM t1 UNION  select * from t2 ORDER BY 1,2;
+    """
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            1,2
+        UNION
+        SELECT
+            3,4
+        FROM t1 GROUP BY k1
+    ),
+    t2(k1, k2) AS (
+        SELECT
+            11,22
+        UNION
+        SELECT t1.k1, t2.k2 FROM t1,t2
+    )
+    select * from t2 ORDER BY 1,2;
+    """
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            1,2
+        UNION
+        SELECT
+            3,4
+        FROM t1 GROUP BY k1
+    ),
+    t2(k1, k2) AS (
+        SELECT
+            2,3
+        UNION
+        SELECT least(t1.k1,t2.k1), least(t1.k2,t2.k2) FROM t1,t2
+    )
+    select * from t2 ORDER BY 1,2;
+    """
+
+    qt_sql """
+    WITH RECURSIVE t1(k1, k2) AS (
+        SELECT
+            1,2
+        UNION
+        SELECT
+            3,4
+        FROM t1 GROUP BY k1
+    ),
+    t2(k1, k2) AS (
+        SELECT
+            11,22
+        UNION
+        SELECT t1.k1, t1.k2 FROM t1
+    )
+    SELECT * FROM t1 UNION  select * from t2 ORDER BY 1,2;
+    """
+}
diff --git a/regression-test/suites/rec_cte_p0/rec_cte_from_ck_doc/rec_cte_from_ck_doc.groovy b/regression-test/suites/rec_cte_p0/rec_cte_from_ck_doc/rec_cte_from_ck_doc.groovy
new file mode 100644
index 00000000000..a01ab03db34
--- /dev/null
+++ b/regression-test/suites/rec_cte_p0/rec_cte_from_ck_doc/rec_cte_from_ck_doc.groovy
@@ -0,0 +1,144 @@
+// 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.
+
+import org.codehaus.groovy.runtime.IOGroovyMethods
+
+// https://clickhouse.com/docs/sql-reference/statements/select/with
+suite ("rec_cte_from_ck_doc") {
+    qt_q1 """
+    WITH RECURSIVE test_table AS (
+        SELECT cast(1 as int) AS number
+    UNION ALL
+        SELECT cast(number + 1 as int) FROM test_table WHERE number < 100
+    )
+    SELECT sum(number) FROM test_table;
+    """
+
+    sql "DROP TABLE IF EXISTS tree;"
+    sql """
+        CREATE TABLE tree
+        (
+            id int,
+            parent_id int,
+            data varchar(100)
+        ) DUPLICATE KEY (id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1 PROPERTIES ('replication_num' = '1');
+    """
+    sql """INSERT INTO tree VALUES (0, NULL, 'ROOT'), (1, 0, 'Child_1'), (2, 0, 'Child_2'), (3, 1, 'Child_1_1');"""
+
+    qt_q2 """
+    WITH RECURSIVE search_tree AS (
+        SELECT id, parent_id, data
+        FROM tree t
+        WHERE t.id = 0
+    UNION ALL
+        SELECT t.id, t.parent_id, t.data
+        FROM tree t, search_tree st
+        WHERE t.parent_id = st.id
+    )
+    SELECT * FROM search_tree order BY id;
+    """
+
+    qt_q3 """
+    WITH RECURSIVE search_tree AS (
+        SELECT id, parent_id, data, array(t.id) AS path
+        FROM tree t
+        WHERE t.id = 0
+    UNION ALL
+        SELECT t.id, t.parent_id, t.data, array_concat(path, array(t.id))
+        FROM tree t, search_tree st
+        WHERE t.parent_id = st.id
+    )
+    SELECT * FROM search_tree ORDER BY path;
+    """
+
+    qt_q4 """
+    WITH RECURSIVE search_tree AS (
+        SELECT id, parent_id, data, array(t.id) AS path, cast(0 as int) AS depth
+        FROM tree t
+        WHERE t.id = 0
+    UNION ALL
+        SELECT t.id, t.parent_id, t.data, array_concat(path, array(t.id)), cast(depth + 1 as int)
+        FROM tree t, search_tree st
+        WHERE t.parent_id = st.id
+    )
+    SELECT * FROM search_tree ORDER BY depth, id;
+    """
+
+    sql "DROP TABLE IF EXISTS graph;"
+    sql """
+        CREATE TABLE graph
+        (
+            c_from int,
+            c_to int,
+            label varchar(100)
+        ) DUPLICATE KEY (c_from) DISTRIBUTED BY HASH(c_from) BUCKETS 1 PROPERTIES ('replication_num' = '1');
+    """
+    sql """INSERT INTO graph VALUES (1, 2, '1 -> 2'), (1, 3, '1 -> 3'), (2, 3, '2 -> 3'), (1, 4, '1 -> 4'), (4, 5, '4 -> 5');"""
+
+    qt_q5 """
+    WITH RECURSIVE search_graph AS (
+        SELECT c_from, c_to, label FROM graph g
+        UNION ALL
+        SELECT g.c_from, g.c_to, g.label
+        FROM graph g, search_graph sg
+        WHERE g.c_from = sg.c_to
+    )
+    SELECT DISTINCT * FROM search_graph ORDER BY c_from, c_to;
+    """
+
+    sql "INSERT INTO graph VALUES (5, 1, '5 -> 1');"
+    test {
+        sql """
+            WITH RECURSIVE search_graph AS (
+                SELECT c_from, c_to, label FROM graph g
+                UNION ALL
+                SELECT g.c_from, g.c_to, g.label
+                FROM graph g, search_graph sg
+                WHERE g.c_from = sg.c_to
+            )
+            SELECT DISTINCT * FROM search_graph ORDER BY c_from, c_to;
+        """
+        exception "ABORTED"
+    }
+
+    // test global rf
+    sql "set enable_runtime_filter_prune = false;"
+    test {
+        sql """
+        WITH RECURSIVE search_graph AS (
+            SELECT c_from, c_to, label FROM graph g
+            UNION ALL
+            SELECT g.c_from, g.c_to, g.label
+            FROM graph g join [shuffle] search_graph sg
+            on g.c_from = sg.c_to
+        )
+        SELECT DISTINCT * FROM search_graph ORDER BY c_from, c_to;
+        """
+        exception "ABORTED"
+    }
+
+    // do not support use limit to stop recursion now
+    //qt_q6 """
+    //WITH RECURSIVE test_table AS (
+    //    SELECT cast(1 as int) AS number
+    //UNION ALL
+    //    SELECT cast(number + 1 as int) FROM test_table
+    //)
+    //SELECT sum(number) FROM test_table LIMIT 100;
+    //"""
+}
diff --git a/regression-test/suites/rec_cte_p0/rec_cte_from_duckdb_doc/rec_cte_from_duckdb_doc.groovy b/regression-test/suites/rec_cte_p0/rec_cte_from_duckdb_doc/rec_cte_from_duckdb_doc.groovy
new file mode 100644
index 00000000000..62d70558163
--- /dev/null
+++ b/regression-test/suites/rec_cte_p0/rec_cte_from_duckdb_doc/rec_cte_from_duckdb_doc.groovy
@@ -0,0 +1,185 @@
+// 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.
+
+import org.codehaus.groovy.runtime.IOGroovyMethods
+
+// https://duckdb.org/docs/stable/sql/query_syntax/with#recursive-ctes
+suite ("rec_cte_from_duckdb_doc") {
+    qt_q1 """
+    WITH RECURSIVE FibonacciNumbers (
+        RecursionDepth,
+        FibonacciNumber,
+        NextNumber
+    ) AS (
+        -- Base case
+        SELECT
+            cast(0 as int) AS RecursionDepth,
+            cast(0 as int) AS FibonacciNumber,
+            cast(1 as int) AS NextNumber
+        UNION
+        ALL -- Recursive step
+        SELECT
+            cast((fib.RecursionDepth + 1) as int) AS RecursionDepth,
+            fib.NextNumber AS FibonacciNumber,
+            cast((fib.FibonacciNumber + fib.NextNumber) as int) AS NextNumber
+        FROM
+            FibonacciNumbers fib
+        WHERE
+            cast((fib.RecursionDepth + 1) as int) < 10
+    )
+    SELECT
+        *
+    FROM
+        FibonacciNumbers fn ORDER BY fn.RecursionDepth;
+    """
+
+    sql "DROP TABLE IF EXISTS tag;"
+    sql """
+        CREATE TABLE tag
+        (
+            id int,
+            name varchar(100),
+            subclassof int
+        ) DUPLICATE KEY (id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1 PROPERTIES ('replication_num' = '1');
+    """
+    sql """INSERT INTO tag VALUES
+    (1, 'U2',     5),
+    (2, 'Blur',   5),
+    (3, 'Oasis',  5),
+    (4, '2Pac',   6),
+    (5, 'Rock',   7),
+    (6, 'Rap',    7),
+    (7, 'Music',  9),
+    (8, 'Movies', 9),
+    (9, 'Art', NULL);"""
+
+    qt_q2 """
+    WITH RECURSIVE tag_hierarchy(id, source, path) AS (
+            SELECT id, name, array(name) AS path
+            FROM tag
+            WHERE subclassof IS NULL
+        UNION ALL
+            SELECT tag.id, tag.name, array_concat(array(tag.name), tag_hierarchy.path)
+            FROM tag, tag_hierarchy
+            WHERE tag.subclassof = tag_hierarchy.id
+        )
+    SELECT path
+    FROM tag_hierarchy
+    WHERE source = 'Oasis';
+    """
+
+    sql "DROP TABLE IF EXISTS edge;"
+    sql """
+    CREATE TABLE edge
+    (
+        node1id int,
+        node2id int
+    ) DUPLICATE KEY (node1id)
+    DISTRIBUTED BY HASH(node1id) BUCKETS 1 PROPERTIES ('replication_num' = '1');
+    """
+    sql """
+    INSERT INTO edge VALUES
+    (1, 3), (1, 5), (2, 4), (2, 5), (2, 10), (3, 1),
+    (3, 5), (3, 8), (3, 10), (5, 3), (5, 4), (5, 8),
+    (6, 3), (6, 4), (7, 4), (8, 1), (9, 4);
+    """
+
+    qt_q3 """
+    WITH RECURSIVE paths(startNode, endNode, path) AS (
+            SELECT -- Define the path as the first edge of the traversal
+                node1id AS startNode,
+                node2id AS endNode,
+                array_concat(array(node1id), array(node2id)) AS path
+            FROM edge
+            WHERE node1id = 1
+            UNION ALL
+            SELECT -- Concatenate new edge to the path
+                paths.startNode AS startNode,
+                node2id AS endNode,
+                array_concat(path, array(node2id)) AS path
+            FROM paths
+            JOIN edge ON paths.endNode = node1id
+            -- Prevent adding a repeated node to the path.
+            -- This ensures that no cycles occur.
+            WHERE array_contains(paths.path, node2id) = false
+        )
+    SELECT startNode, endNode, path
+    FROM paths
+    ORDER BY array_size(path), path;
+    """
+
+    // do not support subquery containing recursive cte
+    //qt_q4 """
+    //WITH RECURSIVE paths(startNode, endNode, path) AS (
+    //        SELECT -- Define the path as the first edge of the traversal
+    //            node1id AS startNode,
+    //            node2id AS endNode,
+    //            array_concat(array(node1id), array(node2id)) AS path
+    //        FROM edge
+    //        WHERE startNode = 1
+    //        UNION ALL
+    //        SELECT -- Concatenate new edge to the path
+    //            paths.startNode AS startNode,
+    //            node2id AS endNode,
+    //            array_concat(path, array(node2id)) AS path
+    //        FROM paths
+    //        JOIN edge ON paths.endNode = node1id
+    //        -- Prevent adding a node that was visited previously by any path.
+    //        -- This ensures that (1) no cycles occur and (2) only nodes that
+    //        -- were not visited by previous (shorter) paths are added to a path.
+    //        WHERE NOT EXISTS (
+    //            SELECT 1 FROM paths previous_paths
+    //                WHERE array_contains(previous_paths.path, node2id)
+    //            )
+    //    )
+    //SELECT startNode, endNode, path
+    //FROM paths
+    //ORDER BY array_size(path), path;
+    //"""
+
+    //qt_q5 """
+    //WITH RECURSIVE paths(startNode, endNode, path, endReached) AS (
+    //SELECT -- Define the path as the first edge of the traversal
+    //        node1id AS startNode,
+    //        node2id AS endNode,
+    //        array_concat(array(node1id), array(node2id)) AS path,
+    //        (node2id = 8) AS endReached
+    //    FROM edge
+    //    WHERE startNode = 1
+    //UNION ALL
+    //SELECT -- Concatenate new edge to the path
+    //        paths.startNode AS startNode,
+    //        node2id AS endNode,
+    //        array_concat(path, array(node2id)) AS path,
+    //        max(CASE WHEN node2id = 8 THEN 1 ELSE 0 END)
+    //            OVER (ROWS BETWEEN UNBOUNDED PRECEDING
+    //                        AND UNBOUNDED FOLLOWING) AS endReached
+    //    FROM paths
+    //    JOIN edge ON paths.endNode = node1id
+    //    WHERE NOT EXISTS (
+    //            FROM paths previous_paths
+    //            WHERE array_contains(previous_paths.path, node2id)
+    //        )
+    //    AND paths.endReached = 0
+    //)
+    //SELECT startNode, endNode, path
+    //FROM paths
+    //WHERE endNode = 8
+    //ORDER BY array_size(path), path;
+    //"""
+}
diff --git a/regression-test/suites/rec_cte_p0/rec_cte_from_mysql_doc/rec_cte_from_mysql_doc.groovy b/regression-test/suites/rec_cte_p0/rec_cte_from_mysql_doc/rec_cte_from_mysql_doc.groovy
new file mode 100644
index 00000000000..cee07dc6612
--- /dev/null
+++ b/regression-test/suites/rec_cte_p0/rec_cte_from_mysql_doc/rec_cte_from_mysql_doc.groovy
@@ -0,0 +1,140 @@
+// 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.
+
+import org.codehaus.groovy.runtime.IOGroovyMethods
+
+// https://dev.mysql.com/doc/refman/8.4/en/with.html#common-table-expressions-recursive
+suite ("rec_cte_from_mysql_doc") {
+    qt_q1 """
+    WITH RECURSIVE cte AS
+    (
+    SELECT cast(1 as int) AS n, cast('abc' as varchar(65533)) AS str
+    UNION ALL
+    SELECT cast(n + 1 as int), cast(CONCAT(str, str) as varchar(65533)) FROM cte WHERE n < 3
+    )
+    SELECT * FROM cte order by n;
+    """
+
+    qt_q2 """
+    WITH RECURSIVE cte AS
+    (
+    SELECT cast(1 as int) AS n, cast(1 as int) AS p, cast(-1 as int) AS q
+    UNION ALL
+    SELECT cast(n + 1 as int), cast(q * 2 as int), cast(p * 2 as int) FROM cte WHERE n < 5
+    )
+    SELECT * FROM cte order by n;
+    """
+
+    test {
+        sql """
+            WITH RECURSIVE cte (n) AS
+            (
+            SELECT cast(1 as int)
+            UNION ALL
+            SELECT cast(n + 1 as int) FROM cte
+            )
+            SELECT n FROM cte order by n;
+        """
+        exception "ABORTED"
+    }
+    
+    // do not support use limit to stop recursion now
+    //qt_q3 """
+    //WITH RECURSIVE cte (n) AS
+    //(
+    //SELECT cast(1 as int)
+    //UNION ALL
+    //SELECT cast(n + 1 as int) FROM cte LIMIT 10000
+    //)
+    //SELECT n FROM cte order by n;
+    //"""
+
+    sql "DROP TABLE IF EXISTS sales;"
+    sql """
+        CREATE TABLE sales
+        (
+            c_date date,
+            c_price double
+        ) DUPLICATE KEY (c_date)
+        DISTRIBUTED BY HASH(c_date) BUCKETS 1 PROPERTIES ('replication_num' = '1');
+    """
+    sql """insert into sales values
+        ('2017-01-03', 100.0),
+        ('2017-01-03', 200.0),
+        ('2017-01-06', 50.0),
+        ('2017-01-08', 10.0),
+        ('2017-01-08', 20.0),
+        ('2017-01-08', 150.0),
+        ('2017-01-10', 5.0);"""
+
+    qt_q4 """
+        WITH RECURSIVE dates (c_date) AS
+        (
+        SELECT MIN(c_date) FROM sales
+        UNION ALL
+        SELECT c_date + INTERVAL 1 DAY FROM dates
+        WHERE c_date + INTERVAL 1 DAY <= (SELECT MAX(c_date) FROM sales)
+        )
+        SELECT * FROM dates order by 1;
+    """
+
+    qt_q5 """
+        WITH RECURSIVE dates (c_date) AS
+        (
+        SELECT MIN(c_date) FROM sales
+        UNION ALL
+        SELECT c_date + INTERVAL 1 DAY FROM dates
+        WHERE c_date + INTERVAL 1 DAY <= (SELECT MAX(c_date) FROM sales)
+        )
+        SELECT dates.c_date, COALESCE(SUM(c_price), 0) AS sum_price
+        FROM dates LEFT JOIN sales ON dates.c_date = sales.c_date
+        GROUP BY dates.c_date
+        ORDER BY dates.c_date;
+    """
+
+    sql "DROP TABLE IF EXISTS employees;"
+    sql """
+        CREATE TABLE employees (
+        id         INT NOT NULL,
+        name       VARCHAR(100) NOT NULL,
+        manager_id INT NULL
+        ) DISTRIBUTED BY HASH(id) BUCKETS 1 PROPERTIES ('replication_num' = '1');
+    """
+    sql """INSERT INTO employees VALUES
+    (333, "Yasmina", NULL),
+    (198, "John", 333),
+    (692, "Tarek", 333),
+    (29, "Pedro", 198),
+    (4610, "Sarah", 29),
+    (72, "Pierre", 29),
+    (123, "Adil", 692);
+    """
+
+    qt_q6 """
+        WITH RECURSIVE employee_paths (id, name, path) AS
+        (
+        SELECT id, name, CAST(id AS varchar(65533))
+            FROM employees
+            WHERE manager_id IS NULL
+        UNION ALL
+        SELECT e.id, e.name, cast(CONCAT(ep.path, ',', e.id) as varchar(65533))
+            FROM employee_paths AS ep JOIN employees AS e
+            ON ep.id = e.manager_id
+        )
+        SELECT * FROM employee_paths ORDER BY path;
+    """
+}
diff --git a/regression-test/suites/recursive_cte/column_attribute_null_test.groovy b/regression-test/suites/recursive_cte/column_attribute_null_test.groovy
new file mode 100644
index 00000000000..df10fbe7b33
--- /dev/null
+++ b/regression-test/suites/recursive_cte/column_attribute_null_test.groovy
@@ -0,0 +1,94 @@
+// 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("column_attribute_null_test", "rec_cte") {
+
+    String db = context.config.getDbNameByFile(context.file)
+    def prefix_str = "column_attribute_null_"
+    def tb_name = prefix_str + "recursive_cte_tb"
+    def temp_cte_name = prefix_str + "temp_cte_result"
+
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT not null,
+            Name VARCHAR(50)  not NULL,
+            ManagerID INT not NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """INSERT INTO ${tb_name} VALUES 
+        (100, 'Manager X', 0), 
+        (101, 'Alice', 100), 
+        (102, 'Bob', 100), 
+        (103, 'Charlie', 101), 
+        (104, 'David', 103), 
+        (105, 'Eve', 101);"""
+
+
+    sql """drop view if exists ${temp_cte_name}"""
+    sql """
+        CREATE VIEW ${temp_cte_name} AS 
+        WITH recursive SubordinateHierarchy (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            Level,
+            Comments
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(0 as bigint) AS Level,
+                CAST(NULL AS VARCHAR(100)) AS Comments 
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 100
+                
+            UNION ALL
+            
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(H.Level + 1 as bigint) AS Level,
+                cast(H.EmployeeName as VARCHAR(100))
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                SubordinateHierarchy AS H
+                ON E.ManagerID = H.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            SubordinateHierarchy
+        ORDER BY 
+            Level, EmployeeID;
+    """
+
+    def desc_res = sql """desc ${temp_cte_name}"""
+    logger.info("desc_res: " + desc_res)
+    for (int i = 0; i < desc_res.size(); i++) {
+        assertTrue(desc_res[i][4] == null)
+    }
+
+}
diff --git a/regression-test/suites/recursive_cte/complex_rec_cte_test.groovy b/regression-test/suites/recursive_cte/complex_rec_cte_test.groovy
new file mode 100644
index 00000000000..575ba6f80d1
--- /dev/null
+++ b/regression-test/suites/recursive_cte/complex_rec_cte_test.groovy
@@ -0,0 +1,478 @@
+// 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("complex_rec_cte_test", "rec_cte") {
+
+    sql """
+        CREATE TABLE IF NOT EXISTS departments (
+            id INT,
+            parent_id INT,
+            dept_name VARCHAR(50),
+            budget DECIMAL(18, 2)
+        )
+        DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 4
+        PROPERTIES ("replication_num" = "1");"""
+
+    sql """
+        CREATE TABLE IF NOT EXISTS connections (
+            source_id INT,
+            target_id INT,
+            status VARCHAR(20)
+        )
+        DUPLICATE KEY(source_id)
+        DISTRIBUTED BY HASH(source_id) BUCKETS 4
+        PROPERTIES ("replication_num" = "1");"""
+
+    sql """
+        CREATE TABLE IF NOT EXISTS nodes (
+            id INT,
+            type VARCHAR(20)
+        )
+        DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1");"""
+
+    sql """
+        CREATE TABLE IF NOT EXISTS raw_bom (
+            part_id INT,
+            sub_part_id INT,
+            qty DOUBLE
+        )
+        DUPLICATE KEY(part_id)
+        DISTRIBUTED BY HASH(part_id) BUCKETS 4
+        PROPERTIES ("replication_num" = "1");"""
+
+    sql """
+        INSERT INTO departments VALUES 
+        (1, NULL, '总部', 10000.00),
+        (10, 1, '研发部', 5000.00),
+        (11, 1, '市场部', 4000.00),
+        (101, 10, '后端开发', 2000.00),
+        (102, 10, '前端开发', 1500.00),
+        (111, 11, '线上推广', 2000.00);"""
+
+    sql """
+        INSERT INTO nodes VALUES (1, 'seed'), (2, 'normal'), (3, 'normal'), (4, 'normal');"""
+
+    sql """
+        INSERT INTO connections VALUES 
+        (1, 2, 'active'), (2, 3, 'active'), (3, 4, 'active'), 
+        (4, 1, 'active'), (2, 4, 'active'), (1, 3, 'inactive');"""
+
+    sql """
+        INSERT INTO raw_bom VALUES 
+        (1001, 2001, 2.0), (1001, 2002, 5.0),
+        (2001, 3001, 1.5), (2001, 3002, 3.0),
+        (2002, 4001, 1.0);"""
+
+    sql """
+        WITH RECURSIVE 
+        dept_tree(id, parent_id, dept_name, path, budget) AS (
+            SELECT 
+                id, 
+                parent_id, 
+                dept_name, 
+                CAST(dept_name AS CHAR(200)) AS path,
+                budget
+            FROM departments 
+            WHERE parent_id IS NULL
+            UNION ALL
+            SELECT 
+                t.id, 
+                t.parent_id, 
+                t.dept_name, 
+                CAST(CONCAT(c.path, '->', t.dept_name) AS CHAR(200)),
+                t.budget
+            FROM departments t 
+            JOIN dept_tree c ON t.parent_id = c.id
+            WHERE c.id < 1000 
+        ),
+        budget_summary(dept_id, total_budget, lvl) AS (
+            SELECT 
+                id, 
+                CAST(budget AS DECIMAL(18,2)), 
+                CAST(1 AS INT)
+            FROM dept_tree 
+            WHERE parent_id IS NULL
+            UNION ALL
+            SELECT 
+                curr.id, 
+                CAST(curr.budget + prev.total_budget AS DECIMAL(18,2)), 
+                CAST(prev.lvl + 1 AS INT)
+            FROM departments curr
+            JOIN budget_summary prev ON curr.parent_id = prev.dept_id
+            WHERE prev.lvl < 10
+        )
+        SELECT 
+            dept_id, 
+            MAX(total_budget) as max_b,
+            RANK() OVER(ORDER BY MAX(total_budget) DESC) as rk
+        FROM budget_summary
+        GROUP BY dept_id
+        HAVING max_b > 5000
+        ORDER BY rk
+        LIMIT 100;
+    """
+
+    sql """
+        WITH RECURSIVE path_finder(start_node, end_node, hops, visited_nodes) AS (
+        SELECT 
+            source_id, 
+            target_id, 
+            CAST(1 AS BIGINT), 
+            CAST(source_id AS CHAR(500))
+        FROM connections 
+        WHERE source_id IN (SELECT id FROM nodes WHERE type = 'seed')
+        
+        UNION ALL
+        
+        SELECT DISTINCT
+            p.start_node,
+            c.target_id,
+            CAST(p.hops + 1 AS BIGINT),
+            CAST(CONCAT(p.visited_nodes, ',', c.target_id) AS CHAR(500))
+        FROM path_finder p
+        JOIN (
+            SELECT a.source_id, a.target_id 
+            FROM connections a
+            WHERE a.status = 'active'
+        ) c ON p.end_node = c.source_id
+        WHERE p.hops < 5 
+          AND p.visited_nodes NOT LIKE CONCAT('%', CAST(c.target_id AS CHAR), '%')
+    )
+    SELECT 
+        start_node, 
+        end_node, 
+        MIN(hops) as min_hops 
+    FROM path_finder 
+    GROUP BY start_node, end_node 
+    ORDER BY min_hops ASC;"""
+
+    sql """
+        WITH RECURSIVE bom_recursive(part_id, sub_part_id, quantity, depth) AS (
+        SELECT 
+            part_id, 
+            sub_part_id, 
+            CAST(qty AS DOUBLE), 
+            CAST(0 AS INT)
+        FROM (
+            SELECT *, ROW_NUMBER() OVER(PARTITION BY part_id ORDER BY qty DESC) as rn 
+            FROM raw_bom
+        ) t 
+        WHERE rn = 1
+        
+        UNION ALL
+        
+        SELECT 
+            r.part_id, 
+            b.sub_part_id, 
+            CAST(r.quantity * b.qty AS DOUBLE), 
+            CAST(r.depth + 1 AS INT)
+        FROM bom_recursive r
+        JOIN raw_bom b ON r.sub_part_id = b.part_id
+        WHERE r.depth < 20
+    )
+    SELECT 
+        part_id, 
+        SUM(quantity) as total_qty,
+        COUNT(DISTINCT sub_part_id) as unique_subs
+    FROM bom_recursive
+    WHERE depth > 0
+    GROUP BY part_id
+    HAVING total_qty > 0
+    ORDER BY total_qty DESC;"""
+
+    sql """
+        WITH RECURSIVE outer_cte(id, val, lvl) AS (
+        SELECT 
+            cast(id as bigint), 
+            cast(val as bigint), 
+            CAST(0 AS bigint) 
+        FROM (WITH RECURSIVE inner_recursive(n) AS (
+                SELECT CAST(1 AS INT)
+                UNION ALL
+                SELECT CAST(n + 1 AS INT) FROM inner_recursive WHERE n < 3
+            )
+            SELECT n as id, n * 10 as val FROM inner_recursive) as  t
+        
+        UNION ALL
+        
+        SELECT 
+            CAST(o.id + 100 AS bigint), 
+            CAST(o.val + 10 AS bigint), 
+            CAST(o.lvl + 1 AS bigINT)
+        FROM outer_cte o
+        WHERE o.lvl < 2
+    )
+    SELECT * FROM outer_cte ORDER BY id;"""
+
+    sql """
+        WITH RECURSIVE multi_branch_cte(node_id, label, depth) AS (
+        -- branch1:no recursive Anchor 1
+        SELECT CAST(id AS INT), CAST('Type_A' AS CHAR(10)), CAST(0 AS INT)
+        FROM nodes WHERE type = 'seed'
+        UNION
+        -- branch 2:no recursive Anchor 2
+        SELECT CAST(id AS INT), CAST('Type_B' AS CHAR(10)), CAST(0 AS INT)
+        FROM nodes WHERE type = 'normal' AND id < 2
+        
+        UNION ALL
+        
+        -- branch3:recursive part
+        SELECT 
+            CAST(c.target_id AS INT), 
+            CAST(m.label AS CHAR(10)), 
+            CAST(m.depth + 1 AS INT)
+        FROM multi_branch_cte m
+        JOIN connections c ON m.node_id = c.source_id
+        WHERE m.depth < 5
+        )
+        SELECT 
+            label, 
+            depth, 
+            COUNT(DISTINCT node_id) as node_count
+        FROM multi_branch_cte
+        GROUP BY label, depth
+        HAVING node_count > 0
+        ORDER BY label, depth;
+        """
+
+    sql """
+        WITH RECURSIVE complex_logic(curr_id, total_score, step_path) AS (
+        SELECT 
+            id, 
+            CAST(budget AS DOUBLE), 
+            CAST(dept_name AS CHAR(200))
+        FROM (
+            SELECT *, ROW_NUMBER() OVER(ORDER BY budget DESC) as rank_id 
+            FROM departments
+        ) d WHERE rank_id = 1
+        
+        UNION ALL
+        
+        SELECT 
+            CAST(t.id AS INT), 
+            CAST(c.total_score + t.budget AS DOUBLE), 
+            CAST(CONCAT(c.step_path, '->', t.dept_name) AS CHAR(200))
+        FROM departments t
+        INNER JOIN complex_logic c ON t.parent_id = c.curr_id
+        WHERE c.total_score < 50000 
+          AND t.dept_name NOT IN (SELECT dept_name FROM departments WHERE budget < 100)
+        )
+        SELECT 
+            curr_id, 
+            total_score, 
+            step_path,
+            DENSE_RANK() OVER(ORDER BY total_score DESC) as score_rank
+        FROM complex_logic
+        WHERE step_path LIKE '%研发%'
+        ORDER BY score_rank
+        LIMIT 10;
+        """
+
+    sql """
+        WITH RECURSIVE 
+        num_seq(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM num_seq WHERE n < 5
+        ),
+        char_path(lvl, str) AS (
+            SELECT CAST(1 AS INT), CAST('A' AS CHAR(100))
+            UNION ALL
+            SELECT CAST(lvl + 1 AS INT), CAST(CONCAT(str, '->', 'B') AS CHAR(100)) 
+            FROM char_path WHERE lvl < 5
+        )
+        SELECT a.n, b.str 
+        FROM num_seq a 
+        JOIN char_path b ON a.n = b.lvl
+        ORDER BY a.n;
+        """
+
+    sql """
+        WITH RECURSIVE complex_join_cte(id, p_id, info, score) AS (
+            -- Anchor
+            SELECT id, parent_id, CAST(dept_name AS CHAR(100)), CAST(budget AS DOUBLE)
+            FROM departments WHERE parent_id IS NULL
+            UNION ALL
+            -- Recursive part:Join another table to logical judge
+            SELECT 
+                t.id, t.parent_id, 
+                CAST(CONCAT(c.info, '/', t.dept_name) AS CHAR(100)),
+                CAST(CASE WHEN t.budget > 1000 THEN c.score + t.budget ELSE c.score END AS DOUBLE)
+            FROM departments t
+            JOIN complex_join_cte c ON t.parent_id = c.id
+            LEFT JOIN nodes n ON t.id = n.id
+            WHERE c.score < 100000 AND (n.type IS NULL OR n.type != 'blocked')
+        )
+        SELECT * FROM complex_join_cte WHERE score > 5000;
+        """
+
+    sql """
+        WITH RECURSIVE sub_cte(id) AS (
+            SELECT CAST(101 AS INT)
+            UNION ALL
+            SELECT CAST(parent_id AS INT) FROM departments d 
+            JOIN sub_cte s ON d.id = s.id
+            WHERE d.parent_id IS NOT NULL
+        )
+        SELECT * FROM departments 
+        WHERE id IN (SELECT id FROM sub_cte)
+           OR parent_id IN (SELECT id FROM sub_cte);
+        """
+
+    sql """
+        WITH RECURSIVE grouped_cte(grp_key, total_val, depth) AS (
+            SELECT 
+                cast(parent_id as int), 
+                cast(SUM(budget) as double), 
+                CAST(0 AS INT)
+            FROM departments 
+            GROUP BY parent_id
+            UNION ALL
+            SELECT 
+                CAST(d.id AS INT), 
+                CAST(SUM(d.budget + g.total_val) AS DOUBLE), 
+                CAST(g.depth + 1 AS INT)
+            FROM departments d
+            JOIN grouped_cte g ON d.parent_id = g.grp_key
+            WHERE g.depth < 3
+            GROUP BY d.id, g.depth
+        )
+        SELECT grp_key, MAX(total_val) FROM grouped_cte GROUP BY grp_key;
+        """
+
+    test {
+        sql """
+        WITH RECURSIVE multi_union(id, tag) AS (
+            -- two Anchor branch
+            SELECT CAST(1 AS INT), CAST('start1' AS CHAR(50))
+            UNION
+            SELECT CAST(2 AS INT), CAST('start2' AS CHAR(50))
+            UNION ALL
+            -- two recursive branch 
+            SELECT CAST(id + 2 AS INT), CAST('step_a' AS CHAR(50)) FROM multi_union WHERE id < 5
+            UNION ALL
+            SELECT CAST(id + 4 AS INT), CAST('step_b' AS CHAR(50)) FROM multi_union WHERE id < 5
+        )
+        SELECT tag, COUNT(*) FROM multi_union GROUP BY tag;
+        """
+        exception """recursive reference to query multi_union must not appear within its non-recursive term"""
+    }
+
+    sql """
+        WITH RECURSIVE tree_data(id, p_id, val) AS (
+            SELECT id, parent_id, CAST(budget AS DOUBLE) FROM departments WHERE parent_id IS NULL
+            UNION ALL
+            SELECT d.id, d.parent_id, CAST(d.budget AS DOUBLE)
+            FROM departments d JOIN tree_data t ON d.parent_id = t.id
+        )
+        SELECT 
+            *,
+            SUM(val) OVER(PARTITION BY p_id ORDER BY val DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as running_total,
+            LAG(val) OVER(ORDER BY id) as prev_val
+        FROM tree_data;
+        """
+
+    sql """
+        WITH RECURSIVE complex_predicate_cte(id, category) AS (
+            SELECT id, CAST('ROOT' AS CHAR(20)) FROM departments WHERE parent_id IS NULL
+            UNION ALL
+            SELECT 
+                d.id,
+                CAST(
+                    CASE 
+                        WHEN d.budget > 5000 THEN 'High'
+                        WHEN d.id IN (SELECT source_id FROM connections) THEN 'Connected'
+                        ELSE 'Normal'
+                    END AS CHAR(20)
+                )
+            FROM departments d
+            JOIN complex_predicate_cte c ON d.parent_id = c.id
+            WHERE c.category != 'End'
+        )
+        SELECT category, COUNT(*) FROM complex_predicate_cte GROUP BY category;
+        """
+
+    sql """
+        WITH RECURSIVE
+          cte_1(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM cte_1 WHERE n < 5
+          ),
+          cte_2(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM cte_2 WHERE n < 5
+          ),
+          cte_3(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM cte_3 WHERE n < 5
+          ),
+          cte_4(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM cte_4 WHERE n < 5
+          ),
+          cte_5(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM cte_5 WHERE n < 5
+          ),
+          cte_6(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM cte_6 WHERE n < 5
+          ),
+          cte_7(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM cte_7 WHERE n < 5
+          ),
+          cte_8(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM cte_8 WHERE n < 5
+          ),
+          cte_9(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM cte_9 WHERE n < 5
+          ),
+          cte_10(n) AS (
+            SELECT CAST(1 AS INT)
+            UNION ALL
+            SELECT CAST(n + 1 AS INT) FROM cte_10 WHERE n < 5
+        )
+        SELECT * 
+        FROM cte_1
+        JOIN cte_2 ON cte_1.n = cte_2.n
+        JOIN cte_3 ON cte_1.n = cte_3.n
+        JOIN cte_4 ON cte_1.n = cte_4.n
+        JOIN cte_5 ON cte_1.n = cte_5.n
+        JOIN cte_6 ON cte_1.n = cte_6.n
+        JOIN cte_7 ON cte_1.n = cte_7.n
+        JOIN cte_8 ON cte_1.n = cte_8.n
+        JOIN cte_9 ON cte_1.n = cte_9.n
+        JOIN cte_10 ON cte_1.n = cte_10.n;
+        """
+
+
+}
diff --git a/regression-test/suites/recursive_cte/create_and_insert_select_test.groovy b/regression-test/suites/recursive_cte/create_and_insert_select_test.groovy
new file mode 100644
index 00000000000..410290dfd8d
--- /dev/null
+++ b/regression-test/suites/recursive_cte/create_and_insert_select_test.groovy
@@ -0,0 +1,140 @@
+// 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("create_and_insert_select_test", "rec_cte") {
+
+    String db = context.config.getDbNameByFile(context.file)
+    def prefix_str = "create_and_insert_select_"
+    def tb_name = prefix_str + "recursive_cte_tb"
+    def table_cte_name = prefix_str + "temp_cte_table"
+
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT not null,
+            Name VARCHAR(50)  not NULL,
+            ManagerID INT not NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """INSERT INTO ${tb_name} VALUES 
+        (100, 'Manager X', 0), 
+        (101, 'Alice', 100), 
+        (102, 'Bob', 100), 
+        (103, 'Charlie', 101), 
+        (104, 'David', 103), 
+        (105, 'Eve', 101);"""
+
+
+    sql """drop table if exists ${table_cte_name}"""
+    sql """
+        CREATE table ${table_cte_name} 
+        PROPERTIES ('replication_num' = '1') 
+        AS  
+        WITH recursive SubordinateHierarchy (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            Level,
+            Comments
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(0 as bigint) AS Level,
+                CAST(NULL AS VARCHAR(100)) AS Comments 
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 100
+                
+            UNION ALL
+            
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(H.Level + 1 as bigint) AS Level,
+                cast(H.EmployeeName as VARCHAR(100))
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                SubordinateHierarchy AS H
+                ON E.ManagerID = H.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            SubordinateHierarchy
+        ORDER BY 
+            Level, EmployeeID;
+    """
+
+    def tb_res1 = sql """select * from ${table_cte_name}"""
+    assertTrue(tb_res1.size() > 0)
+
+    sql """
+        insert into ${table_cte_name}  
+        WITH recursive SubordinateHierarchy (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            Level,
+            Comments
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(0 as bigint) AS Level,
+                CAST(NULL AS VARCHAR(100)) AS Comments 
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 100
+                
+            UNION ALL
+            
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(H.Level + 1 as bigint) AS Level,
+                cast(H.EmployeeName as VARCHAR(100))
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                SubordinateHierarchy AS H
+                ON E.ManagerID = H.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            SubordinateHierarchy
+        ORDER BY 
+            Level, EmployeeID;
+    """
+
+    def tb_res2 = sql """select * from ${table_cte_name}"""
+    assertTrue(tb_res1.size() * 2 == tb_res2.size())
+
+}
diff --git a/regression-test/suites/recursive_cte/mtmv_and_cte_test.groovy b/regression-test/suites/recursive_cte/mtmv_and_cte_test.groovy
new file mode 100644
index 00000000000..9baa20842cc
--- /dev/null
+++ b/regression-test/suites/recursive_cte/mtmv_and_cte_test.groovy
@@ -0,0 +1,88 @@
+// 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("mtmv_and_cte_test", "rec_cte") {
+
+    String db = context.config.getDbNameByFile(context.file)
+    def prefix_str = "mtmv_and_cte_"
+    def tb_name = prefix_str + "recursive_cte_tb"
+
+    sql """use ${db}"""
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT not null,
+            Name VARCHAR(50)  not NULL,
+            ManagerID INT not NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """INSERT INTO ${tb_name} VALUES 
+        (100, 'Manager X', 0), 
+        (101, 'Alice', 100), 
+        (102, 'Bob', 100), 
+        (103, 'Charlie', 101), 
+        (104, 'David', 103), 
+        (105, 'Eve', 101);"""
+
+    def sql_str = """WITH recursive SubordinateHierarchy (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            Level,
+            Comments
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(0 as bigint) AS Level,
+                CAST(NULL AS VARCHAR(100)) AS Comments 
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 100
+                
+            UNION ALL
+            
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(H.Level + 1 as bigint) AS Level,
+                cast(H.EmployeeName as VARCHAR(100))
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                SubordinateHierarchy AS H
+                ON E.ManagerID = H.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            SubordinateHierarchy
+        ORDER BY 
+            Level, EmployeeID;"""
+    def mtmv_name = prefix_str + "cte_mtmv"
+
+    async_create_mv(db, sql_str, mtmv_name)
+    mv_rewrite_fail(sql_str, mtmv_name)
+
+
+}
diff --git a/regression-test/suites/recursive_cte/mv_and_cte_test.groovy b/regression-test/suites/recursive_cte/mv_and_cte_test.groovy
new file mode 100644
index 00000000000..3f869ea7637
--- /dev/null
+++ b/regression-test/suites/recursive_cte/mv_and_cte_test.groovy
@@ -0,0 +1,88 @@
+// 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("mv_and_cte_test", "rec_cte") {
+
+    String db = context.config.getDbNameByFile(context.file)
+    def prefix_str = "mv_and_cte_"
+    def tb_name = prefix_str + "recursive_cte_tb"
+
+
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT not null,
+            Name VARCHAR(50)  not NULL,
+            ManagerID INT not NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """INSERT INTO ${tb_name} VALUES 
+        (100, 'Manager X', 0), 
+        (101, 'Alice', 100), 
+        (102, 'Bob', 100), 
+        (103, 'Charlie', 101), 
+        (104, 'David', 103), 
+        (105, 'Eve', 101);"""
+
+    def sql_str = """WITH recursive SubordinateHierarchy (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            Level,
+            Comments
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(0 as bigint) AS Level,
+                CAST(NULL AS VARCHAR(100)) AS Comments 
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 100
+                
+            UNION ALL
+            
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(H.Level + 1 as bigint) AS Level,
+                cast(H.EmployeeName as VARCHAR(100))
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                SubordinateHierarchy AS H
+                ON E.ManagerID = H.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            SubordinateHierarchy
+        ORDER BY 
+            Level, EmployeeID;"""
+    def mv_name = prefix_str + "cte_mv"
+    test {
+        sql "CREATE MATERIALIZED VIEW ${mv_name} AS " + sql_str
+        exception "LogicalCTEAnchor is not supported in sync materialized view"
+    }
+
+}
diff --git a/regression-test/suites/recursive_cte/no_data_test.groovy b/regression-test/suites/recursive_cte/no_data_test.groovy
new file mode 100644
index 00000000000..cabdd0cc562
--- /dev/null
+++ b/regression-test/suites/recursive_cte/no_data_test.groovy
@@ -0,0 +1,77 @@
+// 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("no_data_test", "rec_cte") {
+
+    String db = context.config.getDbNameByFile(context.file)
+    def prefix_str = "no_data_"
+    def tb_name = prefix_str + "recursive_cte_tb"
+
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT not null,
+            Name VARCHAR(50)  not NULL,
+            ManagerID INT not NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """
+        WITH recursive SubordinateHierarchy (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            Level,
+            Comments
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(0 as bigint) AS Level,
+                CAST(NULL AS VARCHAR(100)) AS Comments 
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 100
+                
+            UNION ALL
+            
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(H.Level + 1 as bigint) AS Level,
+                cast(H.EmployeeName as VARCHAR(100))
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                SubordinateHierarchy AS H
+                ON E.ManagerID = H.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            SubordinateHierarchy
+        ORDER BY 
+            Level, EmployeeID;
+    """
+
+
+}
diff --git a/regression-test/suites/recursive_cte/recursive_100_number_test.groovy b/regression-test/suites/recursive_cte/recursive_100_number_test.groovy
new file mode 100644
index 00000000000..28a81e15f36
--- /dev/null
+++ b/regression-test/suites/recursive_cte/recursive_100_number_test.groovy
@@ -0,0 +1,106 @@
+// 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("recursive_100_number_test", "rec_cte") {
+
+    def level = 100
+    sql """
+        WITH recursive RecursiveCounter (
+            N,
+            Depth
+        )
+        AS
+        (
+            SELECT 
+                cast(1 as bigint) AS N, 
+                cast(1 as bigint) AS Depth
+            UNION ALL
+            SELECT 
+                cast(N + 1 as bigint) AS N, 
+                cast(Depth + 1 as bigint) AS Depth 
+            FROM 
+                RecursiveCounter           
+            WHERE 
+                Depth < ${level}
+        )
+        SELECT 
+            N,
+            Depth
+        FROM 
+            RecursiveCounter
+        ORDER BY
+            Depth;"""
+
+    level = 101
+    test {
+        sql """WITH recursive RecursiveCounter (
+            N,
+            Depth
+        )
+        AS
+        (
+            SELECT 
+                cast(1 as bigint) AS N, 
+                cast(1 as bigint) AS Depth
+            UNION ALL
+            SELECT 
+                cast(N + 1 as bigint) AS N, 
+                cast(Depth + 1 as bigint) AS Depth 
+            FROM 
+                RecursiveCounter           
+            WHERE 
+                Depth < ${level}
+        )
+        SELECT 
+            N,
+            Depth
+        FROM 
+            RecursiveCounter
+        ORDER BY
+            Depth;"""
+        exception "reach cte_max_recursion_depth 100"
+    }
+
+    sql "set cte_max_recursion_depth=101"
+    sql """
+        WITH recursive RecursiveCounter (
+            N,
+            Depth
+        )
+        AS
+        (
+            SELECT 
+                cast(1 as bigint) AS N, 
+                cast(1 as bigint) AS Depth
+            UNION ALL
+            SELECT 
+                cast(N + 1 as bigint) AS N, 
+                cast(Depth + 1 as bigint) AS Depth 
+            FROM 
+                RecursiveCounter           
+            WHERE 
+                Depth < ${level}
+        )
+        SELECT 
+            N,
+            Depth
+        FROM 
+            RecursiveCounter
+        ORDER BY
+            Depth;"""
+
+}
diff --git a/regression-test/suites/recursive_cte/recursive_cte_exception_test.groovy b/regression-test/suites/recursive_cte/recursive_cte_exception_test.groovy
new file mode 100644
index 00000000000..dfa7ed5d462
--- /dev/null
+++ b/regression-test/suites/recursive_cte/recursive_cte_exception_test.groovy
@@ -0,0 +1,216 @@
+// 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("exception_test", "rec_cte") {
+    String db = context.config.getDbNameByFile(context.file)
+    def prefix_str = "exception_"
+    def tb_name = prefix_str + "recursive_cte_tb"
+
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT not null,
+            Name VARCHAR(50) NOT NULL,
+            ManagerID INT NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """INSERT INTO ${tb_name} VALUES 
+        (100, 'Manager X', 1), 
+        (101, 'Alice', 100), 
+        (102, 'Bob', 100), 
+        (103, 'Charlie', 101), 
+        (104, 'David', 103), 
+        (105, 'Eve', 101);"""
+
+    test {
+        sql """
+        WITH recursive SubordinateHierarchy (
+                EmployeeID,
+                EmployeeID,
+                EmployeeName,
+                ManagerID,
+                Level,
+                Comments
+        )
+        AS
+        (
+                SELECT
+                EmployeeID,
+                EmployeeID,
+                Name AS EmployeeName,
+                ManagerID,
+                cast(0 as bigint) AS Level,
+                CAST(NULL AS VARCHAR(100)) AS Comments
+        FROM
+        ${tb_name}
+        WHERE
+        EmployeeID = 100
+
+        UNION ALL
+
+        SELECT
+        E.EmployeeID,
+        E.EmployeeID,
+        E.Name AS EmployeeName,
+        E.ManagerID,
+        cast(H.Level + 1 as bigint) AS Level,
+        cast(H.EmployeeName as VARCHAR(100))
+        FROM
+        ${tb_name} AS E
+        INNER JOIN
+        SubordinateHierarchy AS H
+        ON E.ManagerID = H.EmployeeID
+        )
+        SELECT
+                *
+                FROM
+        SubordinateHierarchy
+        ORDER BY
+        Level, EmployeeID;
+        """
+        exception "is ambiguous"
+    }
+
+    // drop recursive part, become normal cte, no recursive cte
+    sql """
+        WITH recursive SubordinateHierarchy (
+        EmployeeID, 
+        EmployeeName, 
+        ManagerID, 
+        Level,
+        Comments
+    )
+    AS
+    (
+        SELECT 
+            EmployeeID, 
+            Name AS EmployeeName, 
+            ManagerID, 
+            cast(0 as bigint) AS Level,
+            CAST(NULL AS VARCHAR(100)) AS Comments 
+        FROM 
+            ${tb_name}
+        WHERE 
+            EmployeeID = 100
+    )
+    SELECT 
+        *
+    FROM 
+        SubordinateHierarchy
+    ORDER BY 
+        Level, EmployeeID;
+    """
+
+
+    test {
+        sql """
+            WITH recursive SubordinateHierarchy (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            Level,
+            Comments
+        )
+        AS
+        (
+
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(H.Level + 1 as bigint) AS Level,
+                cast(H.EmployeeName as VARCHAR(100))
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                SubordinateHierarchy AS H
+                ON E.ManagerID = H.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            SubordinateHierarchy
+        ORDER BY 
+            Level, EmployeeID;
+        """
+        exception "recursive cte must be union"
+    }
+
+    test {
+        sql """
+            WITH 
+            StartingEmployees AS (
+                SELECT 
+                    EmployeeID,
+                    Name,
+                    ManagerID
+                FROM 
+                    ${tb_name}
+                WHERE 
+                    Name LIKE 'A%' 
+            ),
+            SubordinateHierarchy (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            Level,
+            Comments
+            )
+            AS
+            (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(0 as bigint) AS Level,
+                CAST(NULL AS VARCHAR(100)) AS Comments 
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 100
+                
+            UNION ALL
+            
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(H.Level + 1 as bigint) AS Level,
+                cast(H.EmployeeName as VARCHAR(100))
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                SubordinateHierarchy AS H
+                ON E.ManagerID = H.EmployeeID
+            )
+            SELECT 
+                EmployeeID, 
+                EmployeeName, 
+                ManagerID, 
+                Level,
+                Comments
+            FROM 
+                StartingEmployees
+            ORDER BY 
+                Path DESC;
+        """
+        exception "does not exist"
+    }
+
+
+}
diff --git a/regression-test/suites/recursive_cte/same_data_type_recursive_test.groovy b/regression-test/suites/recursive_cte/same_data_type_recursive_test.groovy
new file mode 100644
index 00000000000..b4047650abb
--- /dev/null
+++ b/regression-test/suites/recursive_cte/same_data_type_recursive_test.groovy
@@ -0,0 +1,430 @@
+// 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("same_data_type_recursive_test", "rec_cte") {
+    String db = context.config.getDbNameByFile(context.file)
+    def prefix_str = "data_null_not_recursive_"
+    def tb_name = prefix_str + "tb"
+
+    // No data satisfies the recursion criteria
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT not null,
+            Name VARCHAR(50)  not NULL,
+            ManagerID INT not NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """INSERT INTO ${tb_name} VALUES 
+        (101, 'Alice', 999), 
+        (102, 'Bob', 999), 
+        (103, 'Charlie', 999);"""
+
+    sql """
+        WITH recursive HierarchyTraversal (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            TraversalLevel
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(1 as bigint) AS TraversalLevel
+            FROM 
+                ${tb_name}
+            WHERE 
+                ManagerID IS NULL
+        
+            UNION ALL
+        
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(R.TraversalLevel + 1 as bigint) AS TraversalLevel
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                HierarchyTraversal AS R
+                ON E.ManagerID = R.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            HierarchyTraversal;"""
+
+
+
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT null,
+            Name VARCHAR(50) NULL,
+            ManagerID INT NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+    sql """INSERT INTO ${tb_name} VALUES 
+        (100, 'CEO Root', NULL), 
+        (101, 'Manager Alpha', 100), 
+        (102, 'Manager Beta', 100),
+        (103, 'Manager Gamma', 100);"""
+
+    sql """
+        WITH recursive ShallowHierarchy (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            TraversalLevel
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(1 as bigint) AS TraversalLevel
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 100 
+        
+            UNION ALL
+        
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(R.TraversalLevel + 1 as bigint) AS TraversalLevel
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                ShallowHierarchy AS R
+                ON E.ManagerID = R.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            ShallowHierarchy
+        WHERE 
+            TraversalLevel > 1
+        ORDER BY
+            EmployeeID;"""
+
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT null,
+            Name VARCHAR(50)  NULL,
+            ManagerID INT NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+    sql """INSERT INTO ${tb_name} VALUES 
+        (100, 'Project A', NULL), 
+        (101, 'Project B', NULL), 
+        (102, 'Project C', NULL);"""
+
+    sql """
+        WITH recursive ExhaustiveAnchor (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            TraversalLevel
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(1 as bigint) AS TraversalLevel
+            FROM 
+                ${tb_name}
+            WHERE 
+                ManagerID IS NULL
+        
+            UNION ALL
+        
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(R.TraversalLevel + 1 as bigint) AS TraversalLevel
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                ExhaustiveAnchor AS R
+                ON E.ManagerID = R.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            ExhaustiveAnchor
+        ORDER BY
+            EmployeeID;"""
+
+
+    // Some data satisfies the recursion criteria, while others do not
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT null,
+            Name VARCHAR(50) NULL,
+            ManagerID INT NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """INSERT INTO ${tb_name} VALUES 
+                (100, 'CEO Alpha', NULL), 
+                (101, 'Manager X', 100), 
+                (102, 'Worker Y', 101),
+                (200, 'Isolated Lead', 999),
+                (201, 'Isolated Worker', 200);"""
+
+    sql """
+        WITH recursive MixedHierarchyTraversal (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            TraversalLevel
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(1 as bigint) AS TraversalLevel
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 100
+        
+            UNION ALL
+        
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(R.TraversalLevel + 1 as bigint) AS TraversalLevel
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                MixedHierarchyTraversal AS R
+                ON E.ManagerID = R.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            MixedHierarchyTraversal
+        ORDER BY
+            EmployeeID;"""
+
+
+    // Multi-branch recursion, executed successfully
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT null,
+            Name VARCHAR(50) NULL,
+            ManagerID INT NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """INSERT INTO ${tb_name} VALUES 
+                (100, 'CEO Root', NULL), 
+                (101, 'Manager Alpha', 100), 
+                (102, 'Worker A1', 101),
+                (103, 'Worker A2', 101),
+                (104, 'Manager Beta', 100),
+                (105, 'Worker B1', 104);"""
+
+    sql """
+        WITH recursive MultiBranchHierarchy (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            TraversalLevel,
+            BranchPath
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(1 as bigint) AS TraversalLevel,
+                CAST(Name AS VARCHAR(65533)) AS BranchPath
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 100
+        
+            UNION ALL
+        
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(R.TraversalLevel + 1 as bigint) AS TraversalLevel,
+                cast(CONCAT(R.BranchPath, ' -> ', E.Name) as VARCHAR(65533)) AS BranchPath
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                MultiBranchHierarchy AS R
+                ON E.ManagerID = R.EmployeeID
+        )
+        SELECT 
+            *
+        FROM 
+            MultiBranchHierarchy
+        ORDER BY
+            BranchPath;"""
+
+    // Cyclic recursion with UNION
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT null,
+            Name VARCHAR(50) NULL,
+            ManagerID INT NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """INSERT INTO ${tb_name} VALUES 
+                (100, 'CEO Root', NULL), 
+                (201, 'Cycle Node A', 202), 
+                (202, 'Cycle Node B', 201);"""
+
+    test {
+        sql """
+        WITH recursive CycleTraversal (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            TraversalCount,
+            PathCheck      
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(1 as bigint) AS TraversalLevel,
+                -- CAST(',' + CAST(EmployeeID AS VARCHAR(65533)) + ',' AS VARCHAR(65533)) AS PathCheck 
+                cast(concat(',', CAST(EmployeeID AS VARCHAR(65533)), ',') as VARCHAR(65533) ) AS PathCheck
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 201 
+        
+            UNION 
+            
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(R.TraversalCount + 1 as bigint) AS TraversalCount,
+                -- R.PathCheck + CAST(E.EmployeeID AS VARCHAR(MAX)) + ',' AS PathCheck
+                cast(concat(R.PathCheck, CAST(E.EmployeeID AS VARCHAR(65533)), ',') as VARCHAR(65533)) AS PathCheck 
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                CycleTraversal AS R 
+                ON E.ManagerID = R.EmployeeID 
+        )
+        SELECT 
+            EmployeeID,
+            EmployeeName,
+            TraversalCount,
+            PathCheck
+        FROM 
+            CycleTraversal
+        ORDER BY
+            TraversalCount;"""
+        exception "reach cte_max_recursion_depth 100"
+    }
+
+    // Cyclic recursion with UNION ALL
+    sql """drop table if exists ${tb_name}"""
+    sql """CREATE TABLE ${tb_name} (
+            EmployeeID INT null,
+            Name VARCHAR(50) NULL,
+            ManagerID INT NULL
+        ) DUPLICATE KEY(EmployeeID) 
+        DISTRIBUTED BY HASH(EmployeeID) 
+        BUCKETS 3 PROPERTIES ('replication_num' = '1');"""
+
+    sql """INSERT INTO ${tb_name} VALUES 
+                (100, 'CEO Root', NULL), 
+                (201, 'Cycle Node A', 202), 
+                (202, 'Cycle Node B', 201);"""
+
+    test {
+        sql """
+        WITH recursive CycleTraversal (
+            EmployeeID, 
+            EmployeeName, 
+            ManagerID, 
+            TraversalCount,
+            PathCheck
+        )
+        AS
+        (
+            SELECT 
+                EmployeeID, 
+                Name AS EmployeeName, 
+                ManagerID, 
+                cast(1 as bigint) AS TraversalCount, 
+                cast(concat(',', CAST(EmployeeID AS VARCHAR(65533)), ',') as VARCHAR(65533)) as PathCheck 
+            FROM 
+                ${tb_name}
+            WHERE 
+                EmployeeID = 201 
+        
+            UNION ALL
+            
+            SELECT 
+                E.EmployeeID, 
+                E.Name AS EmployeeName, 
+                E.ManagerID, 
+                cast(R.TraversalCount + 1 as bigint) AS TraversalCount,
+                cast(concat(R.PathCheck, CAST(E.EmployeeID AS VARCHAR(65533)), ',') as VARCHAR(65533)) as PathCheck
+            FROM 
+                ${tb_name} AS E
+            INNER JOIN 
+                CycleTraversal AS R 
+                ON E.ManagerID = R.EmployeeID 
+        )
+        SELECT 
+            EmployeeID,
+            EmployeeName,
+            TraversalCount,
+            PathCheck
+        FROM 
+            CycleTraversal
+        ORDER BY
+            TraversalCount;"""
+        exception "reach cte_max_recursion_depth 100"
+    }
+
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@doris.apache.org
For additional commands, e-mail: commits-help@doris.apache.org