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 3bfc343de0c branch-4.1: [feature](fe) Push down limit into CTE
producer (#63675) (#64003)
3bfc343de0c is described below
commit 3bfc343de0c496597a59a8377ff2043339987ba8
Author: Calvin Kirs <[email protected]>
AuthorDate: Tue Aug 25 11:19:50 2026 +0800
branch-4.1: [feature](fe) Push down limit into CTE producer (#63675)
(#64003)
Master PR: #63675
---
.../org/apache/doris/nereids/CascadesContext.java | 8 +
.../org/apache/doris/nereids/StatementContext.java | 24 +++
.../doris/nereids/jobs/executor/Rewriter.java | 3 +
.../org/apache/doris/nereids/rules/RuleType.java | 1 +
.../nereids/rules/rewrite/ClearContextStatus.java | 1 +
.../rules/rewrite/CollectLimitAboveConsumer.java | 58 +++++++
.../nereids/rules/rewrite/RewriteCteChildren.java | 18 ++-
.../rewrite/CollectLimitAboveConsumerTest.java | 109 +++++++++++++
.../rules/rewrite/CteLimitPushdownPlanTest.java | 167 ++++++++++++++++++++
.../RewriteCteChildrenLimitPushdownTest.java | 86 ++++++++++
.../limit_push_down/limit_push_down.out | 6 +-
.../limit_push_down/order_push_down.out | 6 +-
.../test_cte_limit_pushdown.groovy | 175 +++++++++++++++++++++
13 files changed, 657 insertions(+), 5 deletions(-)
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 257315a496e..d72a4b034e9 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
@@ -481,6 +481,14 @@ public class CascadesContext implements ScheduleContext {
return this.statementContext.getConsumerIdToFilters();
}
+ public void putConsumerIdToLimitRows(RelationId id, long rows) {
+ this.statementContext.getConsumerIdToLimitRows().merge(id, rows,
Math::max);
+ }
+
+ public Map<RelationId, Long> getConsumerIdToLimitRows() {
+ return this.statementContext.getConsumerIdToLimitRows();
+ }
+
public void addCTEConsumerGroup(CTEId cteId, Group g, Multimap<Slot, Slot>
producerSlotToConsumerSlot) {
List<Pair<Multimap<Slot, Slot>, Group>> consumerGroups =
this.statementContext.getCteIdToConsumerGroup().computeIfAbsent(cteId, k -> new
ArrayList<>());
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
index 4a2c3deeb72..5fd4afa1fc3 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
@@ -172,6 +172,7 @@ public class StatementContext implements Closeable {
private final Map<CTEId, LogicalCTEProducer<? extends Plan>>
cteIdToProducer = new HashMap<>();
private final Map<RelationId, Set<Expression>> consumerIdToFilters = new
HashMap<>();
+ private final Map<RelationId, Long> consumerIdToLimitRows = new
HashMap<>();
// Used to update consumer's stats
private final Map<CTEId, List<Pair<Multimap<Slot, Slot>, Group>>>
cteIdToConsumerGroup = new HashMap<>();
private final Map<CTEId, LogicalPlan> rewrittenCteProducer = new
HashMap<>();
@@ -669,6 +670,10 @@ public class StatementContext implements Closeable {
return consumerIdToFilters;
}
+ public Map<RelationId, Long> getConsumerIdToLimitRows() {
+ return consumerIdToLimitRows;
+ }
+
public PlaceholderId getNextPlaceholderId() {
return placeHolderIdGenerator.getNextId();
}
@@ -693,6 +698,18 @@ public class StatementContext implements Closeable {
return rewrittenCteConsumer;
}
+ /** Clear CTE-related rewrite and memo state before rebuilding it from a
new plan tree. */
+ public void clearCteEnvironment() {
+ cteIdToConsumers.clear();
+ cteIdToOutputIds.clear();
+ cteIdToProducer.clear();
+ consumerIdToFilters.clear();
+ consumerIdToLimitRows.clear();
+ cteIdToConsumerGroup.clear();
+ rewrittenCteProducer.clear();
+ rewrittenCteConsumer.clear();
+ }
+
/**
* Snapshot current CTE-related environment for temporary
rewrite/optimization.
*/
@@ -702,6 +719,7 @@ public class StatementContext implements Closeable {
copyMapOfSets(cteIdToOutputIds),
new HashMap<>(cteIdToProducer),
copyMapOfSets(consumerIdToFilters),
+ new HashMap<>(consumerIdToLimitRows),
copyMapOfLists(cteIdToConsumerGroup),
new HashMap<>(rewrittenCteProducer),
new HashMap<>(rewrittenCteConsumer));
@@ -721,6 +739,9 @@ public class StatementContext implements Closeable {
consumerIdToFilters.clear();
consumerIdToFilters.putAll(snapshot.consumerIdToFilters);
+ consumerIdToLimitRows.clear();
+ consumerIdToLimitRows.putAll(snapshot.consumerIdToLimitRows);
+
cteIdToConsumerGroup.clear();
cteIdToConsumerGroup.putAll(snapshot.cteIdToConsumerGroup);
@@ -753,6 +774,7 @@ public class StatementContext implements Closeable {
private final Map<CTEId, Set<Slot>> cteIdToOutputIds;
private final Map<CTEId, LogicalCTEProducer<? extends Plan>>
cteIdToProducer;
private final Map<RelationId, Set<Expression>> consumerIdToFilters;
+ private final Map<RelationId, Long> consumerIdToLimitRows;
private final Map<CTEId, List<Pair<Multimap<Slot, Slot>, Group>>>
cteIdToConsumerGroup;
private final Map<CTEId, LogicalPlan> rewrittenCteProducer;
private final Map<CTEId, LogicalPlan> rewrittenCteConsumer;
@@ -765,6 +787,7 @@ public class StatementContext implements Closeable {
Map<CTEId, Set<Slot>> cteIdToOutputIds,
Map<CTEId, LogicalCTEProducer<? extends Plan>> cteIdToProducer,
Map<RelationId, Set<Expression>> consumerIdToFilters,
+ Map<RelationId, Long> consumerIdToLimitRows,
Map<CTEId, List<Pair<Multimap<Slot, Slot>, Group>>>
cteIdToConsumerGroup,
Map<CTEId, LogicalPlan> rewrittenCteProducer,
Map<CTEId, LogicalPlan> rewrittenCteConsumer) {
@@ -772,6 +795,7 @@ public class StatementContext implements Closeable {
this.cteIdToOutputIds = cteIdToOutputIds;
this.cteIdToProducer = cteIdToProducer;
this.consumerIdToFilters = consumerIdToFilters;
+ this.consumerIdToLimitRows = consumerIdToLimitRows;
this.cteIdToConsumerGroup = cteIdToConsumerGroup;
this.rewrittenCteProducer = rewrittenCteProducer;
this.rewrittenCteConsumer = rewrittenCteConsumer;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Rewriter.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Rewriter.java
index 7667b93f6a8..9167a0aed68 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Rewriter.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/jobs/executor/Rewriter.java
@@ -52,6 +52,7 @@ import org.apache.doris.nereids.rules.rewrite.CheckScoreUsage;
import org.apache.doris.nereids.rules.rewrite.ClearContextStatus;
import org.apache.doris.nereids.rules.rewrite.CollectCteConsumerOutput;
import org.apache.doris.nereids.rules.rewrite.CollectFilterAboveConsumer;
+import org.apache.doris.nereids.rules.rewrite.CollectLimitAboveConsumer;
import org.apache.doris.nereids.rules.rewrite.CollectPredicateOnScan;
import org.apache.doris.nereids.rules.rewrite.ColumnPruning;
import org.apache.doris.nereids.rules.rewrite.ConstantPropagation;
@@ -393,6 +394,7 @@ public class Rewriter extends AbstractBatchJobExecutor {
topic("Push project and filter on cte consumer to
cte producer",
topDown(
new CollectFilterAboveConsumer(),
+ new CollectLimitAboveConsumer(),
new CollectCteConsumerOutput())
),
topic("eliminate join according unique or foreign
key",
@@ -768,6 +770,7 @@ public class Rewriter extends AbstractBatchJobExecutor {
topic("Push project and filter on cte consumer to cte
producer",
topDown(
new CollectFilterAboveConsumer(),
+ new CollectLimitAboveConsumer(),
new CollectCteConsumerOutput()
)
),
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 43383be8ad7..25f46eba8e5 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
@@ -403,6 +403,7 @@ public enum RuleType {
CTE_INLINE(RuleTypeClass.REWRITE),
REWRITE_CTE_CHILDREN(RuleTypeClass.REWRITE),
COLLECT_FILTER_ABOVE_CTE_CONSUMER(RuleTypeClass.REWRITE),
+ COLLECT_LIMIT_ABOVE_CTE_CONSUMER(RuleTypeClass.REWRITE),
INLINE_VIEW(RuleTypeClass.REWRITE),
CHECK_PRIVILEGES(RuleTypeClass.REWRITE),
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ClearContextStatus.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ClearContextStatus.java
index cb5dcf5526a..649d7682e1d 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ClearContextStatus.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ClearContextStatus.java
@@ -35,6 +35,7 @@ public class ClearContextStatus implements CustomRewriter {
jobContext.getCascadesContext().getStatementContext().getRewrittenCteProducer().clear();
jobContext.getCascadesContext().getStatementContext().getCteIdToOutputIds().clear();
jobContext.getCascadesContext().getStatementContext().getConsumerIdToFilters().clear();
+
jobContext.getCascadesContext().getStatementContext().getConsumerIdToLimitRows().clear();
return plan;
}
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectLimitAboveConsumer.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectLimitAboveConsumer.java
new file mode 100644
index 00000000000..bb573304e3a
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectLimitAboveConsumer.java
@@ -0,0 +1,58 @@
+// 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.CascadesContext;
+import org.apache.doris.nereids.rules.Rule;
+import org.apache.doris.nereids.rules.RuleType;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
+import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * Collect limit rows needed by CTE consumers.
+ */
+public class CollectLimitAboveConsumer implements RewriteRuleFactory {
+
+ @Override
+ public List<Rule> buildRules() {
+ return ImmutableList.of(
+ logicalLimit(logicalCTEConsumer()).thenApply(ctx -> {
+ LogicalLimit<LogicalCTEConsumer> limit = ctx.root;
+ collectLimitRows(ctx.cascadesContext, limit,
limit.child());
+ return ctx.root;
+ }).toRule(RuleType.COLLECT_LIMIT_ABOVE_CTE_CONSUMER),
+ logicalLimit(logicalProject(logicalCTEConsumer()))
+ .thenApply(ctx -> {
+ LogicalLimit<LogicalProject<LogicalCTEConsumer>>
limit = ctx.root;
+ collectLimitRows(ctx.cascadesContext, limit,
limit.child().child());
+ return ctx.root;
+ }).toRule(RuleType.COLLECT_LIMIT_ABOVE_CTE_CONSUMER)
+ );
+ }
+
+ private void collectLimitRows(CascadesContext cascadesContext,
LogicalLimit<?> limit,
+ LogicalCTEConsumer cteConsumer) {
+ cascadesContext.putConsumerIdToLimitRows(
+ cteConsumer.getRelationId(), limit.getLimit() +
limit.getOffset());
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildren.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildren.java
index 0360c4aa0e7..c957a9e853f 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildren.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildren.java
@@ -28,12 +28,14 @@ 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.LimitPhase;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.RelationId;
import org.apache.doris.nereids.trees.plans.logical.LogicalCTEAnchor;
import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
import org.apache.doris.nereids.trees.plans.logical.LogicalCTEProducer;
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
+import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
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.visitor.CustomRewriter;
@@ -124,9 +126,10 @@ public class RewriteCteChildren extends
DefaultPlanRewriter<CascadesContext> imp
} else {
child = (LogicalPlan) cteProducer.child();
child = tryToConstructFilter(cascadesContext,
cteProducer.getCteId(), child);
+ child = tryToConstructLimit(cascadesContext,
cteProducer.getCteId(), child);
Set<Slot> producerOutputs = cascadesContext.getStatementContext()
.getCteIdToOutputIds().get(cteProducer.getCteId());
- if (producerOutputs.size() < child.getOutput().size()) {
+ if (producerOutputs != null && producerOutputs.size() <
child.getOutput().size()) {
ImmutableList.Builder<NamedExpression> projectsBuilder
=
ImmutableList.builderWithExpectedSize(producerOutputs.size());
for (Slot slot : child.getOutput()) {
@@ -162,6 +165,19 @@ public class RewriteCteChildren extends
DefaultPlanRewriter<CascadesContext> imp
return plan;
}
+ private LogicalPlan tryToConstructLimit(CascadesContext cascadesContext,
CTEId cteId, LogicalPlan child) {
+ Set<LogicalCTEConsumer> consumers =
cascadesContext.getCteIdToConsumers().get(cteId);
+ long limit = 0;
+ for (LogicalCTEConsumer consumer : consumers) {
+ Long rowsNeeded =
cascadesContext.getConsumerIdToLimitRows().get(consumer.getRelationId());
+ if (rowsNeeded == null) {
+ return child;
+ }
+ limit = Math.max(limit, rowsNeeded);
+ }
+ return pushPlanUnderAnchor(new LogicalLimit<>(limit, 0,
LimitPhase.ORIGIN, child));
+ }
+
/*
* An expression can only be pushed down if it has filter expressions on
all consumers that reference the slot.
* For example, let's assume a producer has two consumers, consumer1 and
consumer2:
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CollectLimitAboveConsumerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CollectLimitAboveConsumerTest.java
new file mode 100644
index 00000000000..94532027e66
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CollectLimitAboveConsumerTest.java
@@ -0,0 +1,109 @@
+// 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.CascadesContext;
+import org.apache.doris.nereids.rules.Rule;
+import org.apache.doris.nereids.trees.expressions.CTEId;
+import org.apache.doris.nereids.trees.plans.LimitPhase;
+import org.apache.doris.nereids.trees.plans.RelationId;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
+import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanConstructor;
+import org.apache.doris.qe.ConnectContext;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tests for {@link CollectLimitAboveConsumer}.
+ */
+class CollectLimitAboveConsumerTest {
+
+ @Test
+ void testCollectDirectLimitRowsNeeded() {
+ LogicalOlapScan producerPlan = PlanConstructor.newLogicalOlapScan(0,
"t1", 0);
+ LogicalCTEConsumer consumer = new LogicalCTEConsumer(
+ PlanConstructor.getNextRelationId(), new CTEId(1), "cte1",
producerPlan);
+ LogicalLimit<LogicalCTEConsumer> limit = new LogicalLimit<>(10, 5,
LimitPhase.ORIGIN, consumer);
+
+ CascadesContext cascadesContext =
MemoTestUtils.createCascadesContext(new ConnectContext(), limit);
+ Rule rule = new CollectLimitAboveConsumer().buildRules().get(0);
+ rule.transform(limit, cascadesContext);
+
+ Map<RelationId, Long> collected =
cascadesContext.getStatementContext().getConsumerIdToLimitRows();
+ Assertions.assertEquals(15L, collected.get(consumer.getRelationId()));
+ }
+
+ @Test
+ void testCollectLocalLimitRowsNeededWithoutAddingOffsetAgain() {
+ LogicalOlapScan producerPlan = PlanConstructor.newLogicalOlapScan(1,
"t2", 0);
+ LogicalCTEConsumer consumer = new LogicalCTEConsumer(
+ PlanConstructor.getNextRelationId(), new CTEId(2), "cte2",
producerPlan);
+ LogicalLimit<LogicalCTEConsumer> limit = new LogicalLimit<>(15, 0,
LimitPhase.LOCAL, consumer);
+
+ CascadesContext cascadesContext =
MemoTestUtils.createCascadesContext(new ConnectContext(), limit);
+ Rule rule = new CollectLimitAboveConsumer().buildRules().get(0);
+ rule.transform(limit, cascadesContext);
+
+ Map<RelationId, Long> collected =
cascadesContext.getStatementContext().getConsumerIdToLimitRows();
+ Assertions.assertEquals(15L, collected.get(consumer.getRelationId()));
+ }
+
+ @Test
+ void testKeepMaxRowsNeededWhenConsumerIsCollectedMultipleTimes() {
+ LogicalOlapScan producerPlan = PlanConstructor.newLogicalOlapScan(10,
"t_merge", 0);
+ LogicalCTEConsumer consumer = new LogicalCTEConsumer(
+ PlanConstructor.getNextRelationId(), new CTEId(10),
"cte_merge", producerPlan);
+ LogicalLimit<LogicalCTEConsumer> highLimit = new LogicalLimit<>(20, 0,
LimitPhase.ORIGIN, consumer);
+ LogicalLimit<LogicalCTEConsumer> lowLimit = new LogicalLimit<>(3, 0,
LimitPhase.ORIGIN, consumer);
+
+ CascadesContext cascadesContext =
MemoTestUtils.createCascadesContext(new ConnectContext(), highLimit);
+ Rule rule = new CollectLimitAboveConsumer().buildRules().get(0);
+ rule.transform(highLimit, cascadesContext);
+ rule.transform(lowLimit, cascadesContext);
+
+ Map<RelationId, Long> collected =
cascadesContext.getStatementContext().getConsumerIdToLimitRows();
+ Assertions.assertEquals(20L, collected.get(consumer.getRelationId()));
+ }
+
+ @Test
+ void testCollectLimitAboveProjectRowsNeeded() {
+ LogicalOlapScan producerPlan = PlanConstructor.newLogicalOlapScan(2,
"t3", 0);
+ LogicalCTEConsumer consumer = new LogicalCTEConsumer(
+ PlanConstructor.getNextRelationId(), new CTEId(3), "cte3",
producerPlan);
+ LogicalProject<LogicalCTEConsumer> project = new LogicalProject<>(
+ ImmutableList.copyOf(consumer.getOutput()), consumer);
+ LogicalLimit<LogicalProject<LogicalCTEConsumer>> limit = new
LogicalLimit<>(
+ 7, 0, LimitPhase.LOCAL, project);
+
+ CascadesContext cascadesContext =
MemoTestUtils.createCascadesContext(new ConnectContext(), limit);
+ List<Rule> rules = new CollectLimitAboveConsumer().buildRules();
+ rules.get(1).transform(limit, cascadesContext);
+
+ Map<RelationId, Long> collected =
cascadesContext.getStatementContext().getConsumerIdToLimitRows();
+ Assertions.assertEquals(7L, collected.get(consumer.getRelationId()));
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CteLimitPushdownPlanTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CteLimitPushdownPlanTest.java
new file mode 100644
index 00000000000..fae1be1f6d4
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CteLimitPushdownPlanTest.java
@@ -0,0 +1,167 @@
+// 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.trees.expressions.StatementScopeIdGenerator;
+import org.apache.doris.nereids.util.MemoPatternMatchSupported;
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.utframe.TestWithFeService;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Planner-level tests for CTE limit pushdown.
+ */
+class CteLimitPushdownPlanTest extends TestWithFeService implements
MemoPatternMatchSupported {
+
+ @Override
+ protected void runBeforeAll() throws Exception {
+ createDatabase("test");
+ useDatabase("test");
+
connectContext.getSessionVariable().setDisableNereidsRules("PRUNE_EMPTY_PARTITION");
+
+ createTable("CREATE TABLE cte_limit_pushdown_t (\n"
+ + " k1 int NULL,\n"
+ + " k2 int NULL\n"
+ + ") ENGINE=OLAP\n"
+ + "DISTRIBUTED BY HASH(k1) BUCKETS 1\n"
+ + "PROPERTIES (\n"
+ + " \"replication_allocation\" = \"tag.location.default:
1\"\n"
+ + ");");
+ }
+
+ @Override
+ protected void runBeforeEach() throws Exception {
+ StatementScopeIdGenerator.clear();
+ }
+
+ @Test
+ void testPushLimitWithOffsetToProducer() {
+ String sql = "WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_t) "
+ + "(SELECT * FROM cte LIMIT 10 OFFSET 5) "
+ + "UNION ALL "
+ + "(SELECT * FROM cte LIMIT 3)";
+
+ PlanChecker.from(connectContext)
+ .analyze(sql)
+ .rewrite()
+ .matches(logicalCTEProducer(
+ logicalLimit().when(limit -> limit.getLimit() == 15 &&
limit.getOffset() == 0)));
+ }
+
+ @Test
+ void testPushLimitBeforeProducerOutputPruning() {
+ String sql = "WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_t) "
+ + "(SELECT k1 FROM cte LIMIT 7) "
+ + "UNION ALL "
+ + "(SELECT k1 FROM cte LIMIT 3)";
+
+ PlanChecker.from(connectContext)
+ .analyze(sql)
+ .rewrite()
+ .matches(logicalCTEProducer(
+ logicalLimit().when(limit -> limit.getLimit() == 7 &&
limit.getOffset() == 0)));
+ }
+
+ @Test
+ void testPushMaxLimitForAllLimitedConsumers() {
+ String sql = "WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_t) "
+ + "(SELECT * FROM cte LIMIT 10 OFFSET 5) "
+ + "UNION ALL "
+ + "(SELECT * FROM cte LIMIT 20)";
+
+ PlanChecker.from(connectContext)
+ .analyze(sql)
+ .rewrite()
+ .matches(logicalCTEProducer(
+ logicalLimit().when(limit -> limit.getLimit() == 20 &&
limit.getOffset() == 0)));
+ }
+
+ @Test
+ void testSkipProducerLimitWhenAnyConsumerNeedsFullRows() {
+ String sql = "WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_t) "
+ + "(SELECT * FROM cte LIMIT 10) "
+ + "UNION ALL "
+ + "(SELECT * FROM cte)";
+
+ assertNoProducerLimit(sql);
+ }
+
+ @Test
+ void testSkipProducerLimitWhenLimitIsAboveFilter() {
+ String sql = "WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_t) "
+ + "(SELECT * FROM cte WHERE k1 > 1 LIMIT 10) "
+ + "UNION ALL "
+ + "(SELECT * FROM cte LIMIT 3)";
+
+ assertNoProducerLimit(sql);
+ }
+
+ @Test
+ void testSkipProducerLimitWhenConsumerUsesTopN() {
+ String sql = "WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_t) "
+ + "(SELECT * FROM (SELECT * FROM cte ORDER BY k1 LIMIT 10)
topn_branch) "
+ + "UNION ALL "
+ + "(SELECT * FROM cte LIMIT 3)";
+
+ assertNoProducerLimit(sql);
+ }
+
+ @Test
+ void testSkipProducerLimitWhenLimitIsAboveJoin() {
+ String sql = "WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_t) "
+ + "(SELECT c.k1, c.k2 FROM cte c "
+ + "JOIN cte_limit_pushdown_t t ON c.k1 = t.k1 LIMIT 10) "
+ + "UNION ALL "
+ + "(SELECT * FROM cte LIMIT 3)";
+
+ assertNoProducerLimit(sql);
+ }
+
+ @Test
+ void testSkipProducerLimitWhenLimitIsAboveAggregate() {
+ String sql = "WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_t) "
+ + "(SELECT k1, COUNT(*) FROM cte GROUP BY k1 LIMIT 10) "
+ + "UNION ALL "
+ + "(SELECT * FROM cte LIMIT 3)";
+
+ assertNoProducerLimit(sql);
+ }
+
+ @Test
+ void testSkipProducerLimitWhenLimitIsAboveWindow() {
+ String sql = "WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_t) "
+ + "(SELECT k1, rn FROM ("
+ + "SELECT k1, ROW_NUMBER() OVER (ORDER BY k1) AS rn FROM cte"
+ + ") window_branch LIMIT 10) "
+ + "UNION ALL "
+ + "(SELECT * FROM cte LIMIT 3)";
+
+ assertNoProducerLimit(sql);
+ }
+
+ private void assertNoProducerLimit(String sql) {
+ PlanChecker.from(connectContext)
+ .analyze(sql)
+ .rewrite()
+ .matches(logicalCTEProducer())
+ .nonMatch(logicalCTEProducer(logicalLimit()))
+ .nonMatch(logicalCTEProducer(logicalLimit(logicalProject())))
+ .nonMatch(logicalCTEProducer(logicalProject(logicalLimit())));
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildrenLimitPushdownTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildrenLimitPushdownTest.java
new file mode 100644
index 00000000000..fb7a07e47b6
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildrenLimitPushdownTest.java
@@ -0,0 +1,86 @@
+// 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.CascadesContext;
+import org.apache.doris.nereids.trees.expressions.CTEId;
+import org.apache.doris.nereids.trees.plans.LimitPhase;
+import org.apache.doris.nereids.trees.plans.Plan;
+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.LogicalLimit;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanConstructor;
+import org.apache.doris.qe.ConnectContext;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests producer-side CTE limit construction in {@link RewriteCteChildren}.
+ */
+class RewriteCteChildrenLimitPushdownTest {
+
+ @Test
+ void testPushMaxConsumerLimitToProducer() {
+ LogicalOlapScan scan = PlanConstructor.newLogicalOlapScan(0, "t1", 0);
+ CTEId cteId = new CTEId(1);
+ LogicalCTEProducer<LogicalOlapScan> producer = new
LogicalCTEProducer<>(cteId, scan);
+ LogicalCTEConsumer consumer1 = new LogicalCTEConsumer(
+ PlanConstructor.getNextRelationId(), cteId, "cte1", producer);
+ LogicalCTEConsumer consumer2 = new LogicalCTEConsumer(
+ PlanConstructor.getNextRelationId(), cteId, "cte1", producer);
+ CascadesContext cascadesContext =
MemoTestUtils.createCascadesContext(new ConnectContext(), producer);
+ cascadesContext.getCteIdToConsumers().put(cteId,
ImmutableSet.of(consumer1, consumer2));
+ cascadesContext.putConsumerIdToLimitRows(consumer1.getRelationId(),
10L);
+ cascadesContext.putConsumerIdToLimitRows(consumer2.getRelationId(),
20L);
+
+ Plan rewritten = new RewriteCteChildren(ImmutableList.of(), false)
+ .visitLogicalCTEProducer(producer, cascadesContext);
+
+ LogicalCTEProducer<?> rewrittenProducer = (LogicalCTEProducer<?>)
rewritten;
+ Assertions.assertInstanceOf(LogicalLimit.class,
rewrittenProducer.child());
+ LogicalLimit<?> limit = (LogicalLimit<?>) rewrittenProducer.child();
+ Assertions.assertEquals(20L, limit.getLimit());
+ Assertions.assertEquals(0L, limit.getOffset());
+ Assertions.assertEquals(LimitPhase.ORIGIN, limit.getPhase());
+ }
+
+ @Test
+ void testSkipProducerLimitWhenAnyConsumerHasNoLimit() {
+ LogicalOlapScan scan = PlanConstructor.newLogicalOlapScan(1, "t2", 0);
+ CTEId cteId = new CTEId(2);
+ LogicalCTEProducer<LogicalOlapScan> producer = new
LogicalCTEProducer<>(cteId, scan);
+ LogicalCTEConsumer consumer1 = new LogicalCTEConsumer(
+ PlanConstructor.getNextRelationId(), cteId, "cte2", producer);
+ LogicalCTEConsumer consumer2 = new LogicalCTEConsumer(
+ PlanConstructor.getNextRelationId(), cteId, "cte2", producer);
+ CascadesContext cascadesContext =
MemoTestUtils.createCascadesContext(new ConnectContext(), producer);
+ cascadesContext.getCteIdToConsumers().put(cteId,
ImmutableSet.of(consumer1, consumer2));
+ cascadesContext.putConsumerIdToLimitRows(consumer1.getRelationId(),
10L);
+
+ Plan rewritten = new RewriteCteChildren(ImmutableList.of(), false)
+ .visitLogicalCTEProducer(producer, cascadesContext);
+
+ LogicalCTEProducer<?> rewrittenProducer = (LogicalCTEProducer<?>)
rewritten;
+ Assertions.assertSame(scan, rewrittenProducer.child());
+ }
+}
diff --git
a/regression-test/data/nereids_rules_p0/limit_push_down/limit_push_down.out
b/regression-test/data/nereids_rules_p0/limit_push_down/limit_push_down.out
index 7e7e5544611..5ae4ea2f1dd 100644
--- a/regression-test/data/nereids_rules_p0/limit_push_down/limit_push_down.out
+++ b/regression-test/data/nereids_rules_p0/limit_push_down/limit_push_down.out
@@ -399,8 +399,10 @@ PhysicalResultSink
-- !limit_cte_query --
PhysicalCteAnchor ( cteId=CTEId#0 )
--PhysicalCteProducer ( cteId=CTEId#0 )
-----filter((t1.id < 10))
-------PhysicalOlapScan[t1]
+----PhysicalLimit[GLOBAL]
+------PhysicalLimit[LOCAL]
+--------filter((t1.id < 10))
+----------PhysicalOlapScan[t1]
--PhysicalResultSink
----PhysicalLimit[GLOBAL]
------PhysicalLimit[LOCAL]
diff --git
a/regression-test/data/nereids_rules_p0/limit_push_down/order_push_down.out
b/regression-test/data/nereids_rules_p0/limit_push_down/order_push_down.out
index fbed0a9f889..8ab46e582e5 100644
--- a/regression-test/data/nereids_rules_p0/limit_push_down/order_push_down.out
+++ b/regression-test/data/nereids_rules_p0/limit_push_down/order_push_down.out
@@ -491,8 +491,10 @@ PhysicalCteAnchor ( cteId=CTEId#0 )
-- !limit_cte_outside_query --
PhysicalCteAnchor ( cteId=CTEId#0 )
--PhysicalCteProducer ( cteId=CTEId#0 )
-----filter((t1.id < 10))
-------PhysicalOlapScan[t1]
+----PhysicalLimit[GLOBAL]
+------PhysicalLimit[LOCAL]
+--------filter((t1.id < 10))
+----------PhysicalOlapScan[t1]
--PhysicalResultSink
----PhysicalLimit[GLOBAL]
------PhysicalLimit[LOCAL]
diff --git
a/regression-test/suites/nereids_rules_p0/cte_limit_pushdown/test_cte_limit_pushdown.groovy
b/regression-test/suites/nereids_rules_p0/cte_limit_pushdown/test_cte_limit_pushdown.groovy
new file mode 100644
index 00000000000..863b1b1a3f1
--- /dev/null
+++
b/regression-test/suites/nereids_rules_p0/cte_limit_pushdown/test_cte_limit_pushdown.groovy
@@ -0,0 +1,175 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_cte_limit_pushdown") {
+ sql "SET enable_nereids_planner=true"
+ sql "SET enable_fallback_to_original_planner=false"
+ sql "SET disable_nereids_rules='PRUNE_EMPTY_PARTITION'"
+
+ sql "DROP TABLE IF EXISTS cte_limit_pushdown_regression_t"
+ sql """
+ CREATE TABLE cte_limit_pushdown_regression_t (
+ k1 int NULL,
+ k2 int NULL
+ )
+ DUPLICATE KEY(k1)
+ DISTRIBUTED BY HASH(k1) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1"
+ )
+ """
+
+ sql """
+ INSERT INTO cte_limit_pushdown_regression_t VALUES
+ (1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)
+ """
+
+ def cteProducerFragment = { explainString ->
+ int multicast = explainString.indexOf("MultiCastDataSinks")
+ assert multicast >= 0
+ int fragmentStart = explainString.lastIndexOf("PLAN FRAGMENT",
multicast)
+ assert fragmentStart >= 0
+ int fragmentEnd = explainString.indexOf("PLAN FRAGMENT", multicast + 1)
+ if (fragmentEnd < 0) {
+ fragmentEnd = explainString.length()
+ }
+ return explainString.substring(fragmentStart, fragmentEnd)
+ }
+
+ def cteProducerSourceBlock = { explainString ->
+ int multicast = explainString.indexOf("MultiCastDataSinks")
+ assert multicast >= 0
+ int scanStart = explainString.indexOf(":VOlapScanNode", multicast)
+ assert scanStart >= 0
+ int nextFragment = explainString.indexOf("PLAN FRAGMENT", scanStart +
1)
+ if (nextFragment < 0) {
+ nextFragment = explainString.length()
+ }
+ return explainString.substring(scanStart, nextFragment)
+ }
+
+ def hasExactLimit = { planBlock, expectedLimit ->
+ return planBlock.readLines().any { line -> line.trim() == "limit:
${expectedLimit}" }
+ }
+
+ def hasAnyLimit = { planBlock ->
+ return planBlock.readLines().any { line ->
line.trim().startsWith("limit: ") }
+ }
+
+ def assertProducerLimit = { explainString, expectedLimit ->
+ String producerFragment = cteProducerFragment(explainString)
+ String producerSource = cteProducerSourceBlock(explainString)
+ assert producerFragment.contains("MultiCastDataSinks")
+ assert producerSource.contains("cte_limit_pushdown_regression_t")
+ assert hasExactLimit(producerFragment, expectedLimit)
+ assert hasExactLimit(producerSource, expectedLimit)
+ return true
+ }
+
+ def assertNoProducerLimit = { explainString ->
+ String producerFragment = cteProducerFragment(explainString)
+ String producerSource = cteProducerSourceBlock(explainString)
+ assert producerFragment.contains("MultiCastDataSinks")
+ assert producerSource.contains("cte_limit_pushdown_regression_t")
+ assert !hasAnyLimit(producerFragment)
+ assert !hasAnyLimit(producerSource)
+ return true
+ }
+
+ explain {
+ sql """
+ WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_regression_t)
+ (SELECT * FROM cte LIMIT 10 OFFSET 5)
+ UNION ALL
+ (SELECT * FROM cte LIMIT 3)
+ """
+ check { explainString -> assertProducerLimit(explainString, 15) }
+ }
+
+ explain {
+ sql """
+ WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_regression_t)
+ (SELECT k1 FROM cte LIMIT 7)
+ UNION ALL
+ (SELECT k1 FROM cte LIMIT 3)
+ """
+ check { explainString -> assertProducerLimit(explainString, 7) }
+ }
+
+ explain {
+ sql """
+ WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_regression_t)
+ (SELECT * FROM cte LIMIT 10)
+ UNION ALL
+ (SELECT * FROM cte)
+ """
+ check { explainString -> assertNoProducerLimit(explainString) }
+ }
+
+ explain {
+ sql """
+ WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_regression_t)
+ (SELECT * FROM cte WHERE k1 > 1 LIMIT 10)
+ UNION ALL
+ (SELECT * FROM cte LIMIT 3)
+ """
+ check { explainString -> assertNoProducerLimit(explainString) }
+ }
+
+ explain {
+ sql """
+ WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_regression_t)
+ (SELECT * FROM (SELECT * FROM cte ORDER BY k1 LIMIT 10)
topn_branch)
+ UNION ALL
+ (SELECT * FROM cte LIMIT 3)
+ """
+ check { explainString -> assertNoProducerLimit(explainString) }
+ }
+
+ explain {
+ sql """
+ WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_regression_t)
+ (SELECT c.k1, c.k2 FROM cte c
+ JOIN cte_limit_pushdown_regression_t t ON c.k1 = t.k1 LIMIT 10)
+ UNION ALL
+ (SELECT * FROM cte LIMIT 3)
+ """
+ check { explainString -> assertNoProducerLimit(explainString) }
+ }
+
+ explain {
+ sql """
+ WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_regression_t)
+ (SELECT k1, COUNT(*) FROM cte GROUP BY k1 LIMIT 10)
+ UNION ALL
+ (SELECT * FROM cte LIMIT 3)
+ """
+ check { explainString -> assertNoProducerLimit(explainString) }
+ }
+
+ explain {
+ sql """
+ WITH cte AS (SELECT k1, k2 FROM cte_limit_pushdown_regression_t)
+ (SELECT k1, rn FROM (
+ SELECT k1, ROW_NUMBER() OVER (ORDER BY k1) AS rn FROM cte
+ ) window_branch LIMIT 10)
+ UNION ALL
+ (SELECT * FROM cte LIMIT 3)
+ """
+ check { explainString -> assertNoProducerLimit(explainString) }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]