Repository navigation
feat(pass2): share SQL table scans; a coarser grouping reads a finer grouping's pane chain, newest panes rolled up (#878 part 2) - #887
Merged
Conversation
…grouping's pane chain, newest panes rolled up Two queries over one SQL table shared nothing: structural CSE shares a non-ASAP node only when its rows have a unique key, and a table scan's rows have none. Equal scans are now shared: each reader reads every row the scan outputs. The window-composition rule now also groups a build with a finer grouping's build over a window at least its own: one chain of the finer grouping's panes, sized for every reader, which the coarser build reads as its newest panes rolled up into its own groups. A global SQL percentile over 5 minutes reads the newest 30 of 120 per-city panes kept for a per-city percentile over 20 minutes. A rollup to no keys of a SQL relation's states now stands for the global SQL aggregate: the executor outputs one empty state when it has no input state, so the answer is one NULL row, as the global build gives. The coverage check no longer rejects it, and the rollup rule offers it. Part of #878 (problem 2).
…ion (a count of no rows is 0, a sum of no counts NULL)
…d once, an exact count is never rolled up (Int64), emit_empty_global, docs
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
Issue #878, problem 2. Q1 (the overall p50 and p90 over 5 minutes) and Q2 (the p90 per city over 20 minutes) run every 10 s over one SQL table, yet no candidate shared anything between them, not even the scan. Q2's per-city chain of 120 panes of 10 s holds everything Q1 needs: Q1 could merge the newest 30 panes of each city and then roll the cities up into one state.
Diagnosis (the three suspected causes)
crates/types/src/ir/cse.rs,InternTable::intern): a non-ASAP node is shared only when its rows have a unique key, and a table's rows have none. Sosharing_candidatesnever offeredshared Scan (Q1, Q2).SummaryMergewith bothnewest_panesandgroup_byis rejected (ASAPOp::validate_inputs). Two merges stacked (a rollup over a newest-panes merge) were already valid IR (rolled_up_buildandSummaryCoverage::deriveread a merged chain). The real gaps were these:same_build_over_one_input), and the sketch rollup rule only between builds over the same rows, so the same window (finer_keys/same_rows).CoverageError::GlobalRelationRegroup, and infiner_keys). The executor's merge outputs no row when it has no input state, but a global SQL aggregate has one row.sized_for_allover every member. KLL's rank error and DDSketch's relative error do not depend on q, so one sketch serves Q1's p50 and p90. Here every reader has ε = 0.01, and the chain is a KLL with k = 345, the size each query has alone.What
Equal scans are shared even when their rows have no unique key, so the identical-expression rule offers
shared Scan (Q1, Q2).The window-composition rule offers one more shared chain per subset of pane candidates. A sketch build over a SQL table's rows, grouped by fewer keys, reads the chain of a finer grouping when:
The coarser build reads the chain as a rollup (
SummaryMerge { group_by }) over the merge of its newest panes (SummaryMerge { newest_panes }). The chain is sized for every reader. PromQL builds and exact accumulators are not rolled up this way yet; this PR has no end-to-end test for them.A rollup to no keys of a SQL relation's states stands for the global SQL aggregate. When there is no input state, the executor outputs one empty state, so the answer is one NULL row, as the global build gives. The coverage check no longer rejects it, and the rollup rule offers it too. An exact global
COUNT(*)is still never answered from grouped counts: the exact rollup never sums Int64 counts again, so it can't turn 0 into NULL.Candidate labels name the readers of such a chain:
one Kll for Q1, Q2 in 120 × 10s panes (Q1 newest 30).How
cse.rs:interntreats aScanas reusable.asap_aware_cse.rs:rollable_pane_candidates(computed once per rule call) marks the sketch builds over SQL rows whose readers keep a merged guarantee.rollup_window_groups(chosen, rollable)picks hosts by most group keys, then longest window. Each pane candidate joins one group (rolls_up_into). Only groups with a coarser member are listed; the rest arenested_window_groups' groups.window_composition_pass2_candidatespushes these groups through the existingpush_shared_chains.shared_chain_buildtakes, among the longest windows, the finest grouping as the host.in_paneswraps a coarser member's newest-panes merge inregrouped(member, merge).sketch_rollup_candidatesintoread_with_merged_guarantee.SummaryMergegets anemit_empty_globalflag (named after the build's own).compile_internalsets it for agroup_by: Some([])merge whose states were built from a relation's rows (built_from_relation), andmerge_summarythen adds one empty group when there is no input state.Key code interfaces added / modified
crates/executor/src/operators/mod.rs,crates/executor/src/operators/summary/mod.rscrates/types/src/ir/properties/summary_coverage.rsThe new Pass 2 helpers (
rollable_pane_candidates,rollup_window_groups,rolls_up_into,group_key_count,read_with_merged_guarantee) are private toasap_aware_cse.rs.Usage (end to end, the issue's workload):
Major algorithm changes
Problem, with a concrete input. Two queries over
metrics(ts, city, value)run every 10 s at ε = 0.01:percentile_cont0.5 and 0.9 over the last 5 minutes;percentile_cont0.9GROUP BY cityover the last 20 minutes.Each query's plan is
Scan(metrics) → TimeRange(W) → SummaryAgg(Kll). Q1's build is global; Q2's is per city, in 30 and 120 panes of 10 s. Before this PR, the two plans shared no node.1. SQL scan sharing (structural CSE,
asap_types::ir::cse)Before:
share_common_sub_dagsinterns bottom-up.InternTable::intern(node)returns an existing equal node only ifnode.is_asap() || node.schema.has_unique_key().Scan(metrics)stay two nodes.sharing_candidatesfinds nothing, and the identical-expression rule offers noshared Scan (Q1, Q2).After:
internalso reuses an equal node when it is aNonASAPOp::Scan.sharing_candidatesnow lists the two scans, andidentical_expression_pass2_candidatesoffers the candidate with them shared, next to the one without. Stage 3 picks between them.2. Newest-n plus rollup composition (window-composition rule,
asap_aware_cse)Before,
window_composition_pass2_candidates(roots, candidates)did this for each non-empty subsetchosenof pane candidates:in_panes(roots, chosen, []).nested_window_groups(chosen)and fromgcd_window_groups(chosen). Both admit a member only ifsame_build_over_one_inputholds, which requires the same reduction. So Q1 (global) and Q2 (by city) never shared a chain.After, the same loop also does:
For the issue's workload this gives:
PaneChain(Kll k = 345, by city, 120 × 10 s);SummaryMerge(all panes);SummaryMerge { group_by: [] }overSummaryMerge { newest_panes: 30 }, and both of its estimates read it.3. Global SQL rollup in the executor
SummaryCoverageno longer rejects a rollup to no keys over a relation (GlobalRelationRegroupis removed), andfiner_keysno longer refuses it.compile_internalsetswith_emit_empty_globalon agroup_by: Some([])merge whose statesbuilt_from_relation.merge_summarythen outputs one empty state when it has no input state.Correctness argument and tests
TimeRangenodes above the scan, so readers of different windows stay separate. Test:q1_and_q2_share_the_table_scan;cse::tests::equal_scans_without_a_unique_key_are_shared.W_i / wpanes. The merge of those panes equals one build overW_i; this is the already-provenmerged_window_buildequivalence.SummaryCoverage::derivechecks the stack (rollup over newest panes) against the coarse build's definition.sized_for_all), so each reader gets at least its own accuracy. KLL and DDSketch error does not depend on q, so one state answers both p50 and p90.Tests in
q2_pane_chain_serves_q1_through_newest_panes_and_a_rollup(KLL and DDSketch, the issue's queries, NULL cities included), run on the executor with pane chains expanded:percentile_cont;a_global_count_is_not_rolled_up_from_grouped_countschecks that an exactCOUNT(*)is never rolled up into a NULL. The guard is the existing Int64 refusal inrolled_up.Complexity and candidate count
rollable_pane_candidatesis one DAG walk per rule call.rollup_window_groupsis O(n²) per subset over n ≤MAX_PANE_CANDIDATES= 20 pane candidates. A subset gains at most one Pass 2 candidate.Limits
quantile by (job)fromby (job, instance)panes) and exact accumulators are not rolled up from a chain yet.Before this PR
On the issue's workload, after #881: 155 logical and 413 physical candidates. No candidate shares a node between Q1 and Q2. Stage 3 selects
P49-m1(Q1 Kll + Kll · Q2 Kll · Kll for Q1 in 30 × 10s panes · shared PaneChain (Q1), SummaryMerge (Q1)) at 0.5475 cost/s.After this PR
shared Scan (Q1, Q2), SummaryAgg (Q1))The selected plan is 8.9% cheaper.
Search size. Scan sharing doubles this workload's candidates: every candidate also comes with its scan shared, as the identical-expression rule lists both. The rollup chains add 38 logical and 170 physical candidates. Always sharing equal scans, instead of listing each candidate with and without, would undo that doubling. It is left as a question, not done here.
Verification
Integration tests,
sql_two_percentiles(about 2 s; Stage 1 only for these three):a_global_count_is_not_rolled_up_from_grouped_counts: with a rollup to no keys allowed over a table, no candidate answers a globalCOUNT(*)from per-city counts.q1_and_q2_share_the_table_scan. Some Stage 1 candidate reads one scan in both queries.q2_pane_chain_serves_q1_through_newest_panes_and_a_rollup. For KLL and DDSketch, there is a candidate in which both of Q1's estimates read a rollup to no keys over the newest 30 panes of Q2's 120-pane chain. The test runs that candidate on 2,400 rows over 20 minutes, with a NULL city on every seventh row, and with each pane chain expanded into its panes:percentile_cont.emit_empty_global, which gives no row.Unit and other tests:
cse::tests::equal_scans_without_a_unique_key_are_shared(types).summary_coverage::a_rollup_to_no_keys_of_a_relation_covers_the_global_build(types; replaces the rejection test).sketch_rollup::invalid_rollups_are_rejectednow expects a rollup to no keys to be valid.cargo test --no-fail-fast -p asap-types -p asap-executor -p asap-logical-asap-optimization -p asap-integration-tests -p asap-devtools -p asap-plan-selection -p asap-planner --all-features: every test binary passes, including the committed dag-viewer examples check,joint_search_equivalence,sketch_rollup*,sql_trailing_windowandtumbling_pane_merge.cargo fmt --all --checkandcargo clippy -p asap-types -p asap-executor -p asap-logical-asap-optimization -p asap-devtools -p asap-integration-tests --all-targets --all-features -- -D warnings: clean.Docs:
planner-layering.md(SQL scan sharing; a coarser grouping reading a finer grouping's chain) andasap-primitive-schema.md(a rollup to no keys over a relation,CoverageError).Closes #878.