Repository navigation
Re-check dynamic filters before a Parquet file's first data read - #170
Merged
Merged
Conversation
A Parquet file range claimed before a TopK dynamic filter is first updated (opened ahead by `file_stream_open_ahead`, or claimed by a partition whose first byte range was an empty sliver) passes file and row-group pruning against the still-empty filter. For a single-row-group file the per-batch early stop then drops it only after its whole row group has been fetched. Add `datafusion.execution.recheck_dynamic_filter_before_read` (default false). When enabled, `EarlyStoppingStream` runs its existing file-pruner check once before the first poll of the inner stream, so such a file is skipped without any row-group I/O when the filter has since been updated to exclude it. The pruner only rebuilds its pruning predicate when a watched filter changed, and a file stopped this way is counted like an early-stopped file in `files_ranges_pruned_statistics`. A new `files_ranges_pruned_before_read` metric counts these ranges; it is only registered when the option is on and the scan predicate holds a dynamic filter, so EXPLAIN ANALYZE output is otherwise unchanged. The option is read from the session when a scan partition is opened, through a new `FileSource::with_execution_options` hook, so it also reaches Parquet sources that custom table providers build with default options. The re-check only runs where the early-stop wrapper already exists (the predicate holds a dynamic filter that is still incomplete when the file's stream is built) and applies the same pruning rule as the per-batch check. Hash-join filters are marked complete before the probe side is first polled (unless probe buffering is enabled), so join probe scans are unaffected. Tests: - opener unit tests: a file stopped before its first read makes zero data reads, while without the re-check the same file is fetched first - gated object store integration tests at 1 partition and at 5 partitions (10 files split into 14 byte ranges, 4 of them empty) asserting row-group fetches, bytes_scanned and pruning metrics with the option off and on, plus a check that scans without a dynamic filter are unchanged - randomized differential fuzz test comparing option off and on against a full-sort oracle on a multi-threaded runtime with delayed I/O; it asserts that the re-check skipped files in some cases. NaN keys are only written to files without statistics: Parquet min/max exclude NaN, and the scan's constant-column rewrite and statistics pruning currently mishandle NaN independently of this option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ranges with no row group left (empty byte-range slivers, fully pruned ranges) and files with a RowGroupPruner no longer get the pre-read re-check, so files_ranges_pruned_before_read counts only skipped fetches. The option doc now says which statistics the check uses, which filters it applies to, and that multi-row-group files are already re-checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
datafusion.execution.recheck_dynamic_filter_before_read(defaultfalse). When it is on, a Parquet scan whose predicate holds a dynamic filter that can still change re-checks the file pruner once, right before the file's first data read. A file range claimed early is then skipped without fetching its row group if the threshold published meanwhile excludes it. A range is claimed early byfile_stream_open_ahead, or by a partition that drew an empty byte-range sliver.Why:
ORDER BY key LIMIT kover many single-row-group files. Each partition claims and opens its next file, and evaluatesprune_file, before the TopK heap holds k rows. So those files pass pruning against the emptylit(true)filter, and their row groups are fetched and decoded even though the early-stop check then drops them after the first batch.Measured on SPCS through Rustice (SF1 lineitem, 10 files,
LIMIT 10):Mechanism
EarlyStoppingStreamruns its existingFilePruner::should_prunecheck once before the first inner poll. The pruning predicate is rebuilt only when a watched filter changed; otherwise the check is an atomic load. A file stopped this way counts like an early-stopped file infiles_ranges_pruned_statistics.RowGroupPruner. Multi-row-group files are already checked before each row group.files_ranges_pruned_before_read. It is registered only when the option is on and the predicate holds a dynamic filter, so defaultEXPLAIN ANALYZEoutput is unchanged.FileSource::with_execution_options(&ExecutionOptions) -> Option<Arc<dyn FileSource>>, defaultNone.FileScanConfig::open_with_argscalls it with the task's session config, so sources built with default options get the setting too.ParquetSourceclones itself only when the value differs.With the option off, behaviour is unchanged: no wrapper, metric or clone.
Tests
core/tests/parquet/topk_recheck_before_read.rs): metadata reads return at once and data reads yield first.core/tests/fuzz_cases/topk_recheck_before_read_fuzz.rs,extended_tests). It runs on a multi-thread runtime over a delayed store, comparing TopK with the option on vs off vs a full-sort oracle, every column by row id. Covered: one/two keys, ASC/DESC, NULLS FIRST/LAST, ties, Int64/Float64/Utf8/Decimal128/Date keys, files with and without statistics, constant-key files, batch sizes 1..8192. Seed and config are logged (TOPK_RECHECK_FUZZ_SEED,TOPK_RECHECK_FUZZ_CASES). Run on 3 × 256 seeded cases after the last change, and on ~1,900 cases before it: all pass.cargo test -p datafusion-datasource-parquet,parquet_integration(topk_recheck),information_schema.sltsettings rows,configs.md.cargo clippy --no-deps -D warningson the changed crates.cargo fmt.Found while fuzzing, not changed here: NaN rows. Parquet min/max exclude NaN, so
constant_value_from_statstreats{1.0, NaN}as the constant1.0, and max-side pruning can drop NaN rows. NaN cases are therefore written only to files without statistics, and a separate fix will follow.🤖 Generated with Claude Code