fix: prune row groups when file statistics collapse the predicate to a constant - #24770
fix: prune row groups when file statistics collapse the predicate to a constant#24770jensholdgaard wants to merge 1 commit into
Conversation
…stant Closes apache#24769. `constant_columns_from_stats` substitutes columns that file statistics prove constant — including the all-NULL case, which folds to a NULL literal. When the substituted predicate then simplifies to a bare constant (`NULL`, or `false`), `build_pruning_predicates` returns `None` because there are no column references left, and the opener falls through with no pruning at all. That makes the substitution strictly counterproductive for these files. A predicate like `col = <literal>` over a column whose statistics say `null_count == row_count` is provably unsatisfiable, and before the substitution existed it *was* pruned via the `col_null_count != row_count` conjunct of the pruning predicate. The regression bisects to apache#22969, which enabled `collect_statistics` by default and so began feeding the substitution on paths that previously had no file statistics. Recognise the collapsed-to-constant case explicitly: if the predicate is a `false`/NULL literal and the original predicate referenced a column that `constant_columns_from_stats` proved constant, skip every remaining row group and credit the skip to statistics pruning. Results are unchanged either way — a filter drops NULL and false rows alike — but the scan work is not. The `stats_constants_in_predicate` guard deliberately narrows the skip to collapses that file statistics contributed to. A predicate can also collapse via the missing-column adapter (schema evolution) or partition-value folding; both keep their existing behaviour — partition-driven collapse is already `FilePruner`'s job, and widening the skip to missing columns changes the raw scan output that nine `evolved_schema` / `test_pushdown_with_missing_*` tests assert. The regression test asserts the `row_groups_pruned_statistics` metric rather than the row count, since a row-count assertion cannot tell "pruned" from "scanned, then row-filtered": both yield zero rows.
37f208e to
3f41e77
Compare
|
The The nine failing The update narrows the skip with a
If maintainers would rather generalise the skip to the missing-column collapse as well, I'm happy to do that in a follow-up with the corresponding test updates — it just seemed wrong to smuggle a second behaviour change into a targeted regression fix. |
Note
This fix was investigated and written with AI assistance (Claude Code), posted with the account owner's review and consent.
Which issue does this PR close?
col = <literal>when statistics show the column is entirely NULL #24769.Rationale for this change
A query like
SELECT ... WHERE col = 'x'over a Parquet file whose statistics saycolis entirely NULL cannot match any row, and DataFusion 54 pruned such row groups from the footer without reading them. On 55 the same query reads them.The cause is an interaction rather than a bug in pruning itself.
constant_columns_from_statssubstitutes columns that file statistics prove constant, and its all-NULL branch folds such a column to a NULL literal. Once substituted, the predicate simplifies to a bare constant —NULLhere — at which pointbuild_pruning_predicatesreturnsNone(there are no column references left to build a pruning predicate over), andprune_row_groupsfalls through with no pruning at all.So for exactly the files where the statistics carry the most information, the substitution is strictly counterproductive: before it, the pruning predicate's own
col_null_count != row_countconjunct proved the row group empty and skipped it.This surfaced as a regression when bisecting a real workload (a Parquet log store where a
bodycolumn is NULL for the vast majority of rows) from 54 → 55. The bisect lands on #22969, which removedListingOptions::collect_statin favour of the session'sexecution.collect_statistics— defaulttrue. That change is correct in itself; it simply began feeding per-file statistics to the substitution on paths that previously had none, exposing the gap. Settingexecution.collect_statistics = falserestores pruning on 55, which is a useful confirmation but obviously not a fix.Results were never wrong — a filter drops NULL and false rows alike — but the scan work is real: row groups that used to be skipped from the footer are now decoded in full.
What changes are included in this PR?
prune_row_groupsnow recognises the collapsed-to-constant case: if the (post-substitution, post-simplification) predicate is afalseor NULL literal, every remaining row group is skipped and the skip is credited torow_groups_pruned_statistics. A smallRowGroupAccessPlanFilter::skip_allhelper is added alongside the existingprune_by_*methods.The check is deliberately narrow — a downcast to
Literalplus a NULL/falsevalue test — so it cannot affect predicates that still reference columns; those take the existing path unchanged.Are these changes tested?
Yes:
test_prune_all_null_column_equality_from_file_statisticsinopener/mod.rs, modelled on the neighbouringtest_prune_on_partition_values_and_file_statistics. It fails on currentmain(3 rows scanned, 0 pruned) and passes with this change.One note on how it asserts, in case it saves a reviewer time: it checks the
row_groups_pruned_statisticsmetric rather than the returned row count. A row-count assertion cannot distinguish "pruned" from "scanned, then row-filtered" — both give zero rows — and an earlier draft of this test passed against the unfixed code for exactly that reason.Verified
cargo test -p datafusion-datasource-parquet --lib opener::— 47 passed. The 16 pre-existingbloom_filter::testsfailures on this crate are unrelated and reproduce identically on an untouched checkout.Are there any user-facing changes?
No API or result changes. Queries that were already correct stay correct; affected scans read less data. Plans may show more row groups pruned, and
row_groups_pruned_statisticsincreases correspondingly.