Skip to content

feat(parquet): skip page headers of unselected pages via OffsetIndex-based direct read plan - #167

Merged
SteNicholas merged 3 commits into
apache:mainfrom
zhf999:arrow-header-fix
Aug 3, 2026
Merged

feat(parquet): skip page headers of unselected pages via OffsetIndex-based direct read plan#167
SteNicholas merged 3 commits into
apache:mainfrom
zhf999:arrow-header-fix

Conversation

@zhf999

@zhf999 zhf999 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Purpose

Page-level filtering currently installs a data_page_filter on Arrow's PageReader. This skips the page body (read/decrypt/decompress/decode) of unselected pages, but SerializedPageReader::NextPage() still walks the column chunk sequentially: it Peeks and Thrift-deserializes the header of every page, because the only way to find the next page boundary is the compressed_page_size field inside the header. On top of the page-level ReadRangeCache, each skipped page header whose bytes are not cached triggers a real positional read of up to 16 KiB (kDefaultPageHeaderSize), so skipping many pages still causes significant small-read I/O amplification and header-parsing CPU cost.

This PR eliminates header reads for unselected pages by driving the PageReader with a precomputed read plan derived from the OffsetIndex:

  • Arrow patch (cmake_modules/arrow.diff)

    • Adds an experimental DataPageReadPlanEntry {page_ordinal, offset, compressed_page_size} and PageReader::set_data_page_read_plan(first_data_page_offset, data_pages), mutually exclusive with set_data_page_filter. Offsets are relative to the column chunk stream.
    • SerializedPageReader::NextPage() reads the region before first_data_page_offset sequentially (dictionary page), then jumps directly to each planned data page via Advance() and returns EOS once the plan is exhausted. Unselected pages are never Peeked or deserialized.
    • Header Peek size is clamped to min(16 KiB, compressed_page_size) so a header probe never crosses the cached page range (which would fall back to real I/O).
  • Paimon (src/paimon/format/parquet/page_filtered_row_group_reader.{h,cpp})

    • Replaces the per-page callback MakePageFilter with MakeDataPageReadPlan, which converts RowRanges + OffsetIndex into stream-relative plan entries.
    • ComputePageRanges now prefetches exact page ranges {offset, compressed_page_size} plus the dictionary prefix, and returns no ranges for an empty row selection.
    • An empty row selection no longer constructs a PageReader (the column iterator is created with no row groups), avoiding the eager whole-column-chunk read in GetColumnPageReader() when no range cache is present.

Note: rebuilding the Arrow external project is required after this change, since cmake_modules/arrow.diff is applied at Arrow EP patch time and the stamp chain does not track it automatically.

Tests

New UT cases in src/paimon/format/parquet/page_filtered_row_group_reader_test.cpp, using a ReadAtTrackingInputStream that records every positional read issued while consuming data pages and asserts that no recorded range covers an unselected page header:

  • DirectOffsetIndexJumpDoesNotReadUnselectedPageHeaders: selects rows from pages 1 and 8 out of 10; asserts selected pages are read and unselected page headers are never touched (this assertion fails before this change).
  • DirectOffsetIndexJumpReadsEachLeafDictionary: two dictionary-encoded leaves selecting only the last data page; each leaf still loads its own dictionary before jumping.
  • DirectOffsetIndexJumpSupportsDictionaryFallbackToPlain: a column that falls back from dictionary encoding to PLAIN mid-chunk; one plan decodes both kinds of selected pages correctly.
  • DirectOffsetIndexJumpDataPageV2AdjacentPages: DATA_PAGE_V2 with two adjacent selected pages, verifying the plan cursor advances exactly once per page.
  • DictionaryEmptySelectionDoesNotReadPages: an empty row selection reads no dictionary/data page bytes.
  • MissingOffsetIndexFallsBackToSequentialRead: files written without a page index keep the original full-column read path and return correct results.

All existing page-filtering tests in this suite now exercise the direct read plan path and continue to verify end-to-end results (flat/nested columns, V1/V2 pages, predicates + bitmaps, multiple row groups).

API and Format

No changes under include/paimon/ and no storage format or protocol change. The new PageReader::set_data_page_read_plan / DataPageReadPlanEntry are experimental APIs added only to the vendored Arrow patch (cmake_modules/arrow.diff), consumed internally by the Paimon Parquet reader. Read results are unchanged; only the I/O pattern of page-filtered reads is affected.

Documentation

No new user-facing feature; behavior and constraints are documented via comments on the new Arrow API and in page_filtered_row_group_reader.{h,cpp}.

Generative AI tooling

No.

@duanyyyyyyy

Copy link
Copy Markdown

LGTM
pls merge asap @lxy-9602

@lxy-9602 lxy-9602 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@SteNicholas SteNicholas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@SteNicholas
SteNicholas merged commit 75544f7 into apache:main Aug 3, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants