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

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


The following commit(s) were added to refs/heads/master by this push:
     new 8aeeb15997 [common] Add support for btree global prefix range scan 
(#8339)
8aeeb15997 is described below

commit 8aeeb15997eba0f5c7ea897883da47eb00bb86ee
Author: Arnav Balyan <[email protected]>
AuthorDate: Tue Jun 23 18:22:47 2026 -0700

    [common] Add support for btree global prefix range scan (#8339)
    
    - StartsWith on a btree global index returns every non null row today,
    so a prefix query like col like 'abc%' reads the whole segment and the
    lower level filters afterwards.
    - The index keys are sorted, so a prefix is a contiguous range and can
    be checked with the min and max values of the keys, similar to bitmap
    global index.
    - Add support for btree level pushdown which checks the min/max keys and
    prunes startwith values based on the btree values.
    
    ### Benchmark (Before vs After):
    High cardinality keys were generated, and query selects single prefix
    using the startswith. Performance was measured against a highly
    selective query (defined by the % match illustrated in the workload
    column).
    | Workload | Before | After | Speedup |
    | --- | --- | --- | --- |
    | 50K rows, 1% match | 2.73 ms | 0.085 ms | 32x |
    | 1M rows, 1% match | 42.8 ms | 0.857 ms | 50x |
    | 1M rows, 0.1% match | 42.2 ms | 0.061 ms | 697x |
---
 .../paimon/globalindex/SortedFileMetaSelector.java |  2 +-
 .../paimon/globalindex/btree/BTreeIndexReader.java | 19 ++++++++++++-
 .../globalindex/btree/AbstractIndexReaderTest.java | 32 ++++++++++++++++++++++
 3 files changed, 51 insertions(+), 2 deletions(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/globalindex/SortedFileMetaSelector.java
 
b/paimon-common/src/main/java/org/apache/paimon/globalindex/SortedFileMetaSelector.java
index 047f7b17a5..144b3195d1 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/globalindex/SortedFileMetaSelector.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/globalindex/SortedFileMetaSelector.java
@@ -276,7 +276,7 @@ public class SortedFileMetaSelector implements 
FunctionVisitor<Optional<List<Glo
                 .collect(Collectors.toList());
     }
 
-    protected static byte[] prefixUpperBound(byte[] prefix) {
+    public static byte[] prefixUpperBound(byte[] prefix) {
         for (int i = prefix.length - 1; i >= 0; i--) {
             int unsignedByte = prefix[i] & 0xFF;
             if (unsignedByte != 0xFF) {
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/BTreeIndexReader.java
 
b/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/BTreeIndexReader.java
index 92816e909e..e9cb833a68 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/BTreeIndexReader.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/globalindex/btree/BTreeIndexReader.java
@@ -23,6 +23,7 @@ import org.apache.paimon.fs.SeekableInputStream;
 import org.apache.paimon.globalindex.GlobalIndexIOMeta;
 import org.apache.paimon.globalindex.GlobalIndexResult;
 import org.apache.paimon.globalindex.KeySerializer;
+import org.apache.paimon.globalindex.SortedFileMetaSelector;
 import org.apache.paimon.globalindex.SortedIndexFileMeta;
 import org.apache.paimon.globalindex.io.GlobalIndexFileReader;
 import org.apache.paimon.io.cache.CacheManager;
@@ -250,7 +251,23 @@ public class BTreeIndexReader implements Closeable {
     }
 
     public Optional<GlobalIndexResult> visitStartsWith(Object literal) {
-        return createResult(this::allNonNullRows);
+        return createResult(
+                () -> {
+                    if (minKey == null) {
+                        return new RoaringNavigableMap64();
+                    }
+                    byte[] upperBound =
+                            SortedFileMetaSelector.prefixUpperBound(
+                                    keySerializer.serialize(literal));
+                    if (upperBound == null) {
+                        return rangeQuery(literal, maxKey, true, true);
+                    }
+                    return rangeQuery(
+                            literal,
+                            
keySerializer.deserialize(MemorySlice.wrap(upperBound)),
+                            true,
+                            false);
+                });
     }
 
     public Optional<GlobalIndexResult> visitEndsWith(Object literal) {
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/globalindex/btree/AbstractIndexReaderTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/globalindex/btree/AbstractIndexReaderTest.java
index b2d0fa2d7f..d2c280f1e1 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/globalindex/btree/AbstractIndexReaderTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/globalindex/btree/AbstractIndexReaderTest.java
@@ -44,6 +44,7 @@ import org.apache.paimon.types.CharType;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypeDefaultVisitor;
+import org.apache.paimon.types.DataTypeFamily;
 import org.apache.paimon.types.DateType;
 import org.apache.paimon.types.DecimalType;
 import org.apache.paimon.types.DoubleType;
@@ -275,6 +276,37 @@ public abstract class AbstractIndexReaderTest {
         }
     }
 
+    @TestTemplate
+    public void testStartsWith() throws Exception {
+        if (!dataType.is(DataTypeFamily.CHARACTER_STRING)) {
+            return;
+        }
+
+        FieldRef ref = new FieldRef(1, "testField", dataType);
+        try (GlobalIndexReader reader = prepareDataAndCreateReader()) {
+            Random random = new Random();
+            for (int i = 0; i < 5; i++) {
+                String value =
+                        ((BinaryString) 
data.get(random.nextInt(dataNum)).getKey()).toString();
+                String prefix = value.substring(0, 1 + 
random.nextInt(value.length()));
+                GlobalIndexResult result =
+                        reader.visitStartsWith(ref, 
BinaryString.fromString(prefix)).join().get();
+                assertResult(
+                        result, filter(obj -> ((BinaryString) 
obj).toString().startsWith(prefix)));
+            }
+
+            GlobalIndexResult all =
+                    reader.visitStartsWith(ref, 
BinaryString.fromString("")).join().get();
+            assertResult(all, filter(Objects::nonNull));
+
+            GlobalIndexResult none =
+                    reader.visitStartsWith(ref, 
BinaryString.fromString("zzz_no_such_prefix"))
+                            .join()
+                            .get();
+            Assertions.assertTrue(none.results().isEmpty());
+        }
+    }
+
     protected abstract GlobalIndexReader prepareDataAndCreateReader() throws 
Exception;
 
     protected GlobalIndexIOMeta writeData(List<Pair<Object, Long>> data) 
throws IOException {

Reply via email to