Repository navigation
feat(optimizer): reuse a finer aggregate through a compensating filter and a rollup of values or states - #952
Merged
Merged
Conversation
This was referenced Oct 27, 2026
zzylol
force-pushed
the
feat/finer-aggregate-reuse
branch
from
October 27, 2026 21:32
83c87c0 to
5db72e8
Compare
This was referenced Oct 27, 2026
A Filter over State whose predicate reads only plain (key) columns selects whole states, so its output stays State and a SummaryMerge can read it. Summary coverage maps the selection back to the same build over filtered input rows; rolled_up_build, source_kind and quantile_may_be_empty see through it. A filter reading a state column is still rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
match_finer decides whether a query aggregate is computable from a finer one over the same scan and time ranges: the finer conjuncts are among the query's, every other query conjunct reads only finer group-by columns, and the query keys are finer keys. The memo rule finer_aggregate replaces filtered_group: it reads the finer result through a compensating Filter, then MIN per group when every dropped key is pinned (any measure), or rolls sums, counts (grouped only), minima and maxima up. The selected-group guarantee holds for any unique key covered by group keys and pinned columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sketch build over fewer rows than a finer build of the same family, whose extra conjuncts read only the finer keys (filtered_finer, via match_finer), is answered by a SummaryMerge over a compensating Filter of the finer states. Resizing keeps the filter; the rolled-up guarantee reads through the selection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each positive case executes the original and the reuse candidate on the same rows (empty input, absent group, NULL samples) and compares; example B also checks the selected plan's cost with and without the filtered query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generalized selected-group guarantee accepted any filter predicate. A predicate that also reads the estimate decides by an approximate value which group is kept, so the filter must read only columns of the covered unique key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two aggregates with the same group keys in a different order and no extra conjuncts each got a MIN read of the other, a cycle in the memo (CyclicDependencies). Equal key sets without compensation are no read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The state path's Filter over states was classified as raw rows, rejected by the Filter edge's state check, and refused by expression compilation; its global rollup also lost SQL's one row over an empty selection, since built_from_relation did not look through the filter. A node whose output is state now carries summary state, a key filter passes states of any family, the executor compiles its predicate over the plain columns only, and a global rollup over selected states emits its empty row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cement per build - The filtered finer build is chosen with finest's tie-break, so the choice does not depend on query order. - A same-level read takes one row per group: aggregates with a top-k or count-values measure are not read from a finer one. - A build that is both a finest build and the coarse build of another choice is replaced once, as the finest build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
filtered_kll_states_merge executes the merged-state read against the query's own KLL read on the four row sets, empty input included. permuted_group_by_is_not_a_finer_read covers the cycle. The example C cost test is ignored until the joint search returns the independent plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r path
The executor rejected every `x [NOT] IN (...)` (unsupported scalar
expression), so a plan whose filter or Scan predicate held one could not
compile; in example C every state-path plan for `city IN ('NYC', 'SF')`
was rejected. IN now evaluates as SQL defines it: true on a match, else
NULL when the operand or an item is NULL, else false; NOT IN negates it.
An expression the vector path cannot lower is evaluated per input with
NULL in the columns it does not read; a filter over summary states now
accepts NULL there as well as a state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The state-merge test now runs the query as written (`city IN (...)`). The example C cost test stays ignored: its joint optimum is 0.1963/s, which the search proves only after about 9 s, past the seed round, and needs the independent plan as a seed (separate plan-selection change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`x [NOT] IN (v1, ...)` lowers to `[NOT] (x = v1 OR ...)` on the batch path, which has the same three-valued result, so a filter with IN runs as Arrow kernels instead of once per row. A NULL literal item keeps it on the row path, as for `=`. An empty list still validates its operand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… form Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first round now evaluates the queries' separate plans combined when the joint search ends above them, so the test no longer needs #[ignore]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
force-pushed
the
feat/finer-aggregate-reuse
branch
from
October 28, 2026 01:18
f7e5ef7 to
8336e8f
Compare
3 tasks
zzylol
pushed a commit
that referenced
this pull request
Oct 29, 2026
#943 fixed #902's literal, ungrouped case and closed the issue. This test's grouped variant (`... AND city = 'city1' GROUP BY city`) holds only since #952's finer-aggregate reuse, which closes #950: q' now adds 0.000040/s against the Filter witness's 0.000010/s, within the 0.001963/s tolerance. The test still expected the violation, so main's CI has failed on finds_issue_902_filter_on_group_key_not_reading_the_grouped_result since b89a602 (#952). As the file header says, its expectation flips to Outcome::Holds and the test stays as the regression test; on failure it prints the outcome. 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.
Why
A query is often computable from a finer query the workload already computes. Example:
SUM(value) … WHERE city = 'NYC'is computable fromSUM(value) … GROUP BY city, host. Today the planner offers this reuse in only two fixed shapes:filtered_group.rshandles one global quantile, one finer key and one extrakey = literal.Every other filtered or coarser variant builds its own scan and aggregate. #950 designs one rule for all of them, following view matching for aggregation queries (SIGMOD 2001 §3.3; Calcite
SubstitutionVisitor.unifyAggregates/SqlAggFunction.getRollup).What
A query aggregate gets a candidate that reads a finer aggregate through a compensating filter (the query's extra conjuncts on finer keys) and, when its groups are coarser, a rollup:
Filter, thenAggregate:regroupnot needed), it reads withMINper group. This works for any measure, including quantiles, and keeps SQL's one NULL row for an absent group.SUMof sums or counts (counts only into grouped queries),MIN,MAX.Filter, thenSummaryMerge { group_by }, then the query's estimate. For example, a p90 overcity IN ('NYC','SF')is computed by merging the NYC and SF KLL states of a per-city build.Filterover a state whose predicate reads only plain (key) columns now staysState.filtered_group.rsis deleted; its case is the pinned-key case of the value path.How
match_finer(finer_aggregate.rs) normalizes both aggregates' input rows into a spine (time ranges + scan without predicates) and a conjunct set. It requires:It returns the compensating filter over the finer keys, the query keys' positions, and whether a rollup is needed (some dropped key is not pinned).
Value path: the memo rule
memo/rules/finer_aggregate.rsbuckets SQL aggregate groups by their measures and builds the reader for each matching pair. The reader is a candidate only when its schema equals the query's.State path: in
asap_aware_cse.rs,filtered_fineruses the samematch_finerwhen the builds' rows differ. The sketch rollup then regroups overFilter(states), and resizing for all readers keeps the filter.Guarantees:
selected_group_guaranteenow covers any unique key whose columns are group keys or pinned, not just one key.rolled_up_guaranteereads through the state selection.Deviations from the #950 design.
add_rollup_candidatesstays, so a same-rows SQL rollup can come from two rules: the value path here and the exact-accumulator rollup. Plan selection prices both.match_finerthen checks source and window.OR), not only column-literal comparisons; such an expression is constant within a finer group.Execution of the state path.
timing.rs), and a key filter passes states of any family.CompiledExpression::compile_over_states); other operators still refuse state inputs.built_from_relationlooks through the filter).x [NOT] IN (…). Before, everyINwas an "unsupported scalar expression", in aFilteror aScanpredicate alike, so each state-path plan for example C'scity IN ('NYC','SF')failed to compile.INis vectorized: the batch path lowersx [NOT] IN (v1, …)to[NOT] (x = v1 OR …), which has the same three-valued result, so a filter withINruns as Arrow kernels. A NULL literal item keeps it on the row path, as it does for=. An empty list still validates its operand.=~is not affected: it lowers toCompareOpKind::Regex, notIN.Key code interfaces added / modified
crates/logical-asap-optimization/src/finer_aggregate.rs (new, crate-private)
crates/logical-asap-optimization/src/memo/rules/finer_aggregate.rs (new; replaces
memo/rules/filtered_group.rs)crates/logical-asap-optimization/src/asap_aware_cse.rs
crates/types/src/ir/operator/asap.rs · non_asap.rs · summary_coverage.rs
NonASAPOp::output_kind:Filterover State reading only plain columns →State(wasRelation);validate_inputsaccepts it.crates/executor/src/expressions/planner.rs
Usage (example B, end to end):
(From
finer_aggregate_reuse::filtered_sum_costs_little_beyond_the_finer_sums.)Major algorithm changes
Finer-aggregate reuse (Pass 2 rollup)
filtered_grouponly took one quantile with one key. A KLL p90 overcity IN ('NYC','SF')could not use a per-city KLL.add_filtered_group_candidateshandled one global quantile, one finer key and one extrakey = literalscan conjunct.add_rollup_candidatespaired builds over identical rows.match_finerruns per pair of SQL aggregates with equal measures, which are bucketed.FilterthenAggregatewithMIN(when the query's key set or pinned keys cover the finer unique key) or the per-measure rollup.filtered_fineraccepts is merged throughFilter(states).MINover at most one row is the identity; for a global query over no rows it gives SQL's one NULL row.COUNTis never read from a sum (0 vs NULL).filtered_group). The state path adds onematch_finerper (coarse, build) pair that is not the same rows.AVG, noHAVING, no per-measure filters.Before this PR
SUM(value) … WHERE city = 'NYC'next toSUM(value) … GROUP BY city, hostbuilt its own scan and aggregate: 0.0054229/s on its own.city IN (…).Filterover states was rejected.After this PR
SUMandMAXby city overcity = 'NYC' OR city = 'SF'.Filterthat reads only key columns keeps summary states as states.Verification
Failing first. On origin/main with only the new test file,
cargo test -p asap-integration-tests --test finer_aggregate_reuse:pinned_keys_read_one_finer_quantile_row,filtered_sum_rolls_up_finer_sums,filtered_grouped_aggregates_roll_up_finer_values,filtered_kll_states_merge,filtered_sum_costs_little_beyond_the_finer_sums;On this branch:
finer_aggregate_reusefiltered_kll_states_mergealso checks theINread against theORform, andfiltered_grouped_aggregates_roll_up_finer_valuesrunsINon the value pathasap-types --libasap-types --test summary_coveragea_key_filter_over_states_selects_statesasap-types --test pane_chainasap-logical-asap-optimization --lib6a22ec49)sharing_issue_regressionssketch_rollupsketch_rollup_planningrollup_controlledproduction_rollup_workloadsreuse_rulesidentical_exact_queriesper_computation_candidates_sqlfiltered_aggregatesasap-executor(lib and tests)in_list_follows_sql_three_valued_logicandan_in_list_is_vectorized_with_row_values(batch result equals the row result forIN/NOT IN, NULL items and an empty list); all test targets passjoint_search_equivalencekll_pane_execution/sql_to_physical/materialization_e2ecargo fmt --all -- --checkINsupport, failing first. On the branch rebased on main, before the executor change:filtered_kll_states_mergewith the query as written (city IN ('NYC','SF'), previously rewritten toORbecause the executor had noIN) fails to compile its filter.INevaluated but the per-input path still passing NULL into the state check, it fails withexpression input differs from its bound schema.Review fixes, failing first (each fails at 7faad60, passes at this head):
permuted_group_by_is_not_a_finer_read:CyclicDependenciesbefore.filtered_kll_states_mergenow executes the merged-state read:IllegalChildDataState { edge: "SummaryMerge.children" … }before; then the Filter edge and the executor rejected state input.filtered_group_preserves_sql_empty_and_null_semantics(Gton the key).Test runs for this head were on one 56-core host; main and branch cost comparisons ran there too.
The #923 cost test is timing-sensitive.
exact_coarse_sum_reuses_fine_sumfailed its 0.00004/s margin on both origin/main (0.0566/s) and this branch.Guarantee check.
sharing_issue_regressions::filtered_group_preserves_sql_empty_and_null_semanticsgains a case: a filter that also reads the estimate (IsNotNull(col 1)) must not keep the selected group's rank bound.selected_group_guarantee.Workspace clippy and full-workspace tests were not run locally.
Example C and the joint search
Workload:
SELECT city, p90 … GROUP BY citywithSELECT p90 … WHERE city IN ('NYC','SF'), both every 10 s over 5 minutes.seed-joint-plan-8)The branch's optimum: q0 exact at query time (0.1760); q1 reads q0's query-time rows (shared time range), builds a KLL by city, keeps NYC and SF, merges and estimates.
Cause, from instrumented runs of the seed round:
lower_bounds.rsprices query-time Scan, TimeRange, Aggregate and KLL builds at 0, but an ingestion Scan at 0.0038686/s. Every plan that runs q1 at query time therefore has bound 0, and the MILP proposes those first. The bound gives the proposals no useful order.distinct_values_per_labelis declared, and that evidence exists only in Stage 3. q0's own Pass 1 KLL candidates fail the same way on main. Before theINfix, 40 further proposals failed to compile.min(total/4, 5 s)and 32 evaluations (route.rs), so it ends at 0.3321/s. Later pane rounds return the same. A larger total budget does not help.None of the new candidates can be dropped in Stage 1: the q0 KLL readers depend on Stage 3 evidence, and the q1 state reads are the optimum. The fix belongs to plan selection. #973 (merged) evaluates the queries' separate plans combined when the joint search ends above them, so the default run now returns 0.2284651/s, no worse than main, and the example C cost test (
filtered_kll_states_cost_no_more_than_separate_plans) runs un-ignored. Reaching 0.1963/s within the budget needs the candidate-level rejection cuts of #964 (measured 0.1963 with them) and the lower bounds of #963.Closes #950
🤖 Generated with Claude Code