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

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


The following commit(s) were added to refs/heads/master by this push:
     new 33a60b26ecc [fix](show) apply ORDER BY and LIMIT correctly to SHOW 
TABLETS (#65871) (#66116)
33a60b26ecc is described below

commit 33a60b26ecc084ea63975cbca62d7911ddd41dfe
Author: SudharsanK2308 <[email protected]>
AuthorDate: Tue Aug 11 09:24:30 2026 +0530

    [fix](show) apply ORDER BY and LIMIT correctly to SHOW TABLETS (#65871) 
(#66116)
    
    ### What problem does this PR solve?
    
    Issue Number: close #65871
    
    Related PR: #xxx
    
    Problem Summary:
    
    `SHOW TABLETS FROM tbl ORDER BY <col> LIMIT n` returns the wrong rows.
    
    While walking the partitions and materialized indexes of the table, the
    scan stops as soon as
    `offset + limit` rows have been collected, and the ORDER BY is applied
    *after* that early exit.
    The sort therefore only ever sees the prefix of the tablet set that
    happened to be collected
    first, so
    
    ```sql
    SHOW TABLETS FROM tbl ORDER BY LocalDataSize DESC LIMIT 10;
    ```
    
    returns the 10 largest tablets **of the first partitions/indexes that
    were scanned**, not the 10
    largest tablets of the table.
    
    This PR makes the early exit conditional on the absence of an explicit
    ORDER BY:
    
    - **With ORDER BY**: every tablet of the requested partitions is
    collected first, then sorted,
    then truncated to `limit` rows, and OFFSET is applied on that sorted
    result. The answer is now
      the globally correct top-N.
    - **Without ORDER BY**: unchanged. The scan still stops as soon as
    enough rows are gathered, so
    `LIMIT n` still returns a prefix of the scan, ordered by `(TabletId,
    ReplicaId)` among itself.
    The cheap path is deliberately kept for the common `SHOW TABLETS FROM
    tbl LIMIT n` on tables
      with a large number of tablets.
    
    Two smaller issues fixed along the way:
    
    - `sizeLimit` was a `long` narrowed by a plain `(int)` cast before
    `subList()`. A large limit
    (e.g. `LIMIT 3000000000`) wrapped around to a negative value and made
    the statement fail with
    `IndexOutOfBoundsException`. The value is now clamped to
    `Integer.MAX_VALUE`.
    - The "sort, then keep the first N rows" step is extracted into
    `org.apache.doris.common.util.SortAndLimit`, a small helper for the
    `List<List<Comparable>>`
    rows used by the SHOW / proc-dir style commands. It copies before
    sorting, so it never mutates
      the list handed in by the caller.
    
    Changed files:
    
    - `common/util/SortAndLimit.java` (new): sort-then-truncate helper.
    - `nereids/trees/plans/commands/ShowTabletsFromTableCommand.java`: skip
    the early exit when
      ORDER BY is given; always sort, then truncate, then apply OFFSET.
    - `common/util/SortAndLimitTest.java` (new): unit tests for the helper.
    - `regression-test/suites/show_p0/test_show_tablet.groovy`: coverage for
    ORDER BY / LIMIT /
      OFFSET on a 3-partition, 3-bucket table.
---
 .../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 79e7413ee3d..6c6ffca87ab 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
@@ -36,6 +36,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.mysql.privilege.PrivPredicate;
 import org.apache.doris.nereids.analyzer.UnboundSlot;
@@ -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