Skip to content

feat(parquet): eager pruning during planning to refine scan statistics - #25328

Open
athlcode wants to merge 20 commits into
apache:mainfrom
athlcode:prune_parquet
Open

athlcode wants to merge 20 commits into
apache:mainfrom
athlcode:prune_parquet

Conversation

@athlcode

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

Parquet row group, page index and bloom filter pruning only happens when a scan executes, so the optimizer plans with statistics for entire files. When data is clustered on a filtered column, the estimates can be far off. On the IMDB movie_info table sorted by info, info IN ('Bulgaria') is estimated at 14.8M rows, while page index pruning narrows the scan to 15,360 rows. Better estimates at planning time can improve join selection and enable follow-ups such as eager fetching of small scans (#24922).

What changes are included in this PR?

  • New options datafusion.execution.parquet.eager_pruning (disabled (default), row_groups, page_index, bloom_filters) and eager_pruning_file_limit (default 256). Both can also be set per table via format. options.
  • API change: FileFormat::create_physical_plan takes a new filters: &[Arc<dyn PhysicalExpr>] argument. ListingTable passes its non-partition filters as hints.
  • When enabled, ParquetFormat runs the same pruning stages during planning:
    • drops files where no row can match,
    • attaches the resulting ParquetAccessPlan to each file, with fully matched flags cleared since the scan may execute with a different predicate,
    • rebuilds file statistics as inexact from the row groups that are read.
  • EXPLAIN shows the outcome, e.g. eager_pruning=[level=row_groups, files_pruned=0/1, row_groups_pruned=8/10, rows_pruned=800/1000].
  • Refactor: the bloom filter loading and file schema coercion are shared between the opener and planning.
  • Proto fields for the new options, plus updates to configs.md and the 56.0.0 upgrade guide.

What is the testing strategy for this PR?

  • Unit tests in datasource-parquet/src/eager_pruning.rs (statistics rules, file limit, fully matched flags, display).
  • Integration tests in core/tests/parquet/eager_pruning.rs:
    • per-level estimates, file pruning, file limit, per-table options, partition columns,
    • results identical to pruning disabled at every level, with filter pushdown on and off,
    • correct results when attached access plans are lost (as with plan serialization),
    • a page index pass that prunes nothing leaves files unchanged,
    • collect_statistics = false.
  • parquet_eager_pruning.slt covering EXPLAIN output for each level.
  • Proto round-trip tests, including messages without the new fields.
  • IMDB benchmark (local disk): on sorted movie_info, the estimate goes from 14.8M to 15,360 rows at page_index, with under 0.5 ms of added planning. On this data query 3b got about 10% slower because the better estimates changed the join plan. That is worth discussing.

Are there any user-facing changes?

  • New config options. Eager pruning is disabled by default, so nothing changes unless it is enabled.
  • Breaking API change to FileFormat::create_physical_plan (needs the api change label). Migration notes are in the upgrade guide.
  • Known limitations: encrypted files and tables with virtual columns are skipped. Execution still re-evaluates page indexes and bloom filters; a follow-up issue will cover avoiding that.

@github-actions github-actions Bot added documentation Improvements or additions to documentation core Core DataFusion crate sqllogictest SQL Logic Tests (.slt) catalog Related to the catalog crate common Related to common crate proto Related to proto crate datasource Changes to the datasource crate labels Sep 15, 2026
@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion v55.1.0 (current)
       Built [  62.211s] (current)
     Parsing datafusion v55.1.0 (current)
      Parsed [   0.036s] (current)
    Building datafusion v55.1.0 (baseline)
       Built [  61.819s] (baseline)
     Parsing datafusion v55.1.0 (baseline)
      Parsed [   0.036s] (baseline)
    Checking datafusion v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.585s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [ 126.529s] datafusion
    Building datafusion-catalog-listing v55.1.0 (current)
       Built [  43.236s] (current)
     Parsing datafusion-catalog-listing v55.1.0 (current)
      Parsed [   0.011s] (current)
    Building datafusion-catalog-listing v55.1.0 (baseline)
       Built [  43.246s] (baseline)
     Parsing datafusion-catalog-listing v55.1.0 (baseline)
      Parsed [   0.012s] (baseline)
    Checking datafusion-catalog-listing v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.088s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  88.059s] datafusion-catalog-listing
    Building datafusion-common v55.1.0 (current)
       Built [  35.218s] (current)
     Parsing datafusion-common v55.1.0 (current)
      Parsed [   0.064s] (current)
    Building datafusion-common v55.1.0 (baseline)
       Built [  34.711s] (baseline)
     Parsing datafusion-common v55.1.0 (baseline)
      Parsed [   0.066s] (baseline)
    Checking datafusion-common v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.652s] 223 checks: 222 pass, 1 fail, 0 warn, 31 skip

--- failure constructible_struct_adds_field: struct exhaustively constructible through public API adds field ---

Description:
A pub struct that could be exhaustively constructed with a literal using only public API has a new pub field, breaking existing exhaustive literals.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field ParquetOptions.eager_pruning in /home/runner/work/datafusion/datafusion/datafusion/common/src/config.rs:1381
  field ParquetOptions.eager_pruning_file_limit in /home/runner/work/datafusion/datafusion/datafusion/common/src/config.rs:1381

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  71.873s] datafusion-common
    Building datafusion-datasource v55.1.0 (current)
       Built [  46.080s] (current)
     Parsing datafusion-datasource v55.1.0 (current)
      Parsed [   0.031s] (current)
    Building datafusion-datasource v55.1.0 (baseline)
       Built [  46.111s] (baseline)
     Parsing datafusion-datasource v55.1.0 (baseline)
      Parsed [   0.034s] (baseline)
    Checking datafusion-datasource v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.280s] 223 checks: 222 pass, 1 fail, 0 warn, 31 skip

--- failure trait_method_parameter_count_changed: pub trait method parameter count changed ---

Description:
A trait method now takes a different number of parameters.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#trait-item-signature
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/trait_method_parameter_count_changed.ron

Failed in:
  FileFormat::create_physical_plan now takes 3 instead of 2 parameters, in file /home/runner/work/datafusion/datafusion/datafusion/datasource/src/file_format.rs:201

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  93.909s] datafusion-datasource
    Building datafusion-datasource-arrow v55.1.0 (current)
       Built [  46.569s] (current)
     Parsing datafusion-datasource-arrow v55.1.0 (current)
      Parsed [   0.011s] (current)
    Building datafusion-datasource-arrow v55.1.0 (baseline)
       Built [  46.654s] (baseline)
     Parsing datafusion-datasource-arrow v55.1.0 (baseline)
      Parsed [   0.012s] (baseline)
    Checking datafusion-datasource-arrow v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.078s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  94.516s] datafusion-datasource-arrow
    Building datafusion-datasource-avro v55.1.0 (current)
       Built [  47.539s] (current)
     Parsing datafusion-datasource-avro v55.1.0 (current)
      Parsed [   0.011s] (current)
    Building datafusion-datasource-avro v55.1.0 (baseline)
       Built [  47.478s] (baseline)
     Parsing datafusion-datasource-avro v55.1.0 (baseline)
      Parsed [   0.011s] (baseline)
    Checking datafusion-datasource-avro v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.081s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  96.266s] datafusion-datasource-avro
    Building datafusion-datasource-csv v55.1.0 (current)
       Built [  46.599s] (current)
     Parsing datafusion-datasource-csv v55.1.0 (current)
      Parsed [   0.011s] (current)
    Building datafusion-datasource-csv v55.1.0 (baseline)
       Built [  46.563s] (baseline)
     Parsing datafusion-datasource-csv v55.1.0 (baseline)
      Parsed [   0.014s] (baseline)
    Checking datafusion-datasource-csv v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.114s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  94.429s] datafusion-datasource-csv
    Building datafusion-datasource-json v55.1.0 (current)
       Built [  47.865s] (current)
     Parsing datafusion-datasource-json v55.1.0 (current)
      Parsed [   0.013s] (current)
    Building datafusion-datasource-json v55.1.0 (baseline)
       Built [  47.494s] (baseline)
     Parsing datafusion-datasource-json v55.1.0 (baseline)
      Parsed [   0.013s] (baseline)
    Checking datafusion-datasource-json v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.096s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  96.671s] datafusion-datasource-json
    Building datafusion-datasource-parquet v55.1.0 (current)
       Built [  52.721s] (current)
     Parsing datafusion-datasource-parquet v55.1.0 (current)
      Parsed [   0.035s] (current)
    Building datafusion-datasource-parquet v55.1.0 (baseline)
       Built [  54.148s] (baseline)
     Parsing datafusion-datasource-parquet v55.1.0 (baseline)
      Parsed [   0.038s] (baseline)
    Checking datafusion-datasource-parquet v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.174s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [ 108.411s] datafusion-datasource-parquet
    Building datafusion-proto-common v55.1.0 (current)
       Built [  23.444s] (current)
     Parsing datafusion-proto-common v55.1.0 (current)
      Parsed [   0.047s] (current)
    Building datafusion-proto-common v55.1.0 (baseline)
       Built [  23.260s] (baseline)
     Parsing datafusion-proto-common v55.1.0 (baseline)
      Parsed [   0.049s] (baseline)
    Checking datafusion-proto-common v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   1.155s] 223 checks: 222 pass, 1 fail, 0 warn, 31 skip

--- failure constructible_struct_adds_field: struct exhaustively constructible through public API adds field ---

Description:
A pub struct that could be exhaustively constructed with a literal using only public API has a new pub field, breaking existing exhaustive literals.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field ParquetOptions.eager_pruning in /home/runner/work/datafusion/datafusion/datafusion/proto-common/src/generated/prost.rs:880
  field ParquetOptions.eager_pruning_file_limit_opt in /home/runner/work/datafusion/datafusion/datafusion/proto-common/src/generated/prost.rs:916
  field ParquetOptions.eager_pruning in /home/runner/work/datafusion/datafusion/datafusion/proto-common/src/generated/prost.rs:880
  field ParquetOptions.eager_pruning_file_limit_opt in /home/runner/work/datafusion/datafusion/datafusion/proto-common/src/generated/prost.rs:916
  field ParquetOptions.eager_pruning in /home/runner/work/datafusion/datafusion/datafusion/proto-common/src/generated/prost.rs:880
  field ParquetOptions.eager_pruning_file_limit_opt in /home/runner/work/datafusion/datafusion/datafusion/proto-common/src/generated/prost.rs:916

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  49.176s] datafusion-proto-common
    Building datafusion-proto-models v55.1.0 (current)
       Built [  26.518s] (current)
     Parsing datafusion-proto-models v55.1.0 (current)
      Parsed [   0.136s] (current)
    Building datafusion-proto-models v55.1.0 (baseline)
       Built [  26.335s] (baseline)
     Parsing datafusion-proto-models v55.1.0 (baseline)
      Parsed [   0.138s] (baseline)
    Checking datafusion-proto-models v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   1.870s] 223 checks: 222 pass, 1 fail, 0 warn, 31 skip

--- failure constructible_struct_adds_field: struct exhaustively constructible through public API adds field ---

Description:
A pub struct that could be exhaustively constructed with a literal using only public API has a new pub field, breaking existing exhaustive literals.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field ParquetOptions.eager_pruning in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/datafusion_proto_common.rs:880
  field ParquetOptions.eager_pruning_file_limit_opt in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/datafusion_proto_common.rs:916
  field ParquetOptions.eager_pruning in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/datafusion_proto_common.rs:880
  field ParquetOptions.eager_pruning_file_limit_opt in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/datafusion_proto_common.rs:916

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  56.205s] datafusion-proto-models
    Building datafusion-sqllogictest v55.1.0 (current)
       Built [ 104.631s] (current)
     Parsing datafusion-sqllogictest v55.1.0 (current)
      Parsed [   0.022s] (current)
    Building datafusion-sqllogictest v55.1.0 (baseline)
       Built [ 105.091s] (baseline)
     Parsing datafusion-sqllogictest v55.1.0 (baseline)
      Parsed [   0.023s] (baseline)
    Checking datafusion-sqllogictest v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.096s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [ 212.826s] datafusion-sqllogictest

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Sep 15, 2026
@codecov-commenter

codecov-commenter commented Sep 15, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.10818% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.63%. Comparing base (62a65a1) to head (d928123).

Files with missing lines Patch % Lines
datafusion/datasource-parquet/src/eager_pruning.rs 96.76% 11 Missing and 14 partials ⚠️
datafusion/proto-common/src/generated/pbjson.rs 60.71% 4 Missing and 7 partials ⚠️
datafusion/catalog-listing/src/table.rs 66.66% 5 Missing and 1 partial ⚠️
datafusion/datasource-parquet/src/bloom_filter.rs 90.74% 4 Missing and 1 partial ⚠️
datafusion/proto-common/src/from_proto/mod.rs 93.10% 0 Missing and 2 partials ⚠️
datafusion/proto-models/src/from_proto.rs 93.93% 0 Missing and 2 partials ⚠️
datafusion/datasource-parquet/src/source.rs 90.90% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25328      +/-   ##
==========================================
+ Coverage   82.60%   82.63%   +0.02%     
==========================================
  Files        1144     1145       +1     
  Lines      442904   443867     +963     
  Branches   442904   443867     +963     
==========================================
+ Hits       365881   366793     +912     
- Misses      54870    54887      +17     
- Partials    22153    22187      +34     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@athlcode

Copy link
Copy Markdown
Contributor Author

@tschwarzinger Please do review when you get a chance and add other reviewers if required, thank you

@tschwarzinger

tschwarzinger commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

@athlcode thank you for working on this! Sorry for the late reply, I've been on vacation.

The approach looks sound by quickly scrolling over it. I do have two reservations before doing a full review:

  1. Did you talk to some of the other people whether this is something they want in DataFusion? That was one of my main question when creating the issue. I've added a comment to Epic: Improve TPC-H and TPC-DS cardinality estimates #25610 to maybe get some further opinions on it.
  2. Do you have a benchmark that shows the performance benefits of this approach? For this to work, we would need a query where the more accurate statistics lead to a better join order and thus better performance.

Relevant issues are:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto detected api change Auto detected API change catalog Related to the catalog crate common Related to common crate core Core DataFusion crate datasource Changes to the datasource crate documentation Improvements or additions to documentation proto Related to proto crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Ability to Eagerly Prune Parquet Files for Better Statistics

3 participants