Skip to content

perf: Skip RowFilter when all predicate columns are in the projection#20417

Open
darmie wants to merge 5 commits intoapache:mainfrom
darmie:fix-parquet-filter-pushdown
Open

perf: Skip RowFilter when all predicate columns are in the projection#20417
darmie wants to merge 5 commits intoapache:mainfrom
darmie:fix-parquet-filter-pushdown

Conversation

@darmie
Copy link

@darmie darmie commented Feb 17, 2026

Which issue does this PR close?

Rationale for this change

When pushdown_filters = true and all predicate columns are already in the output projection, the arrow-rs RowFilter (late materialization) machinery provides zero I/O benefit — those columns must be decoded for the projection anyway. Yet the RowFilter adds substantial CPU overhead from CachedArrayReader, ReadPlanBuilder::with_predicate, and ParquetDecoderState::try_next_batch (~1100 extra CPU samples on Q10 flamegraph). This causes regressions on 15 of the 43 ClickBench queries.

See profiling details.

What changes are included in this PR?

In opener.rs, before calling build_row_filter(), check whether all predicate column indices are a subset of the projection column indices. If so:

  • Skip build_row_filter() entirely (no RowFilter overhead)
  • Apply the predicate as a vectorized batch filter post-decode using batch_filter()
  • Filter out empty batches from the stream

If not a subset (i.e., there are non-projected columns that could be skipped), proceed with the RowFilter path as before.

ClickBench results on key regression queries (pushdown ON, fix vs baseline):

  • Q19: 0.46x vs baseline (fully fixed — faster than pushdown OFF)
  • Q26: 0.53x vs baseline (fully fixed)
  • Q10, Q11, Q25: 12-19% improvement vs baseline

Are these changes tested?

Yes. Added test_skip_row_filter_when_filter_cols_subset_of_projection which validates:

  1. Batch filter path (filter cols ⊆ projection): correct row counts and values
  2. RowFilter path (filter cols ⊄ projection): correct filtered values
  3. Batch filter with no matches: 0 rows, 0 batches (empty batches filtered)

All existing tests pass (81 tests in datafusion-datasource-parquet).

Are there any user-facing changes?

No. Behavior is identical — queries return the same results. Performance improves for queries where filter columns overlap with projection columns when pushdown_filters = true.

When all predicate columns are in the output projection, late
materialization provides no I/O benefit. Replace the expensive
RowFilter path with a lightweight batch-level filter to avoid
CachedArrayReader/ReadPlanBuilder/try_next_batch overhead.
Add a dedicated test verifying that when all predicate columns are in
the output projection, the opener skips RowFilter and applies a batch
filter instead — and that both the batch filter and RowFilter paths
produce correct results.

Simplify the 4-way stream branching into two independent steps:
first apply the empty-batch filter, then optionally wrap with
EarlyStoppingStream.
@github-actions github-actions bot added the datasource Changes to the datasource crate label Feb 17, 2026
Skip dynamic filter expressions (TopK, join pushdown) when deciding
whether a predicate is single-conjunct. This preserves the batch
filter optimization for queries like Q25 (WHERE col <> '' ORDER BY
col LIMIT N) where TopK adds runtime conjuncts, while still routing
multi-conjunct static predicates through RowFilter for incremental
evaluation.
@Dandandan
Copy link
Contributor

run benchmark clickbench_partitioned
DATAFUSION_EXECUTION_PARQUET_PUSHDOWN_FILTERS=true
DATAFUSION_EXECUTION_PARQUET_REORDER_FILTERS=true

@alamb-ghbot
Copy link

🤖 ./gh_compare_branch.sh gh_compare_branch.sh Running
Linux aal-dev 6.14.0-1018-gcp #19~24.04.1-Ubuntu SMP Wed Sep 24 23:23:09 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Comparing fix-parquet-filter-pushdown (d7ff890) to 468b690 diff using: clickbench_partitioned
Results will be posted here when complete

@alamb-ghbot
Copy link

🤖: Benchmark completed

Details

Comparing HEAD and fix-parquet-filter-pushdown
--------------------
Benchmark clickbench_partitioned.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃        HEAD ┃ fix-parquet-filter-pushdown ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0  │     2.73 ms │                     2.74 ms │     no change │
│ QQuery 1  │    53.99 ms │                    52.67 ms │     no change │
│ QQuery 2  │   131.93 ms │                   133.78 ms │     no change │
│ QQuery 3  │   159.93 ms │                   157.39 ms │     no change │
│ QQuery 4  │  1038.49 ms │                  1023.11 ms │     no change │
│ QQuery 5  │  1301.27 ms │                  1322.56 ms │     no change │
│ QQuery 6  │    18.18 ms │                    17.52 ms │     no change │
│ QQuery 7  │    66.92 ms │                    53.90 ms │ +1.24x faster │
│ QQuery 8  │  1402.44 ms │                  1404.75 ms │     no change │
│ QQuery 9  │  1848.46 ms │                  1843.40 ms │     no change │
│ QQuery 10 │   472.82 ms │                   355.89 ms │ +1.33x faster │
│ QQuery 11 │   532.90 ms │                   422.79 ms │ +1.26x faster │
│ QQuery 12 │  1365.85 ms │                  1202.05 ms │ +1.14x faster │
│ QQuery 13 │  2061.40 ms │                  1873.72 ms │ +1.10x faster │
│ QQuery 14 │  1394.49 ms │                  1244.36 ms │ +1.12x faster │
│ QQuery 15 │  1159.31 ms │                  1207.16 ms │     no change │
│ QQuery 16 │  2491.28 ms │                  2509.99 ms │     no change │
│ QQuery 17 │  2404.69 ms │                  2480.71 ms │     no change │
│ QQuery 18 │  4550.11 ms │                  4641.62 ms │     no change │
│ QQuery 19 │   140.88 ms │                   120.08 ms │ +1.17x faster │
│ QQuery 20 │  1878.13 ms │                  1848.48 ms │     no change │
│ QQuery 21 │  2286.10 ms │                  2172.65 ms │     no change │
│ QQuery 22 │  3952.49 ms │                  3772.31 ms │     no change │
│ QQuery 23 │  1098.44 ms │                  5938.64 ms │  5.41x slower │
│ QQuery 24 │   246.83 ms │                   219.46 ms │ +1.12x faster │
│ QQuery 25 │   635.97 ms │                   456.41 ms │ +1.39x faster │
│ QQuery 26 │   343.40 ms │                   232.27 ms │ +1.48x faster │
│ QQuery 27 │  2967.57 ms │                  2446.80 ms │ +1.21x faster │
│ QQuery 28 │ 24025.61 ms │                 24439.56 ms │     no change │
│ QQuery 29 │   978.89 ms │                   967.36 ms │     no change │
│ QQuery 30 │  1286.45 ms │                  1288.92 ms │     no change │
│ QQuery 31 │  1343.48 ms │                  1320.07 ms │     no change │
│ QQuery 32 │  4379.76 ms │                  4069.56 ms │ +1.08x faster │
│ QQuery 33 │  4966.18 ms │                  5058.23 ms │     no change │
│ QQuery 34 │  5306.38 ms │                  5682.03 ms │  1.07x slower │
│ QQuery 35 │  1826.82 ms │                  1838.97 ms │     no change │
│ QQuery 36 │   179.55 ms │                   183.06 ms │     no change │
│ QQuery 37 │    86.27 ms │                    88.65 ms │     no change │
│ QQuery 38 │    87.89 ms │                    88.64 ms │     no change │
│ QQuery 39 │   275.95 ms │                   278.41 ms │     no change │
│ QQuery 40 │    55.55 ms │                    60.25 ms │  1.08x slower │
│ QQuery 41 │    50.36 ms │                    49.59 ms │     no change │
│ QQuery 42 │    35.69 ms │                    38.21 ms │  1.07x slower │
└───────────┴─────────────┴─────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary                          ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (HEAD)                          │ 80891.84ms │
│ Total Time (fix-parquet-filter-pushdown)   │ 84608.72ms │
│ Average Time (HEAD)                        │  1881.21ms │
│ Average Time (fix-parquet-filter-pushdown) │  1967.64ms │
│ Queries Faster                             │         12 │
│ Queries Slower                             │          4 │
│ Queries with No Change                     │         27 │
│ Queries with Failure                       │          0 │
└────────────────────────────────────────────┴────────────┘

@Dandandan
Copy link
Contributor

run benchmark tpch
DATAFUSION_EXECUTION_PARQUET_PUSHDOWN_FILTERS=true
DATAFUSION_EXECUTION_PARQUET_REORDER_FILTERS=true

@alamb-ghbot
Copy link

🤖 ./gh_compare_branch.sh gh_compare_branch.sh Running
Linux aal-dev 6.14.0-1018-gcp #19~24.04.1-Ubuntu SMP Wed Sep 24 23:23:09 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Comparing fix-parquet-filter-pushdown (d7ff890) to 468b690 diff using: tpch
Results will be posted here when complete

@alamb-ghbot
Copy link

🤖: Benchmark completed

Details

Comparing HEAD and fix-parquet-filter-pushdown
--------------------
Benchmark tpch_sf1.json
--------------------
┏━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query     ┃      HEAD ┃ fix-parquet-filter-pushdown ┃        Change ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 1  │ 186.45 ms │                   185.05 ms │     no change │
│ QQuery 2  │  95.00 ms │                    88.27 ms │ +1.08x faster │
│ QQuery 3  │ 170.08 ms │                   116.11 ms │ +1.46x faster │
│ QQuery 4  │ 135.47 ms │                   137.20 ms │     no change │
│ QQuery 5  │ 292.90 ms │                   293.04 ms │     no change │
│ QQuery 6  │ 203.81 ms │                   204.16 ms │     no change │
│ QQuery 7  │ 259.28 ms │                   222.75 ms │ +1.16x faster │
│ QQuery 8  │ 309.29 ms │                   256.97 ms │ +1.20x faster │
│ QQuery 9  │ 411.56 ms │                   306.49 ms │ +1.34x faster │
│ QQuery 10 │ 275.71 ms │                   266.22 ms │     no change │
│ QQuery 11 │  73.77 ms │                    67.02 ms │ +1.10x faster │
│ QQuery 12 │ 257.27 ms │                   258.47 ms │     no change │
│ QQuery 13 │ 210.86 ms │                   214.71 ms │     no change │
│ QQuery 14 │ 108.99 ms │                   113.14 ms │     no change │
│ QQuery 15 │ 196.28 ms │                   194.45 ms │     no change │
│ QQuery 16 │  78.51 ms │                    65.16 ms │ +1.20x faster │
│ QQuery 17 │ 225.58 ms │                   239.66 ms │  1.06x slower │
│ QQuery 18 │ 479.65 ms │                   483.12 ms │     no change │
│ QQuery 19 │ 148.75 ms │                   157.90 ms │  1.06x slower │
│ QQuery 20 │ 150.52 ms │                   157.63 ms │     no change │
│ QQuery 21 │ 340.50 ms │                   340.16 ms │     no change │
│ QQuery 22 │  64.36 ms │                    61.32 ms │     no change │
└───────────┴───────────┴─────────────────────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Benchmark Summary                          ┃           ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ Total Time (HEAD)                          │ 4674.57ms │
│ Total Time (fix-parquet-filter-pushdown)   │ 4429.01ms │
│ Average Time (HEAD)                        │  212.48ms │
│ Average Time (fix-parquet-filter-pushdown) │  201.32ms │
│ Queries Faster                             │         7 │
│ Queries Slower                             │         2 │
│ Queries with No Change                     │        13 │
│ Queries with Failure                       │         0 │
└────────────────────────────────────────────┴───────────┘

@Dandandan
Copy link
Contributor

run benchmark tpcds
DATAFUSION_EXECUTION_PARQUET_PUSHDOWN_FILTERS=true
DATAFUSION_EXECUTION_PARQUET_REORDER_FILTERS=true

@alamb-ghbot
Copy link

🤖 ./gh_compare_branch.sh gh_compare_branch.sh Running
Linux aal-dev 6.14.0-1018-gcp #19~24.04.1-Ubuntu SMP Wed Sep 24 23:23:09 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Comparing fix-parquet-filter-pushdown (d7ff890) to 468b690 diff using: tpcds
Results will be posted here when complete

@Dandandan
Copy link
Contributor

show benchmark queue

@alamb-ghbot
Copy link

🤖 Hi @Dandandan, you asked to view the benchmark queue (#20417 (comment)).

Job User Benchmarks Comment
20417_3919093898.sh Dandandan tpcds (env: DATAFUSION_EXECUTION_PARQUET_PUSHDOWN_FILTERS=true DATAFUSION_EXECUTION_PARQUET_REORDER_FILTERS=true) https://github.com/apache/datafusion/pull/20417#issuecomment-3919093898

Change is_subset to strict equality for predicate vs projection
column indices. When there are non-predicate projection columns
(e.g. SELECT * WHERE col = X), RowFilter provides significant value
by skipping their decode for non-matching rows. Only skip RowFilter
when every projected column is a predicate column.

Also exclude dynamic filter expressions (TopK, join pushdown) when
counting conjuncts, so runtime-generated filters don't prevent the
batch filter optimization for single static predicates.
@Dandandan
Copy link
Contributor

run benchmark clickbench_partitioned
DATAFUSION_EXECUTION_PARQUET_PUSHDOWN_FILTERS=true
DATAFUSION_EXECUTION_PARQUET_REORDER_FILTERS=true

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

Labels

datasource Changes to the datasource crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants