Repository navigation
feat(layout): zoned pruning, shares, and dictionary value reuse in physical plans - #10462
Conversation
Merging this PR will regress 1 benchmark
|
…ysical plans Adds the plan operators the planner-driven scan lowers to, compiled to the push-based pipelines: - `Zoned` plans: data plus zone statistics, which an expression over statistics rewrites into a pruning plan that proves per-zone results once and expands them to the selected rows. - `Share` plans, which canonicalize a whole child once and serve its rows. - `Take` keeps whether its values may be reused, and the expression rule pushes the negative-cost part of an expression beneath a share of dictionary values. - `Eval` applies compound infallible predicates to encoded values (dictionary, run-end, PCO) before expanding them. - `ValueCache` keeps whole values, such as dictionary values, between the scans that share it; its owner bounds its lifetime. - `WholeSource` collects an inlet whole before producing one batch. - Scans only count shared segments over the rows their splits read. - Flat layouts lower to a filtered segment scan, and zoned layouts lower. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01P1oQtzTgSzkvrGDKisoNko Signed-off-by: Claude <[email protected]>
ad53eaf to
d4d0acd
Compare
EvalPlan::apply rewrote decoded batches (dictionary, run-end, PCO) before evaluating compound predicates. That is array-level optimization: apply_bound already runs the dictionary push-down and codes pull-up rules, and the remaining gaps belong in vortex-array and the encoding crates, not in a plan operator. EvalStage goes back to applying the bound expression, and vortex-layout no longer depends on vortex-pco. Signed-off-by: Claude <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01PV5czm45tDpWBSSdgPPuCm
…nto Zoned Legacy `vortex.stats` layouts now lower to a `LegacyStats` plan instead of a `Zoned` plan carrying the legacy stat list. The optimizer rewrites it: - into its data alone when a `Zoned` plan already summarises the same rows, whether above or beneath it, so legacy stats are used only when no aggregate zone map exists; - into its data alone for a zero zone length or a table with no stat an aggregate can stand for; - otherwise into `Zoned(data, Eval(convert, zones))`, where `convert` packs each legacy stat column under its aggregate's name. `Zoned` therefore only ever sees the aggregate schema: it drops its legacy field and accessor, prunes through `ZoneMap::try_new` alone, and `ZoneMap::try_new_legacy` is test-only again. Unoptimized, a `LegacyStats` plan reads its data and prunes nothing. Signed-off-by: Claude <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01PV5czm45tDpWBSSdgPPuCm
ValueCache keyed its map by `PlanRef::addr()`, a raw usize, and relied on storing a clone of the plan in each value so that address could not be freed and reused by another plan. `PlanByPtr` holds the plan and hashes and compares by pointer, so the key itself keeps the plan alive and an address can never name a different plan. `PlanRef::addr` is removed. Signed-off-by: Claude <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01PV5czm45tDpWBSSdgPPuCm
`lower` is how a layout tree becomes a physical plan, not only a test fixture builder. Also applies rustfmt to the legacy stats changes. Signed-off-by: Claude <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01PV5czm45tDpWBSSdgPPuCm
develop moved `SharedArray` from `vortex_array::arrays` to `crate::arrays::shared`, constructed through `Shared::new`, which broke the `Share` plan once merged. Signed-off-by: Claude <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01PV5czm45tDpWBSSdgPPuCm
| } | ||
|
|
||
| impl TakeData { | ||
| fn for_values(values: &PlanRef) -> VortexResult<Self> { |
There was a problem hiding this comment.
I think because of the rewrite this can mark dynamic values as cacheable. The example is Eval(dynamic, Concat(...)) into Concat(Eval(dynamic, ...), ...) but not sure whether this actually fits into this flow
| return Ok(None); | ||
| }; | ||
| let values = child.values()?; | ||
| let values = match values.as_opt::<Share>() { |
There was a problem hiding this comment.
Values might have different nullability than the overall plan
| None | ||
| } | ||
|
|
||
| fn with_children( |
There was a problem hiding this comment.
do you want to invalidate the cache here?
| let Some(codes) = compiler.compile(&plan.codes()?, rows, mask)? else { | ||
| return Ok(None); | ||
| }; | ||
| let cache = plan.cacheable().then(|| compiler.values().clone()); |
There was a problem hiding this comment.
I think there's double caching here? If you add Share you will cache there and here
| }) | ||
| } | ||
|
|
||
| fn lower_zoned(layout: &LayoutRef) -> VortexResult<PlanRef> { |
There was a problem hiding this comment.
we should fix the base case here zone_len == 0 and the stats dtype is {}
robert3005
left a comment
There was a problem hiding this comment.
I think just lower and dict rewriting is what we need to fix here. The caching mostly needed explaining
- A take's values are uncacheable when a dynamic comparison sits anywhere beneath them, not only in an `Eval` directly over them: rewrites push expressions down, as into the chunks of a concatenation. - An expression pushed onto a take's values is rebound to their dtype. Codes that may be null make the take nullable while its values are not, and the expression bound to the take failed to bind to the values, failing the whole optimization. Push-down is declined when the rebound take would change the dtype. - A pruning `Zoned` rebuilt over new zones discards the proof over the old ones. - Zoned and legacy stats layouts with no zones, or a zone table holding no statistics, lower to their data. Signed-off-by: Claude <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01PV5czm45tDpWBSSdgPPuCm
An expression holding a dynamic comparison now stays above a take. Kept there it reads the latest bound on every batch, and the dictionary batches the take emits still evaluate it over their values; pushed onto the values it was fixed per split and made the values uncacheable. With nothing dynamic ever beneath a take's values, `TakeData` and its cacheability check go: a take's values are always kept between scans. Signed-off-by: Claude <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01PV5czm45tDpWBSSdgPPuCm
Summary
First of three stacked PRs carved out of #10339, rebuilt on develop's push-based pipelines (#10391–#10393):
IoServicefor planning-protocol scans (stacked on this).This PR is independent of the other two, and nothing outside
plan::physicaluses it yet.Changes
Zonedplan. Data plus zone statistics in the aggregate schema. AnEvalof an expression over statistics rewrites it into a pruning plan, which proves the expression per zone once (re-proving when a dynamic comparison changes) and expands the result to the selected rows. Rebuilding a pruning plan over new zones discards the old proof. Zoned layouts lower to it, flat layouts lower to a filtered segment scan, and zoned or legacy stats layouts with no zones or no statistics lower to their data.LegacyStatsplan. Legacyvortex.statslayouts lower to it. The optimizer rewrites it:Zonedplan already summarises the same rows (above or beneath it), so legacy stats are only used when no aggregate zone map exists;Zoned(data, Eval(convert, zones)), whereconvertpacks each legacy stat column under its aggregate's name. Truncation flags are dropped, as the legacy reader does.Zonedtherefore only ever sees the aggregate schema, andZoneMap::try_new_legacystays test-only.Shareplan. Canonicalizes a whole child once and serves its rows.Take.byte_lengthinsidecast(byte_length($)), rebinding it to the values' dtype when null codes make the take more nullable than its values.ValueCache. Keeps whole values such as dictionary values between the scans that share it, keyed by plan identity (PlanByPtr, which holds the plan so its address can never name another plan). Its owner bounds its lifetime: nothing is kept on plans.WholeSource. Collects an inlet whole before producing one batch; used by shares and zone tables.Shared-segment counting. A pipeline
Scanonly counts shared segments over the rows its splits read, not the whole plan.loweris documented as the layout lowering rather than a test-only fixture builder.Evalstill applies its bound expression to each batch: evaluating predicates on encoded values (dictionary, run-end, PCO) is array-level optimization and belongs invortex-arrayand the encoding crates, not in a plan operator. No existing trait definition changes.Tests:
plan::physical::testsfor zoned pruning, the negative-cost take rule and shares.Zonedover converted zones, a converted legacy table pruning the expected zones, legacy stats dropped above or beneathZoned.ValueCachereads only the codes, while a fresh cache reads the values again.Checks run locally on the branch merged with develop:
cargo clippy --locked -p vortex-layout --all-targets --all-features -- -D warningscargo +nightly-2026-09-10 fmt -p vortex-layout --checkRUSTDOCFLAGS="-D warnings" cargo doc --no-deps --document-private-items -p vortex-layoutcargo test --locked -p vortex-layout --lib(492 passed)Not run locally: workspace-wide clippy and tests, and the other CI suites (SQL logic, DuckDB, Python, CUDA).
🤖 Generated with Claude Code
https://claude.ai/code/session_01PV5czm45tDpWBSSdgPPuCm