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 812dc99ea94 branch-4.1: [fix](show) apply ORDER BY and LIMIT correctly 
to SHOW TABLETS (#65871) #66116 (#66757)
812dc99ea94 is described below

commit 812dc99ea942f23dfd6cb407f9acf52fbf664c15
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Aug 18 09:57:19 2026 +0800

    branch-4.1: [fix](show) apply ORDER BY and LIMIT correctly to SHOW TABLETS 
(#65871) #66116 (#66757)
    
    Cherry-picked from #66116
    
    Co-authored-by: SudharsanK2308 
<[email protected]>
---
 .../org/apache/doris/common/util/SortAndLimit.java | 57 ++++++++++++++++
 .../commands/ShowTabletsFromTableCommand.java      | 67 ++++++++++--------
 .../apache/doris/common/util/SortAndLimitTest.java | 79 ++++++++++++++++++++++
 .../suites/show_p0/test_show_tablet.groovy         | 69 +++++++++++++++++++
 4 files changed, 242 insertions(+), 30 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/common/util/SortAndLimit.java 
b/fe/fe-core/src/main/java/org/apache/doris/common/util/SortAndLimit.java
new file mode 100644
index 00000000000..75fa9bc19c1
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/util/SortAndLimit.java
@@ -0,0 +1,57 @@
+// 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.common.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Utility for the common "sort, then truncate to the first N rows" pattern of 
the SHOW/proc-dir
+ * style commands that operate on {@code List<List<Comparable>>} rows (see 
{@link ListComparator}
+ * and {@link OrderByPair}).
+ */
+public class SortAndLimit {
+
+    private SortAndLimit() {
+    }
+
+    /**
+     * Sorts {@code rows} using {@code comparator} and returns a new list 
truncated to the first
+     * {@code sizeLimit} elements.
+     *
+     * <p>This method does NOT mutate the {@code rows} list passed in: it 
first copies the input
+     * into a new, mutable list and sorts that copy in place, so an immutable 
input list can be
+     * passed safely and the caller's original list/order is left untouched.
+     *
+     * @param rows       the rows to sort; not modified by this call
+     * @param comparator the comparator defining the sort order
+     * @param sizeLimit  the maximum number of rows to keep, counted from the 
start of the sorted
+     *                   result; {@link Optional#empty()} means "no limit" 
(return every row)
+     * @return a new list, sorted by {@code comparator} and truncated to at 
most {@code sizeLimit}
+     *         elements
+     */
+    public static List<List<Comparable>> sortAndLimit(List<List<Comparable>> 
rows,
+            ListComparator<List<Comparable>> comparator, Optional<Integer> 
sizeLimit) {
+        List<List<Comparable>> sorted = new ArrayList<>(rows);
+        sorted.sort(comparator);
+
+        int limit = sizeLimit.orElse(sorted.size());
+        return new ArrayList<>(sorted.subList(0, Math.min(limit, 
sorted.size())));
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowTabletsFromTableCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowTabletsFromTableCommand.java
index 350ccd91f53..491bc93b60c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowTabletsFromTableCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowTabletsFromTableCommand.java
@@ -34,6 +34,7 @@ import org.apache.doris.common.UserException;
 import org.apache.doris.common.proc.TabletsProcDir;
 import org.apache.doris.common.util.ListComparator;
 import org.apache.doris.common.util.OrderByPair;
+import org.apache.doris.common.util.SortAndLimit;
 import org.apache.doris.common.util.Util;
 import org.apache.doris.info.PartitionNamesInfo;
 import org.apache.doris.info.TableNameInfo;
@@ -57,9 +58,9 @@ import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 
 /**
  * ShowTabletsFromTableCommand
@@ -201,13 +202,16 @@ public class ShowTabletsFromTableCommand extends 
ShowCommand {
         OlapTable olapTable = 
db.getOlapTableOrAnalysisException(dbTableName.getTbl());
         olapTable.readLock();
         try {
-            long sizeLimit = -1;
-            if (offset > 0 && limit > 0) {
-                sizeLimit = offset + limit;
-            } else if (limit > 0) {
-                sizeLimit = limit;
+            // The parser passes limit = 0 when the statement carries no LIMIT 
clause
+            // (see LogicalPlanBuilder#visitShowTabletsFromTable), so only a 
positive limit
+            // bounds the result. sizeLimit is how many sorted rows have to be 
kept: the LIMIT
+            // rows plus the OFFSET rows that are skipped afterwards.
+            Optional<Integer> sizeLimit = Optional.empty();
+            if (limit > 0) {
+                long capped = limit + Math.max(offset, 0);
+                sizeLimit = Optional.of((int) Math.min(capped, 
Integer.MAX_VALUE));
             }
-            boolean stop = false;
+
             Collection<Partition> partitions = new ArrayList<Partition>();
             if (partitionNames != null) {
                 List<String> paNames = partitionNames.getPartitionNames();
@@ -223,6 +227,12 @@ public class ShowTabletsFromTableCommand extends 
ShowCommand {
             } else {
                 partitions = olapTable.getPartitions();
             }
+            // With an explicit ORDER BY every tablet has to be collected 
before the result can be
+            // truncated, otherwise the sort only sees an arbitrary prefix of 
the scan and returns
+            // the wrong rows -- the bug reported in #65871. Without ORDER BY 
the scan still stops
+            // as soon as enough rows are gathered, as it did before: LIMIT 
then returns a prefix
+            // of the scan, ordered by (tabletId, replicaId) among itself.
+            boolean stop = false;
             List<List<Comparable>> tabletInfos = new ArrayList<>();
             for (Partition partition : partitions) {
                 if (stop) {
@@ -232,37 +242,34 @@ public class ShowTabletsFromTableCommand extends 
ShowCommand {
                     TabletsProcDir procDir = new TabletsProcDir(olapTable, 
index);
                     tabletInfos.addAll(procDir.fetchComparableResult(
                             version, backendId, replicaState));
-                    if (sizeLimit > -1 && tabletInfos.size() >= sizeLimit) {
+                    if (orderByPairs == null && sizeLimit.isPresent() && 
tabletInfos.size() >= sizeLimit.get()) {
                         stop = true;
                         break;
                     }
                 }
             }
-            if (offset >= tabletInfos.size()) {
-                tabletInfos.clear();
+
+            ListComparator<List<Comparable>> comparator;
+            if (orderByPairs != null) {
+                // order by the keys given by the user
+                OrderByPair[] orderByPairArr = new 
OrderByPair[orderByPairs.size()];
+                comparator = new 
ListComparator<>(orderByPairs.toArray(orderByPairArr));
             } else {
-                // order by
-                ListComparator<List<Comparable>> comparator = null;
-                if (orderByPairs != null) {
-                    OrderByPair[] orderByPairArr = new 
OrderByPair[orderByPairs.size()];
-                    comparator = new 
ListComparator<>(orderByPairs.toArray(orderByPairArr));
-                } else {
-                    // order by tabletId, replicaId
-                    comparator = new ListComparator<>(0, 1);
-                }
-                Collections.sort(tabletInfos, comparator);
-                if (sizeLimit > -1) {
-                    tabletInfos = tabletInfos.subList((int) offset,
-                            Math.min((int) sizeLimit, tabletInfos.size()));
-                }
+                // order by tabletId, replicaId
+                comparator = new ListComparator<>(0, 1);
+            }
+            List<List<Comparable>> orderedTabletInfos = 
SortAndLimit.sortAndLimit(tabletInfos, comparator, sizeLimit);
 
-                for (List<Comparable> tabletInfo : tabletInfos) {
-                    List<String> oneTablet = new 
ArrayList<String>(tabletInfo.size());
-                    for (Comparable column : tabletInfo) {
-                        oneTablet.add(column.toString());
-                    }
-                    rows.add(oneTablet);
+            // If offset is beyond the end of the result, subList yields an 
empty list and no row
+            // is returned.
+            int resultOffset = (int) Math.min(offset, 
orderedTabletInfos.size());
+            for (List<Comparable> tabletInfo
+                    : orderedTabletInfos.subList(resultOffset, 
orderedTabletInfos.size())) {
+                List<String> oneTablet = new 
ArrayList<String>(tabletInfo.size());
+                for (Comparable column : tabletInfo) {
+                    oneTablet.add(column.toString());
                 }
+                rows.add(oneTablet);
             }
         } finally {
             olapTable.readUnlock();
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/util/SortAndLimitTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/common/util/SortAndLimitTest.java
new file mode 100644
index 00000000000..0b72552d72e
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/common/util/SortAndLimitTest.java
@@ -0,0 +1,79 @@
+// 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.common.util;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Optional;
+
+public class SortAndLimitTest {
+
+    private static final ListComparator<List<Comparable>> BY_FIRST_COLUMN = 
new ListComparator<>(0);
+
+    private static List<List<Comparable>> rows(Comparable... 
firstColumnValues) {
+        List<List<Comparable>> rows = Lists.newArrayList();
+        for (Comparable value : firstColumnValues) {
+            rows.add(Lists.newArrayList(value));
+        }
+        return rows;
+    }
+
+    private static List<Comparable> firstColumnOf(List<List<Comparable>> rows) 
{
+        List<Comparable> values = Lists.newArrayList();
+        for (List<Comparable> row : rows) {
+            values.add(row.get(0));
+        }
+        return values;
+    }
+
+    @Test
+    public void testEmptyLimitKeepsEveryRow() {
+        List<List<Comparable>> sorted = SortAndLimit.sortAndLimit(rows(3L, 1L, 
2L), BY_FIRST_COLUMN,
+                Optional.empty());
+        Assert.assertEquals(Lists.newArrayList(1L, 2L, 3L), 
firstColumnOf(sorted));
+    }
+
+    @Test
+    public void testLimitAppliesToTheSortedResult() {
+        // the two smallest values, not the first two rows of the input
+        List<List<Comparable>> sorted = SortAndLimit.sortAndLimit(rows(3L, 1L, 
2L), BY_FIRST_COLUMN,
+                Optional.of(2));
+        Assert.assertEquals(Lists.newArrayList(1L, 2L), firstColumnOf(sorted));
+    }
+
+    @Test
+    public void testLimitLargerThanInputIsClamped() {
+        List<List<Comparable>> sorted = SortAndLimit.sortAndLimit(rows(3L, 
1L), BY_FIRST_COLUMN,
+                Optional.of(100));
+        Assert.assertEquals(Lists.newArrayList(1L, 3L), firstColumnOf(sorted));
+    }
+
+    @Test
+    public void testInputIsNotModified() {
+        List<List<Comparable>> input = ImmutableList.<List<Comparable>>of(
+                ImmutableList.<Comparable>of(3L),
+                ImmutableList.<Comparable>of(1L));
+        List<List<Comparable>> sorted = SortAndLimit.sortAndLimit(input, 
BY_FIRST_COLUMN, Optional.of(1));
+        Assert.assertEquals(Lists.newArrayList(1L), firstColumnOf(sorted));
+        Assert.assertEquals(Lists.newArrayList(3L, 1L), firstColumnOf(input));
+    }
+}
diff --git a/regression-test/suites/show_p0/test_show_tablet.groovy 
b/regression-test/suites/show_p0/test_show_tablet.groovy
index abe54d7e93f..51e5e4fae46 100644
--- a/regression-test/suites/show_p0/test_show_tablet.groovy
+++ b/regression-test/suites/show_p0/test_show_tablet.groovy
@@ -59,4 +59,73 @@ suite("test_show_tablet") {
     } else {
         assertTrue(1 == 2)
     }
+
+    // An explicit ORDER BY must be applied to the whole tablet set of the 
table, not to the
+    // prefix that happens to be collected first while walking partitions and 
indexes.
+    sql """drop table if exists show_tablets_multi_part_t;"""
+    sql """create table show_tablets_multi_part_t (
+                id INT,
+                username VARCHAR(20)
+            )
+            DUPLICATE KEY(id)
+            PARTITION BY RANGE(id) (
+                PARTITION p1 VALUES LESS THAN (10),
+                PARTITION p2 VALUES LESS THAN (20),
+                PARTITION p3 VALUES LESS THAN (30)
+            )
+            DISTRIBUTED BY HASH(id) BUCKETS 3
+            PROPERTIES (
+                "replication_num" = "1"
+            );"""
+
+    def allTablets = sql """SHOW TABLETS FROM show_tablets_multi_part_t"""
+    logger.info("all tablets: " + allTablets.toString())
+    // 3 partitions * 3 buckets, one row per replica
+    assertTrue(allTablets.size() >= 9)
+
+    def allIds = allTablets.collect { it[0] as long }
+    def ascIds = new ArrayList(allIds)
+    Collections.sort(ascIds)
+    def descIds = new ArrayList(ascIds)
+    Collections.reverse(descIds)
+
+    // without ORDER BY the rows come back ordered by (TabletId, ReplicaId)
+    assertEquals(ascIds, allIds)
+
+    // ORDER BY without LIMIT returns every tablet
+    res = sql """SHOW TABLETS FROM show_tablets_multi_part_t ORDER BY TabletId 
DESC"""
+    assertEquals(descIds, res.collect { it[0] as long })
+
+    // ORDER BY ... LIMIT returns the globally largest tablet ids, not the 
largest ones
+    // of the first partition scanned
+    res = sql """SHOW TABLETS FROM show_tablets_multi_part_t ORDER BY TabletId 
DESC LIMIT 3"""
+    assertEquals(descIds.subList(0, 3), res.collect { it[0] as long })
+
+    // OFFSET is applied after sorting
+    res = sql """SHOW TABLETS FROM show_tablets_multi_part_t ORDER BY TabletId 
DESC LIMIT 2, 3"""
+    assertEquals(descIds.subList(2, 5), res.collect { it[0] as long })
+
+    // Without ORDER BY the scan stops as soon as enough rows are gathered, so 
LIMIT returns a
+    // prefix of the scan rather than the globally smallest tablet ids. Only 
the row count and
+    // the ordering inside the returned prefix are guaranteed.
+    def assertPrefixOfTable = { rows, expectedSize ->
+        assertEquals(expectedSize, rows.size())
+        def ids = rows.collect { it[0] as long }
+        def sortedIds = new ArrayList(ids)
+        Collections.sort(sortedIds)
+        assertEquals(sortedIds, ids)
+        assertTrue(allIds.containsAll(ids))
+    }
+
+    res = sql """SHOW TABLETS FROM show_tablets_multi_part_t LIMIT 3"""
+    assertPrefixOfTable(res, 3)
+
+    res = sql """SHOW TABLETS FROM show_tablets_multi_part_t LIMIT 2, 3"""
+    assertPrefixOfTable(res, 3)
+
+    // an offset past the end of the result yields no row
+    res = sql """SHOW TABLETS FROM show_tablets_multi_part_t LIMIT 
${allTablets.size()}, 3"""
+    assertTrue(res.isEmpty())
+
+    sql """drop table if exists show_tablets_multi_part_t;"""
 }


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

Reply via email to