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

lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 143107ea perf(parquet): add opt-in parsed offset index caching (#314)
143107ea is described below

commit 143107ea4db8a11f9fd5966e661b7b0ab97d66dc
Author: wangyong9999 <[email protected]>
AuthorDate: Fri Sep 18 15:27:52 2026 +0800

    perf(parquet): add opt-in parsed offset index caching (#314)
---
 benchmark/parquet_format_benchmark.cpp             | 110 ++++++++++++++++++---
 docs/source/user_guide/parquet_metadata_cache.rst  |  37 ++++++-
 src/paimon/format/parquet/file_reader_wrapper.cpp  |  34 ++++++-
 src/paimon/format/parquet/file_reader_wrapper.h    |   4 +-
 .../format/parquet/file_reader_wrapper_test.cpp    |  46 ++++++++-
 .../parquet/page_filtered_row_group_reader.cpp     |  22 ++---
 .../format/parquet/parquet_file_batch_reader.cpp   |  10 +-
 .../parquet/parquet_file_batch_reader_test.cpp     |  31 ++++++
 src/paimon/format/parquet/parquet_format_defs.h    |   4 +
 9 files changed, 264 insertions(+), 34 deletions(-)

diff --git a/benchmark/parquet_format_benchmark.cpp 
b/benchmark/parquet_format_benchmark.cpp
index 3823ac3b..eff80b39 100644
--- a/benchmark/parquet_format_benchmark.cpp
+++ b/benchmark/parquet_format_benchmark.cpp
@@ -74,6 +74,8 @@
 #include "paimon/status.h"
 #include "paimon/testing/utils/testharness.h"
 #include "paimon/utils/roaring_bitmap32.h"
+#include "parquet/file_reader.h"
+#include "parquet/page_index.h"
 
 namespace {
 
@@ -551,8 +553,9 @@ Result<int64_t> WriteParquetFile(const 
std::shared_ptr<FileSystem>& fs, const st
                                  const std::shared_ptr<arrow::Schema>& schema,
                                  const 
std::vector<std::shared_ptr<arrow::Array>>& batches,
                                  const std::map<std::string, std::string>& 
options,
-                                 const std::string& compression) {
-    ParquetWriterBuilder writer_builder(schema, kWriteBatchSize, options);
+                                 const std::string& compression,
+                                 int32_t write_batch_size = kWriteBatchSize) {
+    ParquetWriterBuilder writer_builder(schema, write_batch_size, options);
     PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<OutputStream> out, fs->Create(path, 
/*overwrite=*/true));
     PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FormatWriter> writer,
                            writer_builder.Build(out, compression));
@@ -704,8 +707,9 @@ class ReadFixture {
  public:
     ReadFixture(const std::string& file_name, const 
std::shared_ptr<arrow::Schema>& schema,
                 const BatchFactory& make_batch,
-                const std::map<std::string, std::string>& write_options = {}) {
-        status_ = Build(file_name, schema, make_batch, write_options);
+                const std::map<std::string, std::string>& write_options = {},
+                int32_t write_batch_size = kWriteBatchSize) {
+        status_ = Build(file_name, schema, make_batch, write_options, 
write_batch_size);
     }
 
     const Status& status() const {
@@ -731,17 +735,19 @@ class ReadFixture {
  private:
     Status Build(const std::string& file_name, const 
std::shared_ptr<arrow::Schema>& schema,
                  const BatchFactory& make_batch,
-                 const std::map<std::string, std::string>& write_options) {
+                 const std::map<std::string, std::string>& write_options,
+                 int32_t write_batch_size) {
         PAIMON_ASSIGN_OR_RAISE(env_, BenchmarkEnv::Create());
         path_ = env_->PathOf(file_name);
         PAIMON_ASSIGN_OR_RAISE(std::vector<std::shared_ptr<arrow::Array>> 
batches,
                                MakeBatches(schema, make_batch, kRowsPerBatch));
         std::map<std::string, std::string> options = write_options;
-        options[paimon::parquet::PARQUET_PAGE_SIZE] = 
std::to_string(kPageSizeBytes);
-        options[paimon::parquet::PARQUET_WRITE_MAX_ROW_GROUP_LENGTH] =
-            std::to_string(kRowGroupLength);
-        PAIMON_ASSIGN_OR_RAISE(file_bytes_, WriteParquetFile(env_->fs(), 
path_, schema, batches,
-                                                             options, 
kDefaultCompression));
+        options.emplace(paimon::parquet::PARQUET_PAGE_SIZE, 
std::to_string(kPageSizeBytes));
+        options.emplace(paimon::parquet::PARQUET_WRITE_MAX_ROW_GROUP_LENGTH,
+                        std::to_string(kRowGroupLength));
+        PAIMON_ASSIGN_OR_RAISE(file_bytes_,
+                               WriteParquetFile(env_->fs(), path_, schema, 
batches, options,
+                                                kDefaultCompression, 
write_batch_size));
         return Status::OK();
     }
 
@@ -751,12 +757,17 @@ class ReadFixture {
     Status status_;
 };
 
+std::map<std::string, std::unique_ptr<ReadFixture>>& ReadFixtures() {
+    static std::map<std::string, std::unique_ptr<ReadFixture>> fixtures;
+    return fixtures;
+}
+
 // Built on first use, so a case excluded by --benchmark_filter never pays to 
write its file.
 // google/benchmark may report from a different thread than it registered on, 
hence the lock.
 const ReadFixture& GetFixture(const std::string& key,
                               const 
std::function<std::unique_ptr<ReadFixture>()>& build) {
     static std::mutex mutex;
-    static std::map<std::string, std::unique_ptr<ReadFixture>> fixtures;
+    auto& fixtures = ReadFixtures();
     std::lock_guard<std::mutex> guard(mutex);
     std::unique_ptr<ReadFixture>& fixture = fixtures[key];
     if (!fixture) {
@@ -1229,6 +1240,71 @@ void RunReadBenchmark(::benchmark::State& state, const 
ReadFixture& fixture,
     state.counters["batches"] = 
::benchmark::Counter(static_cast<double>(stats.batches));
 }
 
+// Sparse lookup into a large row group of 1 KiB values. Page size controls 
how much
+// OffsetIndex parsing each lookup repeats; full scan is the 
decoding-dominated control.
+void BM_ParquetRead_OffsetIndexCache(::benchmark::State& state) {
+    const int64_t page_size = state.range(0);
+    const bool cache = state.range(1) != 0;
+    const bool full_scan = state.range(2) != 0;
+    const std::string key = "offset_index_" + std::to_string(page_size);
+    const auto& fixture = GetFixture(key, [key, page_size] {
+        return std::make_unique<ReadFixture>(
+            key + ".parquet", StringSchema(),
+            [](const std::shared_ptr<arrow::Schema>& schema, int64_t offset,
+               int64_t rows) -> Result<std::shared_ptr<arrow::Array>> {
+                arrow::StringBuilder builder;
+                for (int64_t row = offset; row < offset + rows; ++row) {
+                    std::string value = std::to_string(row);
+                    uint64_t random = static_cast<uint64_t>(row) + 1;
+                    while (value.size() < 1024) {
+                        random ^= random << 13;
+                        random ^= random >> 7;
+                        random ^= random << 17;
+                        value.push_back('a' + random % 26);
+                    }
+                    PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Append(value));
+                }
+                std::shared_ptr<arrow::Array> values;
+                PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Finish(&values));
+                return MakeStructArray(schema->fields(), {values});
+            },
+            std::map<std::string, std::string>{
+                {paimon::parquet::PARQUET_PAGE_SIZE, 
std::to_string(page_size)},
+                {paimon::parquet::PARQUET_WRITE_MAX_ROW_GROUP_LENGTH, 
std::to_string(kRowsPerFile)},
+                {paimon::parquet::PARQUET_ENABLE_DICTIONARY, "false"}},
+            /*write_batch_size=*/16);
+    });
+    if (FailBenchmark(state, fixture.status())) {
+        return;
+    }
+    auto page_count = [&]() -> Result<int64_t> {
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<InputStream> input,
+                               fixture.fs()->Open(fixture.path()));
+        auto stream = std::make_shared<ArrowInputStreamAdapter>(input, 
fixture.file_bytes(),
+                                                                
fixture.arrow_pool());
+        auto reader = ::parquet::ParquetFileReader::Open(stream);
+        return reader->GetPageIndexReader()
+            ->RowGroup(0)
+            ->GetOffsetIndex(0)
+            ->page_locations()
+            .size();
+    }();
+    if (FailBenchmark(state, page_count.status())) {
+        return;
+    }
+    state.counters["pages"] = 
::benchmark::Counter(static_cast<double>(page_count.value()));
+    std::optional<RoaringBitmap32> bitmap;
+    if (!full_scan) {
+        bitmap.emplace();
+        bitmap->Add(kRowsPerFile / 2);
+    }
+    RunReadBenchmark(
+        state, fixture, StringSchema(), nullptr, bitmap,
+        {{paimon::parquet::PARQUET_READ_ENABLE_OFFSET_INDEX_CACHE, cache ? 
"true" : "false"},
+         {paimon::parquet::PARQUET_READ_BITMAP_ROW_RANGE_REFINING_STRATEGY, 
"trim"}},
+        kReadBatchSize, full_scan ? RowExpectation{} : RowExpectation{1, 1});
+}
+
 void BM_ParquetRead_FullScan(::benchmark::State& state) {
     RunReadBenchmark(state, FlatFixture(), FlatSchema(), /*predicate=*/nullptr,
                      /*selection_bitmap=*/std::nullopt, /*options=*/{}, 
kReadBatchSize);
@@ -1496,6 +1572,16 @@ BENCHMARK_CAPTURE(BM_ParquetWrite_Compression, zstd, 
"zstd")
     ->Unit(benchmark::kMillisecond)
     ->UseRealTime();
 
+BENCHMARK(BM_ParquetRead_OffsetIndexCache)
+    ->ArgNames({"page_bytes", "cache", "full_scan"})
+    ->Args({16384, 0, 0})
+    ->Args({16384, 1, 0})
+    ->Args({1048576, 0, 0})
+    ->Args({1048576, 1, 0})
+    ->Args({16384, 0, 1})
+    ->Args({16384, 1, 1})
+    ->Unit(benchmark::kMillisecond)
+    ->UseRealTime();
 
BENCHMARK(BM_ParquetRead_FullScan)->Unit(benchmark::kMillisecond)->UseRealTime();
 BENCHMARK_CAPTURE(BM_ParquetRead_Projection, id, "id")
     ->Unit(benchmark::kMillisecond)
@@ -1590,6 +1676,8 @@ int main(int argc, char** argv) {
         return 1;
     }
     ::benchmark::RunSpecifiedBenchmarks();
+    // Delete temporary files before filesystem singletons (including IOHook) 
are destroyed.
+    ReadFixtures().clear();
     ::benchmark::Shutdown();
     return g_failed.load() ? 1 : 0;
 }
diff --git a/docs/source/user_guide/parquet_metadata_cache.rst 
b/docs/source/user_guide/parquet_metadata_cache.rst
index 804f8432..514f1b64 100644
--- a/docs/source/user_guide/parquet_metadata_cache.rst
+++ b/docs/source/user_guide/parquet_metadata_cache.rst
@@ -37,8 +37,9 @@ This optimization is useful when the same Parquet files are 
opened repeatedly
 in the same process, for example repeated ``get`` or ``scan`` requests over the
 same snapshot. On a cache hit, the read path avoids reading the Parquet footer
 bytes from the filesystem again. Paimon C++ still parses the cached footer 
bytes
-into ``parquet::FileMetaData`` for each reader open. Data pages, page indexes,
-and column chunks are still read from the file as usual.
+into ``parquet::FileMetaData`` for each reader open. ColumnIndex and 
OffsetIndex
+bytes also use ``CacheKind::DATA_FILE_FOOTER`` with their actual positions and
+lengths. Data pages and column chunks are not stored in this metadata cache.
 
 Configuration
 -------------
@@ -100,6 +101,38 @@ Passing ``nullptr`` or omitting ``WithCache()`` leaves 
Parquet metadata caching
 disabled. If a file URI cannot be obtained, Paimon C++ also bypasses the cache
 and opens the Parquet file normally.
 
+Reader-Local Page Indexes
+-------------------------
+
+Set ``parquet.read.enable-offset-index-cache=true`` to reuse parsed OffsetIndex
+objects during bitmap trimming, page-range planning and filtered decoding 
within
+one file reader. The option defaults to ``false``; it does not require
+``WithCache()``. Enable it only after measuring the CPU/memory trade-off for 
the
+workload, especially when projecting many columns.
+
+These objects are not stored in the caller's shared ``Cache``. They are 
released
+with their owning row-group index reader; the existing limit of 1,024 retained
+row-group readers is a count limit, not a parsed-index byte budget. Restricted
+predicate index readers remain separate so their column hints do not restrict
+later projected-column reads.
+
+Parsed page locations require approximately
+``retained row groups x accessed columns x pages per column x 
sizeof(PageLocation)``
+bytes, plus vector/map/object overhead, in addition to serialized index 
buffers.
+For example, with a 24-byte ``PageLocation``, 1,024 retained row groups, 10 
accessed
+columns and 1,000 pages per column require about 234 MiB for page locations 
alone.
+There is no fixed byte upper bound; memory scales with the file's index sizes.
+
+This is independent of ``ReadAheadCache`` and its per-file ``FileBlockCache``,
+which reuse bytes rather than parsed objects. That block cache survives resets
+of the prefetch plan, but does not provide reuse across independent file-cache
+lifetimes. This optimization introduces no data-cache option or shared data 
cache.
+
+Filesystem or read-ahead byte caching does not replace this optimization: Arrow
+deserializes the cached index bytes again on every ``GetOffsetIndex`` call.
+With the option disabled, Paimon retains the existing byte-buffer reuse without
+retaining parsed OffsetIndex objects.
+
 Future Optimizations
 --------------------
 
diff --git a/src/paimon/format/parquet/file_reader_wrapper.cpp 
b/src/paimon/format/parquet/file_reader_wrapper.cpp
index 19c35310..56128328 100644
--- a/src/paimon/format/parquet/file_reader_wrapper.cpp
+++ b/src/paimon/format/parquet/file_reader_wrapper.cpp
@@ -45,6 +45,33 @@ namespace paimon::parquet {
 
 namespace {
 
+// Planning and decoding repeatedly request the same offsets. Keep parsed 
indexes
+// with the row-group reader, not in the cross-reader byte cache.
+class CachedRowGroupPageIndexReader : public 
::parquet::RowGroupPageIndexReader {
+ public:
+    explicit CachedRowGroupPageIndexReader(
+        std::shared_ptr<::parquet::RowGroupPageIndexReader> reader)
+        : reader_(std::move(reader)) {}
+
+    std::shared_ptr<::parquet::ColumnIndex> GetColumnIndex(int32_t i) override 
{
+        return reader_->GetColumnIndex(i);
+    }
+
+    std::shared_ptr<::parquet::OffsetIndex> GetOffsetIndex(int32_t i) override 
{
+        auto found = offset_indexes_.find(i);
+        if (found != offset_indexes_.end()) {
+            return found->second;
+        }
+        auto index = reader_->GetOffsetIndex(i);
+        offset_indexes_.emplace(i, index);
+        return index;
+    }
+
+ private:
+    std::shared_ptr<::parquet::RowGroupPageIndexReader> reader_;
+    std::map<int32_t, std::shared_ptr<::parquet::OffsetIndex>> offset_indexes_;
+};
+
 // Merge overlapping or adjacent ReadRanges into a minimal set of 
non-overlapping ranges.
 // PreBufferRanges requires non-overlapping ranges, so this is necessary when 
combining
 // ranges from multiple sources (page-level ranges, column chunk ranges, etc.).
@@ -87,7 +114,7 @@ std::vector<::arrow::io::ReadRange> MergeOverlappingRanges(
 
 Result<std::unique_ptr<FileReaderWrapper>> FileReaderWrapper::Create(
     std::unique_ptr<::parquet::arrow::FileReader>&& file_reader, int64_t 
batch_size,
-    std::shared_ptr<::arrow::MemoryPool> pool) {
+    bool enable_offset_index_cache, std::shared_ptr<::arrow::MemoryPool> pool) 
{
     try {
         if (file_reader == nullptr) {
             return Status::Invalid("file reader wrapper create failed. file 
reader is nullptr");
@@ -111,6 +138,7 @@ Result<std::unique_ptr<FileReaderWrapper>> 
FileReaderWrapper::Create(
             
arrow::internal::Iota(file_reader->parquet_reader()->metadata()->num_columns());
         auto file_reader_wrapper = std::unique_ptr<FileReaderWrapper>(new 
FileReaderWrapper(
             std::move(file_reader), all_row_group_ranges, num_rows, 
batch_size, pool));
+        file_reader_wrapper->enable_offset_index_cache_ = 
enable_offset_index_cache;
         std::vector<TargetRowGroup> all_target_row_groups;
         for (int32_t i = 0; i < file_reader_wrapper->GetNumberOfRowGroups(); 
i++) {
             all_target_row_groups.emplace_back(/*rg_index=*/i, 
/*is_partially_matched=*/false,
@@ -341,6 +369,10 @@ std::shared_ptr<::parquet::RowGroupPageIndexReader> 
FileReaderWrapper::GetRowGro
     auto page_index_reader = GetPageIndexReader();
     if (page_index_reader) {
         row_group_page_index_reader = 
page_index_reader->RowGroup(row_group_index);
+        if (row_group_page_index_reader && enable_offset_index_cache_) {
+            row_group_page_index_reader =
+                
std::make_shared<CachedRowGroupPageIndexReader>(row_group_page_index_reader);
+        }
     }
 
     // To avoid OOM, limit the number of row group page index readers cached 
in memory.
diff --git a/src/paimon/format/parquet/file_reader_wrapper.h 
b/src/paimon/format/parquet/file_reader_wrapper.h
index 02e6ae5b..e9edca16 100644
--- a/src/paimon/format/parquet/file_reader_wrapper.h
+++ b/src/paimon/format/parquet/file_reader_wrapper.h
@@ -60,7 +60,7 @@ class FileReaderWrapper {
 
     static Result<std::unique_ptr<FileReaderWrapper>> Create(
         std::unique_ptr<::parquet::arrow::FileReader>&& reader, int64_t 
batch_size,
-        std::shared_ptr<arrow::MemoryPool> pool);
+        bool enable_offset_index_cache, std::shared_ptr<arrow::MemoryPool> 
pool);
 
     /// Seek to the specified row number.
     /// @param row_number The row to seek to (must be at a row group boundary).
@@ -228,6 +228,8 @@ class FileReaderWrapper {
     // Track pre-buffered ranges so we can wait on destruction
     std::vector<::arrow::io::ReadRange> prebuffered_ranges_;
 
+    bool enable_offset_index_cache_ = false;
+
     // Arrow caches the file-level PageIndexReader, but RowGroup() creates a 
new reader each time.
     // Keep one reader per row group so its page-index buffers are shared by 
all read stages.
     std::map<int32_t, std::shared_ptr<::parquet::RowGroupPageIndexReader>>
diff --git a/src/paimon/format/parquet/file_reader_wrapper_test.cpp 
b/src/paimon/format/parquet/file_reader_wrapper_test.cpp
index e2335107..74b6bb8e 100644
--- a/src/paimon/format/parquet/file_reader_wrapper_test.cpp
+++ b/src/paimon/format/parquet/file_reader_wrapper_test.cpp
@@ -187,13 +187,16 @@ class FileReaderWrapperTest : public ::testing::Test {
     }
 
     Result<std::unique_ptr<FileReaderWrapper>> PrepareReaderWrapper(
-        const std::string& file_path, int64_t wrapper_batch_size = 0) {
+        const std::string& file_path, int64_t wrapper_batch_size = 0,
+        bool enable_offset_index_cache = false) {
         PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<InputStream> in, 
fs_->Open(file_path));
-        return PrepareReaderWrapperOnStream(std::move(in), wrapper_batch_size);
+        return PrepareReaderWrapperOnStream(std::move(in), wrapper_batch_size,
+                                            enable_offset_index_cache);
     }
 
     Result<std::unique_ptr<FileReaderWrapper>> PrepareReaderWrapperOnStream(
-        std::shared_ptr<InputStream> in, int64_t wrapper_batch_size = 0) {
+        std::shared_ptr<InputStream> in, int64_t wrapper_batch_size = 0,
+        bool enable_offset_index_cache = false) {
         PAIMON_ASSIGN_OR_RAISE(int64_t file_length, in->Length());
         auto input_stream =
             std::make_unique<ArrowInputStreamAdapter>(std::move(in), 
file_length, arrow_pool_);
@@ -212,7 +215,8 @@ class FileReaderWrapperTest : public ::testing::Test {
         
PAIMON_RETURN_NOT_OK_FROM_ARROW(file_reader_builder.memory_pool(arrow_pool_.get())
                                             ->properties(arrow_reader_props)
                                             ->Build(&file_reader));
-        return FileReaderWrapper::Create(std::move(file_reader), 
wrapper_batch_size, arrow_pool_);
+        return FileReaderWrapper::Create(std::move(file_reader), 
wrapper_batch_size,
+                                         enable_offset_index_cache, 
arrow_pool_);
     }
 
     void PrepareParquetFile(const std::string& file_path, int32_t row_count,
@@ -274,6 +278,7 @@ TEST_F(FileReaderWrapperTest, EmptyFile) {
 TEST_F(FileReaderWrapperTest, NullFileReader) {
     ASSERT_NOK_WITH_MSG(FileReaderWrapper::Create(nullptr,
                                                   /*batch_size=*/0,
+                                                  
/*enable_offset_index_cache=*/false,
                                                   /*pool=*/arrow_pool_),
                         "file reader wrapper create failed. file reader is 
nullptr");
 }
@@ -311,6 +316,39 @@ TEST_F(FileReaderWrapperTest, 
PredicateReadsOnlyItsPageIndexesAndKeepsPayloadRea
     ASSERT_FALSE(end);
 }
 
+TEST_F(FileReaderWrapperTest, ReusesParsedOffsetIndexesWithinFileReader) {
+    std::string file_path = PathUtil::JoinPath(dir_->Str(), 
"parsed-index.parquet");
+    PrepareParquetFile(file_path, /*row_count=*/2000, 
/*enable_page_index=*/true);
+    ASSERT_OK_AND_ASSIGN(auto uncached_reader, 
PrepareReaderWrapper(file_path));
+    auto uncached = uncached_reader->GetRowGroupPageIndexReader(0);
+    auto first_uncached = uncached->GetOffsetIndex(0);
+    ASSERT_NE(first_uncached, uncached->GetOffsetIndex(0));
+    ASSERT_OK_AND_ASSIGN(auto reader, PrepareReaderWrapper(file_path, 0, 
true));
+    std::weak_ptr<::parquet::OffsetIndex> released_index;
+    {
+        auto first = reader->GetRowGroupPageIndexReader(0);
+        auto second = reader->GetRowGroupPageIndexReader(1);
+        ASSERT_TRUE(first);
+        ASSERT_TRUE(second);
+        ASSERT_NE(first, second);
+        for (int32_t col = 0; col < 3; ++col) {
+            auto offset = first->GetOffsetIndex(col);
+            auto column = first->GetColumnIndex(col);
+            ASSERT_TRUE(offset);
+            ASSERT_TRUE(column);
+            ASSERT_EQ(offset, first->GetOffsetIndex(col));
+            ASSERT_NE(offset, second->GetOffsetIndex(col));
+            released_index = offset;
+        }
+        ASSERT_THROW(first->GetOffsetIndex(-1), ::parquet::ParquetException);
+        ASSERT_THROW(first->GetColumnIndex(100), ::parquet::ParquetException);
+        ASSERT_TRUE(first->GetOffsetIndex(0));
+    }
+    ASSERT_FALSE(released_index.expired());
+    reader.reset();
+    ASSERT_TRUE(released_index.expired());
+}
+
 TEST_F(FileReaderWrapperTest, Simple) {
     std::string file_path = PathUtil::JoinPath(dir_->Str(), "test.parquet");
     PrepareParquetFile(file_path, /*row_count=*/5500);
diff --git a/src/paimon/format/parquet/page_filtered_row_group_reader.cpp 
b/src/paimon/format/parquet/page_filtered_row_group_reader.cpp
index 78f756fa..b712e54d 100644
--- a/src/paimon/format/parquet/page_filtered_row_group_reader.cpp
+++ b/src/paimon/format/parquet/page_filtered_row_group_reader.cpp
@@ -302,10 +302,10 @@ Result<std::shared_ptr<arrow::ChunkedArray>> 
PageFilteredRowGroupReader::ReadFil
     int64_t row_group_row_count, ::parquet::arrow::FileReader* 
arrow_file_reader) {
     // Factory: set a direct data page read plan on every leaf (per-leaf 
OffsetIndex).
     // The plan lets Arrow jump over unselected page headers as well as page 
bodies.
-    auto factory =
-        [row_group_index, &rg_page_index_reader, &row_ranges, 
row_group_row_count](
-            int col_idx,
-            ::parquet::ParquetFileReader* reader) -> 
::parquet::arrow::FileColumnIterator* {
+    std::unordered_set<int> direct_read_columns;
+    auto factory = [row_group_index, &rg_page_index_reader, &row_ranges, 
row_group_row_count,
+                    &direct_read_columns](int col_idx, 
::parquet::ParquetFileReader* reader)
+        -> ::parquet::arrow::FileColumnIterator* {
         bool has_data_page_read_plan = false;
         int64_t first_data_page_offset = 0;
         std::vector<::parquet::DataPageReadPlanEntry> data_pages;
@@ -320,6 +320,7 @@ Result<std::shared_ptr<arrow::ChunkedArray>> 
PageFilteredRowGroupReader::ReadFil
                     first_data_page_offset = plan->first_data_page_offset;
                     data_pages = std::move(plan->data_pages);
                     has_data_page_read_plan = true;
+                    direct_read_columns.insert(col_idx);
                 }
             }
         }
@@ -359,15 +360,12 @@ Result<std::shared_ptr<arrow::ChunkedArray>> 
PageFilteredRowGroupReader::ReadFil
             auto row_group_metadata =
                 
arrow_file_reader->parquet_reader()->metadata()->RowGroup(row_group_index);
             column_chunk = row_group_metadata->ColumnChunk(col_idx);
-            if (rg_page_index_reader) {
+            if (direct_read_columns.count(col_idx)) {
                 auto offset_index = 
rg_page_index_reader->GetOffsetIndex(col_idx);
-                if (offset_index && MakeDataPageReadPlan(row_ranges, 
offset_index, *column_chunk,
-                                                         row_group_row_count)) 
{
-                    auto [compressed, total] =
-                        ComputeCompressedRowRanges(row_ranges, offset_index, 
row_group_row_count);
-                    effective_ranges = std::move(compressed);
-                    effective_total = total;
-                }
+                auto [compressed, total] =
+                    ComputeCompressedRowRanges(row_ranges, offset_index, 
row_group_row_count);
+                effective_ranges = std::move(compressed);
+                effective_total = total;
             }
         }
 
diff --git a/src/paimon/format/parquet/parquet_file_batch_reader.cpp 
b/src/paimon/format/parquet/parquet_file_batch_reader.cpp
index 76f0df13..faa18c3e 100644
--- a/src/paimon/format/parquet/parquet_file_batch_reader.cpp
+++ b/src/paimon/format/parquet/parquet_file_batch_reader.cpp
@@ -321,9 +321,13 @@ Result<std::unique_ptr<ParquetFileBatchReader>> 
ParquetFileBatchReader::Create(
         
PAIMON_RETURN_NOT_OK_FROM_ARROW(file_reader_builder.memory_pool(pool.get())
                                             
->properties(arrow_reader_properties)
                                             ->Build(&file_reader));
-        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileReaderWrapper> reader,
-                               
FileReaderWrapper::Create(std::move(file_reader),
-                                                         
static_cast<int64_t>(batch_size), pool));
+        PAIMON_ASSIGN_OR_RAISE(bool enable_offset_index_cache,
+                               OptionsUtils::GetValueFromMap<bool>(
+                                   options, 
PARQUET_READ_ENABLE_OFFSET_INDEX_CACHE, false));
+        PAIMON_ASSIGN_OR_RAISE(
+            std::unique_ptr<FileReaderWrapper> reader,
+            FileReaderWrapper::Create(std::move(file_reader), 
static_cast<int64_t>(batch_size),
+                                      enable_offset_index_cache, pool));
         // Arrow silently ignores set_read_dictionary for leaves it cannot 
read as dictionaries,
         // so take the columns it really emits that way from the schema it 
just derived.
         std::set<std::string> dictionary_fields;
diff --git a/src/paimon/format/parquet/parquet_file_batch_reader_test.cpp 
b/src/paimon/format/parquet/parquet_file_batch_reader_test.cpp
index 3c1875ae..f25e0527 100644
--- a/src/paimon/format/parquet/parquet_file_batch_reader_test.cpp
+++ b/src/paimon/format/parquet/parquet_file_batch_reader_test.cpp
@@ -427,6 +427,37 @@ TEST_F(ParquetFileBatchReaderTest, 
TestCachedFooterKeepsAllocatorAliveUntilEvict
     ASSERT_TRUE(cached_pool.expired());
 }
 
+TEST_F(ParquetFileBatchReaderTest, TestOffsetIndexCacheOption) {
+    WriteArray(file_path_, struct_array_, schema_, 1, false, 3, 1);
+    auto projection = arrow::schema({schema_->GetFieldByName("f4")});
+    auto predicate = PredicateBuilder::Equal(0, "f4", FieldType::INT, 
Literal(300002));
+    for (const std::string setting : {"", "false", "true", "invalid"}) {
+        SCOPED_TRACE(setting);
+        std::map<std::string, std::string> options;
+        if (!setting.empty()) {
+            options[PARQUET_READ_ENABLE_OFFSET_INDEX_CACHE] = setting;
+        }
+        ParquetReaderBuilder builder(options, 10);
+        ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> input, 
fs_->Open(file_path_));
+        if (setting == "invalid") {
+            ASSERT_NOK(builder.Build(input));
+            continue;
+        }
+        ASSERT_OK_AND_ASSIGN(auto reader, builder.Build(input));
+        ArrowSchema c_schema;
+        ASSERT_TRUE(arrow::ExportSchema(*projection, &c_schema).ok());
+        ASSERT_OK(reader->SetReadSchema(&c_schema, predicate, std::nullopt));
+        ASSERT_OK_AND_ASSIGN(auto result,
+                             
paimon::test::ReadResultCollector::CollectResult(reader.get()));
+        ASSERT_EQ(1, result->length());
+        auto rows = 
std::dynamic_pointer_cast<arrow::StructArray>(result->chunk(0));
+        ASSERT_TRUE(rows);
+        auto values = 
std::dynamic_pointer_cast<arrow::Int32Array>(rows->field(0));
+        ASSERT_TRUE(values);
+        ASSERT_EQ(300002, values->Value(0));
+    }
+}
+
 TEST_F(ParquetFileBatchReaderTest, TestPointReadReusesFooterAndPageIndexes) {
     WriteArray(file_path_, struct_array_, schema_, /*write_batch_size=*/1,
                /*enable_dictionary=*/false, /*max_row_group_length=*/3,
diff --git a/src/paimon/format/parquet/parquet_format_defs.h 
b/src/paimon/format/parquet/parquet_format_defs.h
index 6bc811e5..f45603fc 100644
--- a/src/paimon/format/parquet/parquet_format_defs.h
+++ b/src/paimon/format/parquet/parquet_format_defs.h
@@ -99,6 +99,10 @@ static inline const char 
PARQUET_READ_PREDICATE_NODE_COUNT_LIMIT[] =
 static inline const char PARQUET_READ_ENABLE_PAGE_INDEX_FILTER[] =
     "parquet.read.enable-page-index-filter";
 
+// Retain parsed OffsetIndex objects within a reader. Disabled by default.
+static inline const char PARQUET_READ_ENABLE_OFFSET_INDEX_CACHE[] =
+    "parquet.read.enable-offset-index-cache";
+
 // Default is true.
 static inline const char PARQUET_READ_ENABLE_PRE_BUFFER[] = 
"parquet.read.enable-pre-buffer";
 

Reply via email to