Skip to content

[enhancement](scan) Optimize Parquet V2 direct predicate filtering - #66360

Merged
Gabriel39 merged 6 commits into
apache:branch-4.1from
Gabriel39:fix/parquet-v2-direct-predicate-filtering-4.1
Aug 3, 2026
Merged

[enhancement](scan) Optimize Parquet V2 direct predicate filtering#66360
Gabriel39 merged 6 commits into
apache:branch-4.1from
Gabriel39:fix/parquet-v2-direct-predicate-filtering-4.1

Conversation

@Gabriel39

@Gabriel39 Gabriel39 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Proposed changes

Backport the selected Parquet V2 direct-predicate filtering changes from #66261 to branch-4.1, then address the review and performance findings on this branch.

  • keep identity selection-vector state implicit and compact selected rows in bulk
  • retain the selection scratch high-water mark across scanner batches and specialize first compaction from implicit identity
  • refresh late runtime-filter requests at safe row-group boundaries
  • re-run footer-statistics pruning and reset adaptive predicate state for unopened row groups after a refresh
  • preserve real COUNT(*) carrier values while runtime filters are pending
  • initialize refreshed JNI predicates and attribute refresh work to TableReader/FileReader/Parquet profiles
  • preserve Hudi/Paimon child-reader predicate state
  • remove query-scoped dictionary-filter cache state
  • share immutable VDirectInPredicate pruning materialization across split-local expression clones
  • add correctness-checked selection and direct-IN lifecycle microbenchmarks

Test

  • ./run-be-ut.sh --run --filter='FileScannerV2Test.*:*Parquet*:*TableReaderTest.*:Hudi*ReaderTest.*:Paimon*ReaderTest.*:SelectionVectorTest.*:DictionaryFilterCostTest.*' -j48
    • 639 tests from 47 test suites passed under ASAN
  • targeted late-RF, COUNT(*), dictionary-snapshot, shared-IN-state, and SelectionVector tests
    • 19 tests from 5 test suites passed under ASAN
  • Release benchmark build and smoke run
    • expected registrations: 228 decoder, 92 kernel, 25 selection, 167 reader, and 8 expression-lifecycle cases
    • all 25 selection and 8 expression-lifecycle cases executed with zero benchmark errors
  • git diff --check

Selection-vector microbenchmark

The final benchmark source validates every surviving original row ID after the timed region. Base, pre-fix PR, and final binaries use the same benchmark source and Clang -O3 -DNDEBUG -mavx2 on the same host. Each comparison uses one pinned CPU, three warmups, eight adjacent A-B-B-A quartets, and at least 0.3 seconds per invocation. The table reports median paired CPU-time ratios; negative values are improvements.

Operation Final selectivity Final vs pre-fix PR Final vs branch base
Identity initialization 100% -15.23% -99.12%
Row filter 1% -24.23% -23.76%
Row filter 50% -16.10% -34.50%
Row filter 90% -45.91% -45.95%
Row filter 100% -31.72% -17.32%
Successive filters 1% -33.25% -35.79%
Successive filters 50% -29.80% -35.10%
Successive filters 90% -25.27% -25.16%
Successive filters 100% -24.93% -23.72%

All final-vs-base paired-ratio CVs are at most 5.85%. The previous 16.43%/59.94% dense row-filter regressions and 27.92%-61.05% successive-filter regressions are no longer present. Retaining _owned avoids repeated value initialization; the implicit-identity specialization removes the remaining source/coordinate branches from the first compaction.

Direct-IN expression lifecycle microbenchmark

FileScannerExpr/direct_in_clone_prepare_open isolates deep-clone, prepare, and open for an already prepared direct-IN runtime filter. Set construction and the original fragment prepare/open are outside the timed region. Shared and forced-rematerialization implementations run in the same Release binary on one pinned CPU, with 10 repetitions and at least 0.5 seconds per repetition.

IN values Rematerialize median Shared median Speedup
128 207.470 us 1.634 us 126.9x
1,024 1.674 ms 1.642 us 1,019.5x
8,192 13.514 ms 1.672 us 8,082.2x
65,536 108.337 ms 1.650 us 65,663.1x

The shared path remains approximately constant because split clones reuse the immutable, fragment-originated pruning state; the rematerialization path scales linearly with runtime-filter cardinality.

Reader-level microbenchmark

The earlier reader-level comparison used an identical Parquet-only Release benchmark binary on the same host, with one pinned CPU, warm cache, three warmups, A-B-B-A order, 10 repetitions, and at least one second per repetition. It covers nullable INT32 predicate scans with a lazy payload for PLAIN and dictionary encoding.

Encoding Selectivity CPU time vs base Wall time vs base
Dictionary 1% +0.44% +0.46%
Dictionary 10% +1.13% +1.22%
Dictionary 50% +1.41% +1.51%
Dictionary 90% +0.41% +0.53%
PLAIN 1% -0.65% -0.66%
PLAIN 10% +0.32% +0.38%
PLAIN 50% -1.21% -1.18%
PLAIN 90% -1.34% -1.29%

The reader-level point estimates span -1.34% to +1.41% CPU time with mixed signs, so this benchmark did not detect a material aggregate regression. It starts at format::parquet::ParquetReader; it does not cover scanner scheduling or end-to-end SQL execution.

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@Gabriel39
Gabriel39 marked this pull request as ready for review August 2, 2026 10:09
@Gabriel39
Gabriel39 requested a review from yiguolei as a code owner August 2, 2026 10:09

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes.

I found two PR-introduced issues in the late runtime-filter refresh path:

  1. [P1] A COUNT(*) carrier that is still represented by synthesized placeholder defaults can be promoted to a late predicate while the active Parquet Row Group continues returning defaults; Scanner immediately evaluates the new RF and can silently drop or retain the wrong rows.
  2. [P2] Activating the new request retains footer-planning and adaptive-prefetch state derived from the old predicate snapshot, losing cheap pruning and potentially warming wide lazy output before a selective RF is sampled.

Critical checkpoint summary:

  • Runtime-filter lifecycle and COUNT(*) semantics: blocked by the placeholder transition above.
  • Parquet Row Group pruning and adaptive lazy-read state: needs request versioning or equivalent invalidation at activation.
  • Direct OR/NULL behavior, fused SelectionVector compaction, dictionary-reader reuse, and MergeRange ordering: no additional PR-introduced defect after focused recheck.
  • Mapping/layout compatibility, condition-cache handling, JNI/Hudi/Paimon forwarding, and expression ownership: no additional issue found.
  • Coverage: the changed tests exercise request queuing/activation, but not mid-Row-Group COUNT(*) result correctness or request-versioned multi-Row-Group I/O.
  • User focus: no additional focus was supplied; the full 39-file diff was reviewed.
  • Validation: static review only as required; no builds or tests were run.

Comment thread be/src/format_v2/table_reader.cpp
Comment thread be/src/format_v2/parquet/parquet_scan.cpp
@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 84.35% (582/690) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 73.48% (30083/40941)
Line Coverage 57.34% (328626/573104)
Region Coverage 54.40% (274595/504815)
Branch Coverage 55.24% (122089/221028)

@Gabriel39
Gabriel39 force-pushed the fix/parquet-v2-direct-predicate-filtering-4.1 branch from db8ebab to 4b5c6c4 Compare August 2, 2026 14:58
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes.

The existing [P2] blocker remains valid and is not duplicated here: refreshed requests retain footer-planning and adaptive-prefetch state derived from the older predicate snapshot (comment 3698725295).

The live [P1] COUNT() comment 3698725294 is also not duplicated, but the final production-plan audit could not prove its required combination of storage-level COUNT() and a late runtime filter targeting the arbitrary retained scan carrier, so this review does not rely on it.

This review adds one distinct [P2]: late-runtime-filter request rebuilding and concrete-reader validation execute outside the owning TableReader/FileReader profile scopes, so refresh latency is visible only at the FileScannerV2 level.

Critical checkpoint summary:

  • Parquet request activation, pruning, and adaptive lazy-read state remain blocked by comment 3698725295.
  • Profile ownership for the new refresh lifecycle is covered by the new inline comment.
  • COUNT(*) late-filter mechanisms were examined but dismissed from this review because the current FE cannot form the required storage-COUNT-plus-scan-RF plan.
  • SelectionVector compaction, mapping/layout compatibility, condition-cache behavior, request ownership, and JNI/Hudi/Paimon forwarding exposed no additional production defect after reachability and duplicate checks.
  • Coverage still lacks request-versioned multi-row-group I/O assertions and an open-reader refresh profile assertion.
  • User focus: no additional focus was supplied; the full authoritative 26-file diff was reviewed.
  • Validation: static review only as required; no builds or tests were run.
  • Workflow limitation: the bundle-required repository code-review SKILL.md was absent after hidden/no-ignore repository search, so it could not be loaded.

Comment thread be/src/format_v2/table_reader.cpp
@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 78.11% (182/233) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 58.06% (24323/41896)
Line Coverage 42.13% (242629/575856)
Region Coverage 38.23% (191726/501510)
Branch Coverage 39.50% (87169/220670)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 78.88% (183/232) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 73.50% (30076/40921)
Line Coverage 57.36% (328530/572725)
Region Coverage 54.29% (273862/504418)
Branch Coverage 55.25% (121992/220797)

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Codex completed, but no new pull request review was submitted for the current head SHA.
Workflow run: https://github.com/apache/doris/actions/runs/30780814572

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@Gabriel39
Gabriel39 force-pushed the fix/parquet-v2-direct-predicate-filtering-4.1 branch from eca8f5e to 4445b7e Compare August 3, 2026 03:49
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I found three new issues: the implicit-selection reset repeatedly initializes the full owned index scratch on every filtered batch (matching the PR's measured dense/successive compaction regressions), the changed-dictionary regression branch preserves the same dictionary-ID bitmap and cannot catch stale reuse, and the mandatory benchmark guide still contains an obsolete conflicting registration count.

Critical checkpoint summary:

  • Late-runtime-filter lifecycle: active/pending request ownership, row-group/pending-output activation, fixed-layout fallback, and cache snapshot handling are otherwise sound. The existing COUNT(*) carrier, stale planning/adaptive-state, and refresh-profile threads remain the authoritative coverage for those issues.

  • Schema/layer boundaries: refreshed mapping stays file-local, incompatible scalar/nested/hidden/delete/virtual layouts fall back conservatively, and original mapping state remains the final-materialization owner.

  • JNI and hybrid readers: refreshed contexts are initialized at the executable lifecycle boundary and the active child receives the new snapshot; later split switches receive the current per-split snapshot.

  • Selection correctness: identity, row-index/compact-index coordinates, bounds, and cursor alignment remain correct; the new finding is the repeated hot-path initialization cost.

  • Tests and benchmarks: no builds or tests were run because this review runner explicitly forbids them. The PR supplies benchmark evidence, but the two inline coverage/record issues must be corrected. No repository code-review SKILL.md was present, so the required AGENTS.md files and mandatory FileScannerV2/Parquet guides were used directly.

User focus: no additional review focus was provided; the whole PR was reviewed.

Comment thread be/src/format_v2/parquet/selection_vector.h Outdated
Comment thread be/benchmark/parquet/AGENTS.md
Comment thread be/test/format_v2/parquet/parquet_scan_test.cpp Outdated
@Gabriel39
Gabriel39 force-pushed the fix/parquet-v2-direct-predicate-filtering-4.1 branch from 4445b7e to 4eeb65b Compare August 3, 2026 05:28
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes.

I found one new issue in this head: the new ParquetSelection benchmark validates only survivor count, so an implementation that keeps the wrong source-row identities can still publish accepted timings. The inline comment has the concrete failure mode and requested oracle.

Five previously raised issues remain visible in the current head, so I did not duplicate them inline:

  • [P1] The COUNT(*) placeholder can hide real carrier values before a late runtime-filter request activates.
  • [P2] Late-filter activation retains stale unopened Row Group planning and adaptive predicate state.
  • [P2] Runtime-filter refresh work is missing the owning TableReader and concrete-reader profile scopes.
  • [P2] SelectionVector clears and value-initializes its retained scratch on every filtered batch.
  • [P2] The dictionary-replacement test keeps the same matching entry-ID bitmap and cannot detect stale reuse.

The earlier benchmark-guide registration-count inconsistency is resolved in this head.

Critical checkpoints:

  • Correctness and row identity: active/pending request switching is safe for ordinary projected columns, pending output, EOF, repeated refreshes, and hybrid native/JNI forwarding. The known COUNT(*) carrier exception remains blocking.
  • Lifecycle and cache safety: no new cleanup, ownership, ConditionCache, or split-transition defect was substantiated; the pre-open stale-cache-key hypothesis was dismissed after caller and timing tracing.
  • Performance and observability: the existing stale planning/adaptive state, refresh profile attribution, and per-batch selection scratch regressions remain unresolved.
  • Compatibility and layer boundaries: fixed file-local layouts, mapper finalization, and Hudi/Paimon/JNI forwarding did not expose a new compatibility or ownership violation.
  • Tests and benchmarks: the new exact-survivor oracle issue and existing dictionary-snapshot oracle gap remain; the registration counts are now internally consistent.

No additional user-provided review focus was supplied, so I reviewed the full PR. This was a static review only; builds and tests were not run per the runner instructions.

Comment thread be/benchmark/parquet/benchmark_parquet_selection.hpp Outdated
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes.

I found two new P2 benchmark-validation defects in this head. Neither duplicates an existing thread:

  • The Direct-IN lifecycle benchmark times clone/prepare/open without ever proving that shared and rematerialized clones preserve the exact predicate truth set; its focused unit test's no-match assertion also passes for an empty or truncated set.
  • The SelectionVector identity case does not expose or validate the state written by resize(), so the resize can be a no-op or be eliminated while the default vector's implicit identity still passes every current check.

The inline comments give the concrete failure modes and deterministic oracle requirements.

Critical checkpoints:

  • Correctness and row identity: late runtime-filter evaluation, COUNT(*) carriers, pending predicate output, repeated refreshes, fixed local layouts, and SelectionVector coordinate/generation behavior did not expose another defect in the current head.
  • Lifecycle and cache safety: row-group-boundary activation, unopened-plan replanning, condition-cache behavior, adaptive-state reset, expression ownership, and close/error paths are internally consistent after the current fixes.
  • Performance and observability: refresh work is attributed through the TableReader/concrete-reader hierarchy and retained selection scratch is covered; the two new benchmark-oracle gaps still make part of the claimed performance evidence unreliable.
  • Compatibility and layer boundaries: shared Direct-IN pruning state remains immutable and type-stable on reachable clone/localization paths, while JNI/Hudi/Paimon forwarding preserves one independently owned prepared snapshot per active child.
  • Tests and benchmarks: registration counts and mandatory documentation are consistent, and the earlier dictionary and exact-survivor oracle gaps are fixed. The remaining lifecycle and identity cases need the independent validations described inline.

I did not duplicate the earlier threads; their author follow-ups and current-head changes were included in this review. No additional user-provided focus was supplied, so I reviewed the complete PR. This was a static review only; builds and tests were not run under the runner contract. The prompt-required repository code-review skill was not present in this checkout, so I followed the review prompt, both required AGENTS.md files, and the three FileScannerV2 design/review documents directly.

Comment thread be/benchmark/parquet/benchmark_file_scanner_expr.hpp
Comment thread be/benchmark/parquet/benchmark_parquet_selection.hpp
@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 87.35% (290/332) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 58.10% (24348/41910)
Line Coverage 42.19% (243092/576235)
Region Coverage 38.26% (192008/501824)
Branch Coverage 39.58% (87396/220820)

@Gabriel39 Gabriel39 changed the title [enhancement](scan) Backport Parquet V2 direct predicate filtering [enhancement](scan) Optimize Parquet V2 direct predicate filtering Aug 3, 2026
@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 87.84% (289/329) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 73.50% (30083/40931)
Line Coverage 57.35% (328634/573054)
Region Coverage 54.27% (273925/504721)
Branch Coverage 55.24% (122033/220933)

@Gabriel39
Gabriel39 merged commit a9641f1 into apache:branch-4.1 Aug 3, 2026
39 of 48 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.

3 participants