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 2242ae9cc7b branch-4.1: [fix](nereids) clamp the merged limit of
MERGE_TOP_N by the parent offset #64306 (#64353)
2242ae9cc7b is described below
commit 2242ae9cc7b54fac4b52ba0e614d12d08f0f05a4
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Jun 17 09:24:28 2026 +0800
branch-4.1: [fix](nereids) clamp the merged limit of MERGE_TOP_N by the
parent offset #64306 (#64353)
Cherry-picked from #64306
Co-authored-by: 924060929 <[email protected]>
---
.../doris/nereids/rules/rewrite/MergeTopNs.java | 11 +++--
.../nereids/rules/rewrite/MergeTopNsTest.java | 20 ++++++++-
.../data/correctness_p0/test_merge_topn_offset.out | 10 +++++
.../correctness_p0/test_merge_topn_offset.groovy | 50 ++++++++++++++++++++++
4 files changed, 87 insertions(+), 4 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeTopNs.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeTopNs.java
index 39ea7a384df..3614434459d 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeTopNs.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeTopNs.java
@@ -30,7 +30,8 @@ import java.util.List;
* <p>
* topN - child topN
* If child topN orderby list is prefix subList of topN =>
- * topN with limit = min(topN.limit, childTopN.limit), offset = topN.offset +
childTopN.offset
+ * topN with limit = min(topN.limit, max(childTopN.limit - topN.offset, 0)),
+ * offset = topN.offset + childTopN.offset
*/
public class MergeTopNs extends OneRewriteRuleFactory {
@Override
@@ -48,8 +49,12 @@ public class MergeTopNs extends OneRewriteRuleFactory {
long childOffset = childTopN.getOffset();
long childLimit = childTopN.getLimit();
long newOffset = offset + childOffset;
- // choose min limit
- long newLimit = Math.min(limit, childLimit);
+ // The parent's offset is applied on top of the child's
output, so only
+ // (childLimit - offset) of the child's rows survive.
Clamp the merged limit
+ // by that remaining count; otherwise rows beyond the
child's limit leak through
+ // when the parent has a non-zero offset (DORIS-26301).
Same semantics as
+ // MergeLimits.mergeLimit for consecutive limits.
+ long newLimit = Math.min(limit, Math.max(childLimit -
offset, 0));
return topN.withLimitChild(newLimit, newOffset,
childTopN.child());
}).toRule(RuleType.MERGE_TOP_N);
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/MergeTopNsTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/MergeTopNsTest.java
index e16a38be6ba..63b2c97ac31 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/MergeTopNsTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/MergeTopNsTest.java
@@ -70,7 +70,25 @@ class MergeTopNsTest implements MemoPatternMatchSupported {
PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
.applyTopDown(new MergeTopNs())
.matches(
- logicalTopN(logicalOlapScan()).when(topN ->
topN.getLimit() == 10 && topN.getOffset() == 4)
+ logicalTopN(logicalOlapScan()).when(topN ->
topN.getLimit() == 9 && topN.getOffset() == 4)
+ );
+ }
+
+ @Test
+ void testParentOffsetReducesChildLimit() {
+ // DORIS-26301: when the parent (upper) TopN has a non-zero offset,
the merged limit must be
+ // clamped by (childLimit - parentOffset); otherwise rows beyond the
child's limit leak through.
+ // child : ORDER BY k LIMIT 5 (limit=5, offset=0) -> yields
5 rows
+ // parent: ORDER BY k LIMIT 3 OFFSET 4 (limit=3, offset=4) -> skips
4, only 1 row remains
+ // merged: offset = 4, limit = min(3, max(5 - 4, 0)) = 1
+ LogicalPlan plan = new LogicalPlanBuilder(score)
+ .topN(5, 0, ImmutableList.of(0))
+ .topN(3, 4, ImmutableList.of(0))
+ .build();
+ PlanChecker.from(MemoTestUtils.createConnectContext(), plan)
+ .applyTopDown(new MergeTopNs())
+ .matches(
+ logicalTopN(logicalOlapScan()).when(topN ->
topN.getLimit() == 1 && topN.getOffset() == 4)
);
}
diff --git a/regression-test/data/correctness_p0/test_merge_topn_offset.out
b/regression-test/data/correctness_p0/test_merge_topn_offset.out
new file mode 100644
index 00000000000..bf103b2fed2
--- /dev/null
+++ b/regression-test/data/correctness_p0/test_merge_topn_offset.out
@@ -0,0 +1,10 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !merge_topn_off4lim3 --
+5 50
+
+-- !merge_topn_off2lim3 --
+3 30
+4 40
+5 50
+
+-- !merge_topn_off6lim3 --
diff --git
a/regression-test/suites/correctness_p0/test_merge_topn_offset.groovy
b/regression-test/suites/correctness_p0/test_merge_topn_offset.groovy
new file mode 100644
index 00000000000..fec6162d1d6
--- /dev/null
+++ b/regression-test/suites/correctness_p0/test_merge_topn_offset.groovy
@@ -0,0 +1,50 @@
+// 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_merge_topn_offset") {
+ // DORIS-26301: when MERGE_TOP_N merges a parent TopN that carries a
non-zero OFFSET into its
+ // child TopN, the merged limit must be clamped by (childLimit -
parentOffset). Otherwise rows
+ // beyond the child's limit leak through. Before the fix, an outer "LIMIT
3 OFFSET 4" over an
+ // inner "LIMIT 5" wrongly returned 3 rows (k=5,6,7) instead of the single
correct row (k=5).
+ sql "SET enable_nereids_planner=true"
+ sql "SET enable_fallback_to_original_planner=false"
+
+ def tableName = "test_merge_topn_offset"
+ sql """ DROP TABLE IF EXISTS ${tableName} """
+ sql """
+ CREATE TABLE ${tableName} (
+ k INT NOT NULL,
+ v INT NOT NULL
+ )
+ DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql """ INSERT INTO ${tableName} VALUES
+ (1, 10), (2, 20), (3, 30), (4, 40), (5, 50),
+ (6, 60), (7, 70), (8, 80), (9, 90), (10, 100) """
+
+ // MERGE_TOP_N is enabled by default; stacking an outer TopN with an
OFFSET over an inner TopN
+ // triggers it. The inner "ORDER BY k LIMIT 5" yields k = 1..5.
+
+ // outer offset (4) consumes 4 of the inner's 5 rows -> only k=5 remains
+ qt_merge_topn_off4lim3 """ SELECT * FROM (SELECT k, v FROM ${tableName}
ORDER BY k LIMIT 5) s ORDER BY k LIMIT 3 OFFSET 4 """
+ // outer offset (2) is within the inner limit -> k = 3,4,5
+ qt_merge_topn_off2lim3 """ SELECT * FROM (SELECT k, v FROM ${tableName}
ORDER BY k LIMIT 5) s ORDER BY k LIMIT 3 OFFSET 2 """
+ // outer offset (6) exceeds the inner limit (5) -> empty
+ qt_merge_topn_off6lim3 """ SELECT * FROM (SELECT k, v FROM ${tableName}
ORDER BY k LIMIT 5) s ORDER BY k LIMIT 3 OFFSET 6 """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]