feat(parquet): support pushing bitmaps down to page-level filtering (except for nested columns)#375
feat(parquet): support pushing bitmaps down to page-level filtering (except for nested columns)#375zhf999 wants to merge 61 commits into
Conversation
| // bitmap pushdown if there is any nested field in the schema | ||
| PAIMON_ASSIGN_OR_RAISE(target_row_groups, | ||
| FilterRowGroupsByBitmap(selection_bitmap.value(), | ||
| target_row_groups, has_nested_field)); |
There was a problem hiding this comment.
PARQUET_READ_ENABLE_PAGE_INDEX_FILTER=false is not respected for bitmap page pruning.
SetReadSchema calls FilterRowGroupsByBitmap before reading PARQUET_READ_ENABLE_PAGE_INDEX_FILTER, and FilterPagesByBitmap may still load offset/page index metadata and mark row groups as partially matched. So even when page-index filtering is explicitly disabled, a bitmap that only hits part of a row group can still trigger page-level pruning.
Could we gate bitmap page-level pruning behind the same option? When disabled, bitmap filtering should only prune at row-group level. A regression test with PARQUET_READ_ENABLE_PAGE_INDEX_FILTER=false and a partial-page bitmap would be useful too.
| } | ||
| } | ||
| return -1; | ||
| } |
There was a problem hiding this comment.
Why does bitmap page pruning derive RowRanges from the first arbitrary column that has an offset index?
Since the bitmap already gives us row ids, the target row ranges can be derived independently of any specific column layout. Using one column’s page layout makes the pruning granularity depend on whichever column FindColumnWithOffsetIndex happens to return. If that column has larger/coarser pages than other columns, we may mark a much wider range as matched and lose a lot of the intended page-pruning benefit.
Could we compute row ranges directly from the bitmap, or at least choose the most selective/fine-grained offset index among the projected columns instead of returning on the first available column?
…ousBatchFirstRowId to GetGlobalRowId
| TargetRowGroup FilterRowGroupPagesByBitmap(const RoaringBitmap32& bitmap, | ||
| const TargetRowGroup& row_group, | ||
| const std::vector<int32_t>& column_indices, | ||
| ::parquet::PageIndexReader& page_index_reader) const; |
There was a problem hiding this comment.
For page_index_reader, please use a pointer if it is an output parameter, and const & if it is an input parameter.
| } else { | ||
| // all within the range, data before readers_[reader_idx]->GetNextRowToRead() has been | ||
| // effectively consumed | ||
| readers_pos_[reader_idx]->store(readers_[reader_idx]->GetNextRowToRead()); |
There was a problem hiding this comment.
It's a accident cause by git merge ... Modified.
| /// | ||
| /// Schema: { id: int32, props: map<utf8, int32> } | ||
| /// 100 rows, 10 per page, 1 row group. | ||
| /// Predicate: id >= 30 would be a partial-row-group match at first 50-row group. |
There was a problem hiding this comment.
1 row group or 2 row groups?
There was a problem hiding this comment.
The comment has been corrected.
| /// Schema: { id: int32, tags: list<item: int32> } | ||
| /// Predicate: id >= 30 would be a partial-row-group match at first 50-row group. | ||
| /// Because nested schema disables page-level filtering, the entire first row group (0..49) is read. | ||
| TEST_F(PageFilteredRowGroupReaderTest, NestedListBitmapFallback) { |
There was a problem hiding this comment.
the entire first row group (0..49) is read first or second?
There was a problem hiding this comment.
The comment has been corrected.
| for (auto rg_index : parquet_fragment->row_groups()) { | ||
| target_row_groups.emplace_back(src_row_groups[rg_index]); | ||
| } | ||
| } |
There was a problem hiding this comment.
[P2] Do not index TargetRowGroups by file row-group id
MakeSerialRowGroups may skip empty row groups, so the position in
src_row_groups is not guaranteed to equal the file row-group id returned by
parquet_fragment->row_groups(). FilterRowGroupsByPredicate currently does
src_row_groups[rg_index], which can select the wrong TargetRowGroup or go out
of bounds when earlier row groups were skipped. Please look up TargetRowGroup by
GetRowGroupIndex() instead of treating the row-group id as a vector index.
| row_ranges = RowRanges::Intersection(row_ranges, page_ranges); | ||
| } | ||
| return TargetRowGroup(row_group_idx, true, std::move(row_ranges)); | ||
| } |
There was a problem hiding this comment.
Why do we assume this is always true here? What if the bitmap covers the entire row group?
zjw1111
left a comment
There was a problem hiding this comment.
Thanks for the follow-up work on page-level bitmap pushdown, and for the solid test coverage. A few follow-up nits on stale comments and naming, on top of the points already raised in the earlier review.
| } | ||
| // Apply page-level filtering after bitmap pruning so we don't read page index | ||
| // pages for row groups that the bitmap already excluded. | ||
| // If no predicate is provided, skip page-level filtering, row_group_row_ranges will be |
There was a problem hiding this comment.
This comment still refers to row_group_row_ranges, but that map was removed in this PR (page-filter results now go straight into target_row_groups). Could you update the wording to describe target_row_groups so it no longer points at a variable that doesn't exist anymore?
| /// | ||
| /// Schema: { id: int32, props: map<utf8, int32> } | ||
| /// 100 rows, 10 per page, 1 row group. | ||
| /// Predicate: id >= 30 would be a partial-row-group match at first 50-row group. |
There was a problem hiding this comment.
Thanks for adding these fallback tests. Two things in this doc comment don't match the test body:
- It says
Predicate: id >= 30, but the test passesnullptras the predicate — only the bitmap (AddRange(70, 100)) drives the filtering. - It says
rows [0, 99] should all be returned, but the assertion expects 50 rows (data->Slice(50, 50), i.e. rows 50-99).
The same stale Predicate: id >= 30 line also appears in NestedListBitmapFallback and NestedStructBitmapFallback. Could you refresh these comments to match the actual bitmap-only behavior?
| /// Test: bitmap hits partial pages of a row group, with page-filtered option disabled. | ||
| /// | ||
| /// 200 rows, 10 rows per page, 100 rows per row group → 2 row groups. | ||
| /// Bitmap: {0..99} hits all of RG0 + {120..149} hits pages 2-4 of RG1. |
There was a problem hiding this comment.
This comment says the bitmap is {0..99} hits all of RG0 + {120..149}, but the test only does bitmap.AddRange(120, 150) (no {0..99}), and the expected result (100 rows, 100-199) confirms only RG1 is hit. Could you drop the {0..99} part so the comment reflects the actual input?
| return row_ranges_; | ||
| } | ||
|
|
||
| static TargetRowGroups MakeSerialRowGroups( |
There was a problem hiding this comment.
nit: MakeSerialRowGroups doesn't quite convey what it does — it builds the initial fully-matched list for all row groups. Would a name like MakeAllRowGroups / MakeFullRowGroups be clearer? A short comment noting that it skips empty (0-row) row groups would also help, since that skipping is exactly what makes list positions diverge from file row-group ids (related to the indexing issue flagged in FilterRowGroupsByPredicate).
|
|
||
| // Compute the set of row ranges within a single column's pages that | ||
| // overlap with the given bitmap. For each page, the bitmap is queried to | ||
| // find the precise [first, last] matching rows. |
There was a problem hiding this comment.
Minor wording: 'find the precise [first, last] matching rows' reads as if per-row filtering happens here, but the implementation only trims the non-matching rows at the page boundaries — gaps inside [first, last] are kept (consistent with the page-level trimming design, as ScatteredBitmapTest demonstrates). Could you reword to something like 'the first/last matching row in each page, used to trim the page head/tail'?
| using TargetRowGroups = std::vector<TargetRowGroup>; | ||
| class TargetRowGroup { | ||
| public: | ||
| explicit TargetRowGroup(int32_t rg_index) : row_group_index_(rg_index) {} |
There was a problem hiding this comment.
nit: this single-arg constructor produces a TargetRowGroup with an empty row_ranges_, and I don't see a caller for it in this PR (the others use the 3-arg ctor or copy). If it's unused, could we drop it to avoid an ambiguous empty-ranges object? Feel free to ignore if there's a caller I missed.
Purpose
This PR follows PR #371.
TargetRowGrouptype to carry row-group index, partial-match status, row ranges, and read-range exclusion flags consistently across planning and reading phases.Changes
src/paimon/format/parquet/target_row_group.hTargetRowGroupclass andTargetRowGroupsalias.row_ranges.h.src/paimon/format/parquet/parquet_file_batch_reader.hTargetRowGroupsinstead of rawstd::vector<int32_t>.src/paimon/format/parquet/parquet_file_batch_reader.cppSetReadSchema, switched row-group candidate list toTargetRowGroupsand applied bitmap filter before page-index filtering.Tests
BitmapAllPagesSomeRowGroupsBitmapPartialPagesSingleRowGroupBitmapAllAndPartialPagesMixedBitmapAllPagesWithPredicateBitmapPartialPagesWithPredicateBitmapMixedWithPredicateNestedMapBitmapFallbackNestedListBitmapFallbackNestedStructBitmapFallbackDatasets
Multiple performance testing were conducted on 2 dataset.
Test Methodology
To validate bitmap behavior, we organize tests into six groups based on bitmap distribution patterns. Each group contains five distinct datasets, and the final result reported is the average of these five runs.
Test Cases
1010101010...).Test Results
On Dataset#1
On Dataset#2
API and Format
Documentation
Generative AI tooling