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

Gabriel39 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 bc03da163b2 [improvement](parquet) Add prefetch for file scanner v2 
(#65151)
bc03da163b2 is described below

commit bc03da163b28b4e5f92b0b07254ec190bd11e790
Author: Gabriel <[email protected]>
AuthorDate: Thu Jul 2 22:57:32 2026 +0800

    [improvement](parquet) Add prefetch for file scanner v2 (#65151)
    
    ## Summary
    
    Add Parquet FileScannerV2 prefetch support and align the v2 row-group
    read path with the v1 Parquet reader's `MergeRangeFileReader` behavior.
    
    ## Changes
    
    - Keep the Arrow Parquet random-access wrapper, but make its underlying
    Doris `FileReader` switchable per row group.
    - Build projected Parquet leaf-column chunk ranges for the current row
    group, including nested projections.
    - Before opening each selected row group, configure
    `MergeRangeFileReader` when the average projected column-chunk IO is
    smaller than `MergeRangeFileReader::SMALL_IO`, matching the v1
    small-random-IO path.
    - Respect the existing `merge_read_slice_size` query option when
    constructing `MergeRangeFileReader`.
    - Preserve `TracingFileReader` statistics when switching between the raw
    reader and merge reader.
    - Keep best-effort file-cache warm-up only as a fallback when
    merge-range reading is not active.
    
    ## Notes
    
    The important v1 parity point is that subsequent Arrow `ReadAt()` calls
    now go through `MergeRangeFileReader` for small projected row-group IOs.
    This changes the actual read path, not just background cache warming.
---
 be/src/format_v2/parquet/parquet_file_context.cpp  | 153 ++++++++++++++++++-
 be/src/format_v2/parquet/parquet_file_context.h    |  39 +++++
 be/src/format_v2/parquet/parquet_reader.cpp        |   5 +
 be/src/format_v2/parquet/parquet_scan.cpp          | 162 ++++++++++++++++++++-
 be/src/format_v2/parquet/parquet_scan.h            |  28 +++-
 .../parquet/parquet_page_cache_range_test.cpp      |  64 ++++++++
 6 files changed, 441 insertions(+), 10 deletions(-)

diff --git a/be/src/format_v2/parquet/parquet_file_context.cpp 
b/be/src/format_v2/parquet/parquet_file_context.cpp
index dd9bf6aa954..dac09747408 100644
--- a/be/src/format_v2/parquet/parquet_file_context.cpp
+++ b/be/src/format_v2/parquet/parquet_file_context.cpp
@@ -24,14 +24,18 @@
 #include <algorithm>
 #include <cstring>
 #include <exception>
+#include <limits>
 #include <mutex>
 #include <unordered_map>
 #include <utility>
 
 #include "common/check.h"
 #include "common/config.h"
+#include "io/cache/cached_remote_file_reader.h"
 #include "io/file_factory.h"
+#include "io/fs/buffered_reader.h"
 #include "io/fs/file_reader.h"
+#include "io/fs/tracing_file_reader.h"
 #include "storage/cache/page_cache.h"
 #include "util/slice.h"
 
@@ -91,6 +95,38 @@ std::vector<ParquetPageCacheReadPlanEntry> 
plan_page_cache_range_read(
     return plan;
 }
 
+std::vector<ParquetPageCacheRange> valid_prefetch_ranges(
+        const std::vector<ParquetPageCacheRange>& ranges) {
+    std::vector<ParquetPageCacheRange> valid_ranges;
+    valid_ranges.reserve(ranges.size());
+    for (const auto& range : ranges) {
+        if (range.offset < 0 || range.size <= 0 ||
+            range.offset > std::numeric_limits<int64_t>::max() - range.size) {
+            continue;
+        }
+        valid_ranges.push_back(range);
+    }
+    return valid_ranges;
+}
+
+size_t average_prefetch_range_size(const std::vector<ParquetPageCacheRange>& 
ranges) {
+    const auto valid_ranges = valid_prefetch_ranges(ranges);
+    if (valid_ranges.empty()) {
+        return 0;
+    }
+    size_t total_size = 0;
+    for (const auto& range : valid_ranges) {
+        total_size += static_cast<size_t>(range.size);
+    }
+    return total_size / valid_ranges.size();
+}
+
+bool should_use_merge_range_reader(const std::vector<ParquetPageCacheRange>& 
ranges,
+                                   size_t avg_io_size, bool 
is_in_memory_reader) {
+    return !is_in_memory_reader && !valid_prefetch_ranges(ranges).empty() &&
+           avg_io_size < io::MergeRangeFileReader::SMALL_IO;
+}
+
 } // namespace detail
 
 namespace {
@@ -172,15 +208,24 @@ public:
     DorisRandomAccessFile(io::FileReaderSPtr file_reader, io::IOContext* 
io_ctx,
                           bool enable_page_cache, std::string 
page_cache_file_key)
             : _file_reader(std::move(file_reader)),
+              _base_file_reader(_file_reader),
               _io_ctx(io_ctx),
               _enable_page_cache(enable_page_cache),
               _page_cache_file_key(std::move(page_cache_file_key)) {
         DORIS_CHECK(_file_reader != nullptr);
+        if (auto tracing_reader = 
std::dynamic_pointer_cast<io::TracingFileReader>(_file_reader)) {
+            _file_reader_stats = tracing_reader->stats();
+            _base_file_reader = tracing_reader->inner_reader();
+        }
+        DORIS_CHECK(_base_file_reader != nullptr);
         set_mode(arrow::io::FileMode::READ);
     }
 
     arrow::Status Close() override {
-        _closed = true;
+        if (!_closed) {
+            collect_active_merge_range_profile();
+            _closed = true;
+        }
         return arrow::Status::OK();
     }
 
@@ -253,6 +298,53 @@ public:
         _page_cache_ranges = std::move(ranges);
     }
 
+    void prefetch_ranges(const std::vector<ParquetPageCacheRange>& ranges,
+                         const io::IOContext* io_ctx) {
+        auto cached_reader = cached_remote_file_reader();
+        if (cached_reader == nullptr) {
+            return;
+        }
+        const auto* prefetch_io_ctx = io_ctx != nullptr ? io_ctx : _io_ctx;
+        for (const auto& range : ranges) {
+            if (range.offset < 0 || range.size <= 0) {
+                continue;
+            }
+            cached_reader->prefetch_range(static_cast<size_t>(range.offset),
+                                          static_cast<size_t>(range.size), 
prefetch_io_ctx);
+        }
+    }
+
+    bool set_random_access_ranges(const std::vector<ParquetPageCacheRange>& 
ranges,
+                                  size_t avg_io_size, RuntimeProfile* profile,
+                                  int64_t merge_read_slice_size) {
+        reset_active_file_reader();
+        const auto valid_ranges = detail::valid_prefetch_ranges(ranges);
+        if (!detail::should_use_merge_range_reader(
+                    valid_ranges, avg_io_size,
+                    
typeid_cast<io::InMemoryFileReader*>(_base_file_reader.get()) != nullptr)) {
+            return false;
+        }
+
+        std::vector<io::PrefetchRange> random_access_ranges;
+        random_access_ranges.reserve(valid_ranges.size());
+        for (const auto& range : valid_ranges) {
+            
random_access_ranges.emplace_back(static_cast<size_t>(range.offset),
+                                              
static_cast<size_t>(range.end_offset()));
+        }
+
+        // This mirrors the v1 parquet reader: when projected column chunks in 
a row group are
+        // small random IOs, make the actual ReadAt path range-aware. Arrow 
still drives decoding,
+        // but every page read below this point sees MergeRangeFileReader 
instead of the raw remote
+        // reader, so adjacent small requests can be coalesced and served from 
merge buffers.
+        // Example: a row group projects leaf chunks [1MB, 1.5MB) and [1.6MB, 
2MB). Arrow later
+        // issues page reads inside those chunks; MergeRangeFileReader can 
fetch a wider slice once
+        // and satisfy the following ReadAt calls from its boxes, reducing 
remote request count.
+        _merge_range_active = true;
+        set_active_file_reader(std::make_shared<io::MergeRangeFileReader>(
+                profile, _base_file_reader, random_access_ranges, 
merge_read_slice_size));
+        return true;
+    }
+
     ParquetPageCacheStats page_cache_stats() const {
         std::lock_guard lock(_page_cache_mutex);
         return _page_cache_stats;
@@ -359,11 +451,56 @@ private:
         ++_page_cache_stats.compressed_write_count;
     }
 
+    void set_active_file_reader(io::FileReaderSPtr reader) {
+        DORIS_CHECK(reader != nullptr);
+        _file_reader = _file_reader_stats != nullptr
+                               ? 
std::make_shared<io::TracingFileReader>(std::move(reader),
+                                                                         
_file_reader_stats)
+                               : std::move(reader);
+    }
+
+    void reset_active_file_reader() {
+        collect_active_merge_range_profile();
+        _merge_range_active = false;
+        set_active_file_reader(_base_file_reader);
+    }
+
+    void collect_active_merge_range_profile() {
+        if (_merge_range_active && _file_reader != nullptr) {
+            // MergeRangeFileReader writes its MergedSmallIO counters only from
+            // collect_profile_before_close(). v2 replaces the active reader 
for every row group,
+            // so collect before overwriting it; Close() handles the final row 
group. Example:
+            // RG0 installs a merge reader, RG1 calls 
set_random_access_ranges() and resets the
+            // active reader first, so RG0's RequestIO/MergedIO counters must 
be flushed here.
+            _file_reader->collect_profile_before_close();
+        }
+    }
+
+    std::shared_ptr<io::CachedRemoteFileReader> cached_remote_file_reader() {
+        if (_merge_range_active) {
+            return nullptr;
+        }
+        auto reader = _file_reader;
+        if (reader == nullptr) {
+            return nullptr;
+        }
+        // FileReader::init wraps the physical reader with TracingFileReader 
when scan IO stats are
+        // enabled. Prefetch should target the physical cached reader below 
that tracing wrapper,
+        // otherwise v2 scans with profiling would silently lose prefetch.
+        if (auto tracing_reader = 
std::dynamic_pointer_cast<io::TracingFileReader>(reader)) {
+            reader = tracing_reader->inner_reader();
+        }
+        return std::dynamic_pointer_cast<io::CachedRemoteFileReader>(reader);
+    }
+
     io::FileReaderSPtr _file_reader;
+    io::FileReaderSPtr _base_file_reader;
+    io::FileReaderStats* _file_reader_stats = nullptr;
     io::IOContext* _io_ctx = nullptr;
     int64_t _pos = 0;
     bool _closed = false;
     bool _enable_page_cache = false;
+    bool _merge_range_active = false;
     std::string _page_cache_file_key;
     mutable std::mutex _page_cache_mutex;
     std::vector<ParquetPageCacheRange> _page_cache_ranges;
@@ -417,6 +554,20 @@ void 
ParquetFileContext::register_page_cache_ranges(std::vector<ParquetPageCache
             ->register_page_cache_ranges(std::move(ranges));
 }
 
+void ParquetFileContext::prefetch_ranges(const 
std::vector<ParquetPageCacheRange>& ranges,
+                                         const io::IOContext* io_ctx) {
+    DORIS_CHECK(arrow_file != nullptr);
+    
static_cast<DorisRandomAccessFile*>(arrow_file.get())->prefetch_ranges(ranges, 
io_ctx);
+}
+
+bool ParquetFileContext::set_random_access_ranges(const 
std::vector<ParquetPageCacheRange>& ranges,
+                                                  size_t avg_io_size, 
RuntimeProfile* profile,
+                                                  int64_t 
merge_read_slice_size) {
+    DORIS_CHECK(arrow_file != nullptr);
+    return static_cast<DorisRandomAccessFile*>(arrow_file.get())
+            ->set_random_access_ranges(ranges, avg_io_size, profile, 
merge_read_slice_size);
+}
+
 ParquetPageCacheStats ParquetFileContext::page_cache_stats() const {
     if (arrow_file == nullptr) {
         return {};
diff --git a/be/src/format_v2/parquet/parquet_file_context.h 
b/be/src/format_v2/parquet/parquet_file_context.h
index 8dedf732c8f..e75293f470f 100644
--- a/be/src/format_v2/parquet/parquet_file_context.h
+++ b/be/src/format_v2/parquet/parquet_file_context.h
@@ -18,6 +18,7 @@
 #include <arrow/io/interfaces.h>
 #include <parquet/api/reader.h>
 
+#include <cstddef>
 #include <cstdint>
 #include <memory>
 #include <vector>
@@ -27,8 +28,13 @@
 
 namespace doris::io {
 struct FileDescription;
+struct IOContext;
 } // namespace doris::io
 
+namespace doris {
+class RuntimeProfile;
+} // namespace doris
+
 namespace doris::format::parquet {
 
 struct ParquetPageCacheRange {
@@ -76,6 +82,27 @@ namespace detail {
 std::vector<ParquetPageCacheReadPlanEntry> plan_page_cache_range_read(
         int64_t position, int64_t nbytes, const 
std::vector<ParquetPageCacheRange>& cached_ranges);
 
+// Keep only byte ranges that are safe to hand to FileReader implementations. 
Parquet metadata is
+// expected to contain non-negative offsets and positive compressed sizes, but 
tests and corrupted
+// footers can still feed invalid values. Example: [100, 64) is kept, while 
[-1, 64), [100, 0) and
+// an offset+size overflow are ignored.
+std::vector<ParquetPageCacheRange> valid_prefetch_ranges(
+        const std::vector<ParquetPageCacheRange>& ranges);
+
+// Average projected column-chunk size for one row group. The v1 parquet path 
uses this value to
+// decide whether a row group is dominated by small random IOs; v2 uses the 
same signal before
+// installing MergeRangeFileReader. Example: chunks of 512KB and 1MB average 
below SMALL_IO and are
+// good merge-reader candidates, while two 8MB chunks should stay on the raw 
random-access reader.
+size_t average_prefetch_range_size(const std::vector<ParquetPageCacheRange>& 
ranges);
+
+// Decide whether Arrow ReadAt() should be routed through MergeRangeFileReader 
for the current row
+// group. This is intentionally stricter than the background warm-up path:
+// - no valid projected chunks -> nothing to merge;
+// - in-memory file readers already avoid remote random IO;
+// - average chunk size >= MergeRangeFileReader::SMALL_IO would make merged 
reading wasteful.
+bool should_use_merge_range_reader(const std::vector<ParquetPageCacheRange>& 
ranges,
+                                   size_t avg_io_size, bool 
is_in_memory_reader);
+
 } // namespace detail
 
 struct ParquetFileContext {
@@ -90,6 +117,18 @@ struct ParquetFileContext {
     // decoding, so v2 caches the serialized bytes read inside these ranges 
and excludes
     // footer/metadata reads that happen before registration.
     void register_page_cache_ranges(std::vector<ParquetPageCacheRange> ranges);
+    // Best-effort asynchronous warm-up for Parquet column chunks. This only 
has an effect when
+    // the underlying Doris file reader is a CachedRemoteFileReader; other 
readers keep the same
+    // random-access behavior and simply skip prefetch.
+    void prefetch_ranges(const std::vector<ParquetPageCacheRange>& ranges,
+                         const io::IOContext* io_ctx);
+    // Switch the active reader used by Arrow ReadAt() to v1's 
MergeRangeFileReader when the current
+    // row group's projected column chunks are small random IOs. This is the 
real v1-compatible
+    // prefetch path: subsequent Arrow page reads go through the merged reader 
instead of merely
+    // warming file cache in the background. Returns true when merge-range 
reading is active.
+    bool set_random_access_ranges(const std::vector<ParquetPageCacheRange>& 
ranges,
+                                  size_t avg_io_size, RuntimeProfile* profile,
+                                  int64_t merge_read_slice_size);
     ParquetPageCacheStats page_cache_stats() const;
     Status close();
 };
diff --git a/be/src/format_v2/parquet/parquet_reader.cpp 
b/be/src/format_v2/parquet/parquet_reader.cpp
index 3e13333e7d4..3d8a0a061fb 100644
--- a/be/src/format_v2/parquet/parquet_reader.cpp
+++ b/be/src/format_v2/parquet/parquet_reader.cpp
@@ -334,6 +334,11 @@ Status ParquetReader::init(RuntimeState* state) {
         _state->scheduler.set_timezone(&state->timezone_obj());
         _state->scheduler.set_enable_strict_mode(_state->enable_strict_mode);
     }
+    int64_t merge_read_slice_size = -1;
+    if (state != nullptr && 
state->query_options().__isset.merge_read_slice_size) {
+        merge_read_slice_size = state->query_options().merge_read_slice_size;
+    }
+    _state->scheduler.set_merge_read_options(_profile, merge_read_slice_size);
     _state->scheduler.set_batch_size(_batch_size);
     // Open parquet file and parse metadata to get file schema.
     RETURN_IF_ERROR(_state->file_context.open(_tracing_file_reader, 
_io_ctx.get(),
diff --git a/be/src/format_v2/parquet/parquet_scan.cpp 
b/be/src/format_v2/parquet/parquet_scan.cpp
index d636f3e3f9e..62dad184b1e 100644
--- a/be/src/format_v2/parquet/parquet_scan.cpp
+++ b/be/src/format_v2/parquet/parquet_scan.cpp
@@ -18,6 +18,8 @@
 #include <algorithm>
 #include <limits>
 #include <memory>
+#include <ranges>
+#include <unordered_set>
 #include <utility>
 
 #include "common/exception.h"
@@ -40,6 +42,39 @@ int64_t column_start_offset(const 
::parquet::ColumnChunkMetaData& column_metadat
                    : cast_set<int64_t>(column_metadata.data_page_offset());
 }
 
+void collect_all_leaf_column_ids(const ParquetColumnSchema& column_schema,
+                                 std::unordered_set<int>* leaf_column_ids) {
+    DORIS_CHECK(leaf_column_ids != nullptr);
+    if (column_schema.kind == ParquetColumnSchemaKind::PRIMITIVE) {
+        if (column_schema.leaf_column_id >= 0) {
+            leaf_column_ids->insert(column_schema.leaf_column_id);
+        }
+        return;
+    }
+    for (const auto& child : column_schema.children) {
+        DORIS_CHECK(child != nullptr);
+        collect_all_leaf_column_ids(*child, leaf_column_ids);
+    }
+}
+
+void collect_projected_leaf_column_ids(const ParquetColumnSchema& 
column_schema,
+                                       const format::LocalColumnIndex& 
projection,
+                                       std::unordered_set<int>* 
leaf_column_ids) {
+    DORIS_CHECK(leaf_column_ids != nullptr);
+    if (projection.project_all_children || projection.children.empty()) {
+        collect_all_leaf_column_ids(column_schema, leaf_column_ids);
+        return;
+    }
+    for (const auto& child_projection : projection.children) {
+        const auto child_it =
+                std::ranges::find_if(column_schema.children, [&](const auto& 
child_schema) {
+                    return child_schema->local_id == 
child_projection.local_id();
+                });
+        DORIS_CHECK(child_it != column_schema.children.end());
+        collect_projected_leaf_column_ids(**child_it, child_projection, 
leaf_column_ids);
+    }
+}
+
 bool is_row_group_outside_range(const ::parquet::FileMetaData& metadata,
                                 const ParquetScanRange& scan_range, int 
row_group_idx) {
     if (scan_range.size < 0) {
@@ -69,6 +104,56 @@ bool is_row_group_outside_range(const 
::parquet::FileMetaData& metadata,
     return row_group_mid_offset < range_start_offset || row_group_mid_offset 
>= range_end_offset;
 }
 
+std::vector<format::LocalColumnIndex> request_scan_columns(const 
format::FileScanRequest& request) {
+    std::vector<format::LocalColumnIndex> scan_columns;
+    scan_columns.reserve(request.predicate_columns.size() + 
request.non_predicate_columns.size());
+    scan_columns.insert(scan_columns.end(), request.predicate_columns.begin(),
+                        request.predicate_columns.end());
+    scan_columns.insert(scan_columns.end(), 
request.non_predicate_columns.begin(),
+                        request.non_predicate_columns.end());
+    return scan_columns;
+}
+
+std::vector<ParquetPageCacheRange> build_row_group_prefetch_ranges(
+        const ::parquet::FileMetaData& metadata,
+        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+        const std::vector<format::LocalColumnIndex>& scan_columns, int 
row_group_idx) {
+    std::unordered_set<int> leaf_column_ids;
+    for (const auto& projection : scan_columns) {
+        const auto local_id = projection.local_id();
+        if (local_id == format::ROW_POSITION_COLUMN_ID ||
+            local_id == format::GLOBAL_ROWID_COLUMN_ID) {
+            continue;
+        }
+        DORIS_CHECK(local_id >= 0 && local_id < 
static_cast<int32_t>(file_schema.size()));
+        DORIS_CHECK(file_schema[local_id] != nullptr);
+        // Prefetch and merge-reader ranges must be physical leaf chunks, not 
Doris logical slots.
+        // Example: for a struct column s<a:int,b:string>, projecting only s.a 
should include only
+        // the Parquet leaf chunk of a. Projecting the whole struct includes 
both a and b.
+        collect_projected_leaf_column_ids(*file_schema[local_id], projection, 
&leaf_column_ids);
+    }
+
+    auto row_group_metadata = metadata.RowGroup(row_group_idx);
+    DORIS_CHECK(row_group_metadata != nullptr);
+    std::vector<int> ordered_leaf_column_ids(leaf_column_ids.begin(), 
leaf_column_ids.end());
+    std::ranges::sort(ordered_leaf_column_ids);
+
+    std::vector<ParquetPageCacheRange> ranges;
+    ranges.reserve(ordered_leaf_column_ids.size());
+    for (const auto leaf_column_id : ordered_leaf_column_ids) {
+        DORIS_CHECK(leaf_column_id >= 0 && leaf_column_id < 
row_group_metadata->num_columns());
+        auto column_metadata = row_group_metadata->ColumnChunk(leaf_column_id);
+        DORIS_CHECK(column_metadata != nullptr);
+        const int64_t offset = column_start_offset(*column_metadata);
+        const int64_t size = column_metadata->total_compressed_size();
+        DORIS_CHECK(offset >= 0);
+        if (size > 0) {
+            ranges.push_back(ParquetPageCacheRange {.offset = offset, .size = 
size});
+        }
+    }
+    return ranges;
+}
+
 } // namespace
 
 Status plan_parquet_row_groups(const ::parquet::FileMetaData& metadata,
@@ -326,11 +411,15 @@ void ParquetScanScheduler::reset_current_row_group() {
     _current_predicate_columns.clear();
     _current_non_predicate_columns.clear();
     _current_row_group_rows = 0;
+    _current_row_group_id = -1;
     _current_row_group_rows_read = 0;
     _current_row_group_first_row = 0;
     _current_selected_ranges.clear();
     _current_range_idx = 0;
     _current_range_rows_read = 0;
+    _current_predicate_prefetched = false;
+    _current_non_predicate_prefetched = false;
+    _current_merge_range_active = false;
 }
 
 Status ParquetScanScheduler::open_next_row_group(
@@ -343,6 +432,8 @@ Status ParquetScanScheduler::open_next_row_group(
     }
     const RowGroupReadPlan& row_group_plan = 
_row_group_plans[_next_row_group_plan_idx++];
     const int row_group_idx = row_group_plan.row_group_id;
+    _current_merge_range_active =
+            prepare_current_row_group_reader(file_context, file_schema, 
request, row_group_idx);
     try {
         _current_row_group = file_context.file_reader->RowGroup(row_group_idx);
     } catch (const ::parquet::ParquetException& e) {
@@ -358,6 +449,7 @@ Status ParquetScanScheduler::open_next_row_group(
     _current_row_group_rows = row_group_metadata->num_rows();
     DORIS_CHECK(_current_row_group_rows == row_group_plan.row_group_rows);
     DORIS_CHECK(_current_row_group_rows > 0);
+    _current_row_group_id = row_group_idx;
     DORIS_CHECK(!row_group_plan.selected_ranges.empty());
     _current_row_group_first_row = row_group_plan.first_file_row;
     _current_row_group_rows_read = 0;
@@ -394,6 +486,13 @@ Status ParquetScanScheduler::open_next_row_group(
         RETURN_IF_ERROR(column_reader_factory.create(*column_schema, &col, 
&column_reader));
         _current_predicate_columns[local_id] = std::move(column_reader);
     }
+    // Start warming filter-column chunks as soon as their row group is 
selected. Parquet v2 still
+    // reads through Arrow's random-access reader; this prefetch only warms 
Doris file cache blocks
+    // in the background and never changes the row/column materialization 
order.
+    if (!_current_merge_range_active) {
+        prefetch_current_row_group_columns(file_context, file_schema, 
request.predicate_columns,
+                                           &_current_predicate_prefetched);
+    }
     for (const auto& col : request.non_predicate_columns) {
         const auto local_id = col.local_id();
         if (local_id == format::ROW_POSITION_COLUMN_ID) {
@@ -416,6 +515,14 @@ Status ParquetScanScheduler::open_next_row_group(
         RETURN_IF_ERROR(column_reader_factory.create(*column_schema, &col, 
&column_reader));
         _current_non_predicate_columns[local_id] = std::move(column_reader);
     }
+    if (!_current_merge_range_active && request.conjuncts.empty() &&
+        request.delete_conjuncts.empty()) {
+        // With no row-level filters there is no lazy-read decision to wait 
for, so start warming
+        // output chunks immediately after their readers are created. Filtered 
scans still defer
+        // this until at least one row survives the predicate phase.
+        prefetch_current_row_group_columns(file_context, file_schema, 
request.non_predicate_columns,
+                                           &_current_non_predicate_prefetched);
+    }
     *has_row_group = true;
     return Status::OK();
 }
@@ -476,10 +583,45 @@ Status ParquetScanScheduler::read_filter_columns(int64_t 
batch_rows,
                                  conjunct_filtered_rows);
 }
 
-Status ParquetScanScheduler::read_current_row_group_batch(int64_t batch_rows,
-                                                          const 
format::FileScanRequest& request,
-                                                          int64_t 
batch_first_file_row,
-                                                          Block* file_block, 
size_t* rows) {
+bool ParquetScanScheduler::prepare_current_row_group_reader(
+        ParquetFileContext& file_context,
+        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+        const format::FileScanRequest& request, int row_group_idx) {
+    if (file_context.metadata == nullptr) {
+        return false;
+    }
+    const auto ranges = build_row_group_prefetch_ranges(
+            *file_context.metadata, file_schema, 
request_scan_columns(request), row_group_idx);
+    const size_t avg_io_size = detail::average_prefetch_range_size(ranges);
+    return file_context.set_random_access_ranges(ranges, avg_io_size, _profile,
+                                                 _merge_read_slice_size);
+}
+
+void ParquetScanScheduler::prefetch_current_row_group_columns(
+        ParquetFileContext& file_context,
+        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+        const std::vector<format::LocalColumnIndex>& scan_columns, bool* 
prefetched) {
+    DORIS_CHECK(prefetched != nullptr);
+    if (_current_merge_range_active || *prefetched || scan_columns.empty() ||
+        _current_row_group_id < 0 || file_context.metadata == nullptr) {
+        return;
+    }
+    *prefetched = true;
+    // The scanner request separates predicate and non-predicate columns so 
Parquet can read
+    // predicate columns first and lazily materialize the rest. Keep the same 
contract for
+    // prefetch: callers decide which side to warm, and this helper only 
translates that selected
+    // projection into physical column-chunk byte ranges for the current row 
group.
+    file_context.prefetch_ranges(
+            build_row_group_prefetch_ranges(*file_context.metadata, 
file_schema, scan_columns,
+                                            _current_row_group_id),
+            nullptr);
+}
+
+Status ParquetScanScheduler::read_current_row_group_batch(
+        ParquetFileContext& file_context,
+        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, 
int64_t batch_rows,
+        const format::FileScanRequest& request, int64_t batch_first_file_row, 
Block* file_block,
+        size_t* rows) {
     if (_scan_profile.total_batches != nullptr) {
         COUNTER_UPDATE(_scan_profile.total_batches, 1);
     }
@@ -527,6 +669,14 @@ Status 
ParquetScanScheduler::read_current_row_group_batch(int64_t batch_rows,
                                             .column->filter(output_filter, 
selected_rows)));
         }
     }
+    if (!_current_merge_range_active && selected_rows > 0 &&
+        !_current_non_predicate_columns.empty()) {
+        // Do not prefetch lazy output columns until at least one row survives 
filtering. This is
+        // the same decision point where the v2 reader switches from 
predicate-only reads to
+        // materializing non-predicate columns, so fully filtered batches 
avoid unnecessary IO.
+        prefetch_current_row_group_columns(file_context, file_schema, 
request.non_predicate_columns,
+                                           &_current_non_predicate_prefetched);
+    }
 
     {
         SCOPED_TIMER(_scan_profile.column_read_time);
@@ -629,8 +779,8 @@ Status ParquetScanScheduler::read_next_batch(
         const int64_t physical_rows_read = batch_rows;
         const int64_t batch_first_file_row =
                 _current_row_group_first_row + _current_row_group_rows_read;
-        RETURN_IF_ERROR(read_current_row_group_batch(batch_rows, request, 
batch_first_file_row,
-                                                     file_block, rows));
+        RETURN_IF_ERROR(read_current_row_group_batch(file_context, 
file_schema, batch_rows, request,
+                                                     batch_first_file_row, 
file_block, rows));
         _current_row_group_rows_read += physical_rows_read;
         _current_range_rows_read += physical_rows_read;
         if (_current_range_rows_read >= current_range.length) {
diff --git a/be/src/format_v2/parquet/parquet_scan.h 
b/be/src/format_v2/parquet/parquet_scan.h
index ca3c665a2e9..0253fbdd50d 100644
--- a/be/src/format_v2/parquet/parquet_scan.h
+++ b/be/src/format_v2/parquet/parquet_scan.h
@@ -109,6 +109,10 @@ public:
         _page_skip_profile = page_skip_profile;
     }
     void set_scan_profile(ParquetScanProfile scan_profile) { _scan_profile = 
scan_profile; }
+    void set_merge_read_options(RuntimeProfile* profile, int64_t 
merge_read_slice_size) {
+        _profile = profile;
+        _merge_read_slice_size = merge_read_slice_size;
+    }
     void set_global_rowid_context(std::optional<format::GlobalRowIdContext> 
context) {
         _global_rowid_context = context;
     }
@@ -145,9 +149,21 @@ private:
                                Block* file_block, SelectionVector* selection,
                                uint16_t* selected_rows, int64_t* 
conjunct_filtered_rows);
 
-    Status read_current_row_group_batch(int64_t batch_rows, const 
format::FileScanRequest& request,
-                                        int64_t batch_first_file_row, Block* 
file_block,
-                                        size_t* rows);
+    void prefetch_current_row_group_columns(
+            ParquetFileContext& file_context,
+            const std::vector<std::unique_ptr<ParquetColumnSchema>>& 
file_schema,
+            const std::vector<format::LocalColumnIndex>& scan_columns, bool* 
prefetched);
+
+    bool prepare_current_row_group_reader(
+            ParquetFileContext& file_context,
+            const std::vector<std::unique_ptr<ParquetColumnSchema>>& 
file_schema,
+            const format::FileScanRequest& request, int row_group_idx);
+
+    Status read_current_row_group_batch(
+            ParquetFileContext& file_context,
+            const std::vector<std::unique_ptr<ParquetColumnSchema>>& 
file_schema,
+            int64_t batch_rows, const format::FileScanRequest& request,
+            int64_t batch_first_file_row, Block* file_block, size_t* rows);
 
     void mark_condition_cache_granules(const SelectionVector& selection, 
uint16_t selected_rows,
                                        int64_t batch_first_file_row);
@@ -161,6 +177,7 @@ private:
     std::map<ColumnId, std::unique_ptr<ParquetColumnReader>>
             _current_non_predicate_columns;   // non-predicate ColumnReaders
     int64_t _current_row_group_rows = 0;      // current row group row count
+    int _current_row_group_id = -1;           // current row group id in 
parquet metadata
     int64_t _current_row_group_rows_read = 0; // rows read in the current row 
group (cursor)
     int64_t _current_row_group_first_row = 0; // first file row of the current 
row group
     std::vector<RowRange>
@@ -168,8 +185,13 @@ private:
     size_t _current_range_idx = 0;        // current selected_range index
     int64_t _current_range_rows_read = 0; // rows read in the current range
 
+    bool _current_predicate_prefetched = false;
+    bool _current_non_predicate_prefetched = false;
+    bool _current_merge_range_active = false;
     ParquetPageSkipProfile _page_skip_profile;
     ParquetScanProfile _scan_profile;
+    RuntimeProfile* _profile = nullptr;
+    int64_t _merge_read_slice_size = -1;
     std::optional<format::GlobalRowIdContext> _global_rowid_context;
     const cctz::time_zone* _timezone = nullptr;
     bool _enable_strict_mode = false;
diff --git a/be/test/format_v2/parquet/parquet_page_cache_range_test.cpp 
b/be/test/format_v2/parquet/parquet_page_cache_range_test.cpp
index f8e12206bb1..fac89b31c42 100644
--- a/be/test/format_v2/parquet/parquet_page_cache_range_test.cpp
+++ b/be/test/format_v2/parquet/parquet_page_cache_range_test.cpp
@@ -17,9 +17,11 @@
 
 #include <gtest/gtest.h>
 
+#include <limits>
 #include <vector>
 
 #include "format_v2/parquet/parquet_file_context.h"
+#include "io/fs/buffered_reader.h"
 
 namespace doris::format::parquet {
 namespace {
@@ -113,5 +115,67 @@ TEST(ParquetPageCacheRangeTest, InvalidRequestMisses) {
     EXPECT_TRUE(detail::plan_page_cache_range_read(100, -1, 
cached_ranges).empty());
 }
 
+TEST(ParquetPageCacheRangeTest, 
ValidPrefetchRangesSkipInvalidAndOverflowRanges) {
+    const std::vector<ParquetPageCacheRange> ranges = {
+            {100, 50},
+            {-1, 50},
+            {200, 0},
+            {300, -1},
+            {std::numeric_limits<int64_t>::max() - 10, 20},
+            {400, 60},
+    };
+
+    const auto valid_ranges = detail::valid_prefetch_ranges(ranges);
+
+    ASSERT_EQ(valid_ranges.size(), 2);
+    EXPECT_EQ(valid_ranges[0].offset, 100);
+    EXPECT_EQ(valid_ranges[0].size, 50);
+    EXPECT_EQ(valid_ranges[1].offset, 400);
+    EXPECT_EQ(valid_ranges[1].size, 60);
+}
+
+TEST(ParquetPageCacheRangeTest, AveragePrefetchRangeSizeUsesOnlyValidRanges) {
+    const std::vector<ParquetPageCacheRange> ranges = {
+            {0, 512},
+            {512, 1536},
+            {-1, 1024},
+            {2048, 0},
+    };
+
+    EXPECT_EQ(detail::average_prefetch_range_size(ranges), 1024);
+    EXPECT_EQ(detail::average_prefetch_range_size({{-1, 1024}, {0, 0}}), 0);
+}
+
+TEST(ParquetPageCacheRangeTest, 
MergeRangeReaderDecisionMatchesV1SmallIoThreshold) {
+    const std::vector<ParquetPageCacheRange> ranges = {
+            {0, 512 * 1024},
+            {512 * 1024, 1024 * 1024},
+    };
+
+    // Two sub-2MB column chunks are the intended merge-reader case: Arrow may 
issue many page-level
+    // ReadAt calls inside these chunks, so v2 should route them through 
MergeRangeFileReader.
+    EXPECT_TRUE(detail::should_use_merge_range_reader(
+            ranges, detail::average_prefetch_range_size(ranges), false));
+
+    // The v1 threshold is strict: a 2MB average chunk is no longer considered 
"small IO".
+    EXPECT_FALSE(detail::should_use_merge_range_reader(ranges, 
io::MergeRangeFileReader::SMALL_IO,
+                                                       false));
+}
+
+TEST(ParquetPageCacheRangeTest, 
MergeRangeReaderDecisionRejectsEmptyInvalidAndInMemoryInputs) {
+    const std::vector<ParquetPageCacheRange> invalid_ranges = {
+            {-1, 128},
+            {0, 0},
+    };
+    const std::vector<ParquetPageCacheRange> valid_ranges = {
+            {0, 128 * 1024},
+    };
+
+    EXPECT_FALSE(detail::should_use_merge_range_reader({}, 0, false));
+    EXPECT_FALSE(detail::should_use_merge_range_reader(invalid_ranges, 128, 
false));
+    EXPECT_FALSE(detail::should_use_merge_range_reader(
+            valid_ranges, detail::average_prefetch_range_size(valid_ranges), 
true));
+}
+
 } // namespace
 } // namespace doris::format::parquet


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


Reply via email to