perf: cache residuals during scan planning#3654
Open
dossett wants to merge 3 commits into
Open
Conversation
Author
|
In the Iceberg slack @jayceslesar pointed out that #2695 addresses a related problem. One key objection in that PR was that the (cc @jayceslesar @Fokko @kevinjqliu @ForeverAngry from that first PR) |
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.
I used CODEX to analyze this problem and create this PR. I've reviewed the code and test and stand by them. This summary is written completely by a human (me) other than very light copy editing by an LLM.
Problem: During planning, a predicate is applied to determine the appropriate contents. Parts of the predicate can be removed because they're already satisfied by the partition scheme in a table. What can't be removed is the
residual. Currently, a residual is constructed for every file. However, files written with the same partition spec and the same relevant partition values will have the same residual, so recalculating it is wasteful.This change caches the file-level residual, but not the mutable
ResidualEvaluator. On a cache miss, it constructs a fresh evaluator, calculates and caches the residual, and then discards the evaluator. The cache key contains the file's partition spec ID and the values of partition fields whose source columns are referenced by the predicate. If a predicate doesn't use one of the partition columns for a table, that column won't be in the key.The cache is local to one
plan_files()call and limited to 128 keys. This retains a small working set of repeated partition values while preventing unbounded growth when every file has a distinct relevant partition value.CODEX-generated summary follows:
Rationale for this change
Scan planning currently calculates a residual for every data file and repeatedly rebuilds the partition schema while projecting predicates. For scans with many files and a large combined filter, this repeated work can dominate driver-side planning time.
Files can also differ in partition fields that the scan filter never references. Those fields cannot affect the residual, but including them in a cache key prevents reuse. This is particularly costly for layouts with a high-cardinality partition hash: files with the same predicate-relevant partition values still appear unique.
This change:
The evaluator lifetime is intentional.
ResidualEvaluatorstores the partition being evaluated inself.struct, so sharing an instance across concurrent evaluations would be unsafe. An earlier version of #2695 attempted to cache evaluator instances; this PR instead caches only the resulting residual expressions. Keeping the spec ID in the key preserves isolation across evolved partition specs. Partition pruning and delete-file matching continue to use the complete partition record.Performance
The benchmark plans 2,000 files with a 66-leaf filter. Every file has a unique high-cardinality partition-hash value that is not referenced by the filter. Results report the mean of three in-process runs using separate
mainand PR worktrees on the same machine and Python environment:mainA same-machine comparison with the previous PR revision, which reused one mutable evaluator per spec, measured 0.009 seconds for seven relevant partitions and 1.468 seconds for 2,000. Fresh evaluator construction therefore leaves the cache-hit-heavy case effectively unchanged and adds about 6% in the all-miss case, while eliminating shared evaluator state.
This is an isolated scan-planning benchmark: manifest entry loading is stubbed so it measures residual evaluation and
FileScanTaskconstruction. It does not include catalog access, manifest I/O, or downstream data execution, so these numbers are not an end-to-end query-speed claim.Are these changes tested?
Yes.
Are there any user-facing changes?
There are no API or result-semantics changes. Local scan planning is faster for filtered scans, especially when many files differ only in partition fields unrelated to the scan predicate.
AI assistance
Codex assisted with implementation, test scaffolding, benchmark design, and PR drafting. I reviewed the logic and validated it with focused tests, repository hooks, and isolated before/after benchmarks.