Repository navigation
Joint planning search: interchangeable queries take one choice; components split by the nodes queries can share - #884
Merged
Conversation
…nents split by the nodes queries can share
zzylol
added a commit
that referenced
this pull request
Oct 22, 2026
…plans made stale (#892) #855 made example 1 select the shared Scan/TimeRange plan from L3, and q3-q5 in the seed-862 plan share one SummaryMerge. #884 searches the seed-862 workload as one (searched_as_one: not_in_joint_space), so the header shows no component count. The viewer renders these documents correctly; the assertions still expected the older plans.
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
The default joint search composes every combination of each query's candidates within one component. Two kinds of query multiply that space without ever adding a cheaper plan:
Examples from the paper workloads:
quantiles-24h@60s(raw data retained): fivequantile_over_time(q, data[24h])queries plus their ratio. Main checks 11.86M candidates; the run had not finished after 3 h 20 min on an idle 56-core machine.Also, components were split only by source. Queries over one source that can never share a node were still searched together.
Part of #879 and #673.
What
The default search now applies two exact rules. Both are checked against
--exhaustive(joint_search_equivalence).Interchangeable queries take one choice (design method 4). Queries are interchangeable when all of these hold:
q;Members of one class then:
Components split by shared nodes (design method 5). Two queries over one source stay in one component only when a Pass 2 rule can make them read one node, for some choice of their own candidates. The rules checked are: identical expressions (including widened scans), summary capability, shared pane chains, rollup and top-k reuse. With a disk price, raw retention links every query over a source, so the components stay per source.
The components' selections are combined directly, no longer looked up among the joint candidates. That lookup enumerated every Pass 2 candidate of the whole workload. The combination falls back to searching as one when:
SearchReportgainsinterchangeable_queries.How
pass/interchangeable.rs(new):Classes::offorms the classes. It compares rank-masked serializations of each query planned alone, then removes members whose rank another query can share (shares_a_rank_node).invalid_prefixskips choices that give two members different candidates.members_read_same_buildsfilters the Stage 1 candidates.pass/components.rs:sharing_componentsruns a union-find over pairs of queries per source, usingcan_share_a_node.combined_pass2andwidens_a_scanreplace the joint-space lookup.Why the class rule is exact
Swapping two members maps every plan to one of the same price, because nothing else tells them apart. Under cost model 1, a summary build's price is the cheapest of its materializations, and each materialization's price is affine in the number of class readers:
A minimum of affine functions is concave. So splitting a class over several builds never costs less than putting it on the best one of them. Classes are formed only under cost model 1 without a measured cost table.
Key code interfaces added / modified
crates/plan-selection/src/lib.rsSearchedAsOne::NotInJointSpacekeeps its name. It is now produced by the two combinations above, not by a failed lookup.Usage
The user plans RangeHeavy's 24 h quantiles component:
The facade calls
run_stage1_to_stage3_from_pass1:sharing_componentsfinds one component, because every query can share the scan;Classes::offorms the class[1, 3, 4](q 0.75, 0.95, 0.99). q 0.5 and q 0.9 stay out, because the ratio can share their exact quantiles;out.json'sstage3_plan_selection.searchreportsinterchangeable_queries: [[1, 3, 4]]and 153,086 candidates checked. The selected plan costs 8.7388e-05 USD/s.Major algorithm changes
1. Interchangeable queries take one choice
Problem. Five RangeHeavy queries
quantile_over_time(q, data[24h]), q ∈ {0.5, 0.75, 0.9, 0.95, 0.99}, every 60 s, each with three Pass 1 candidates (exact, KLL, DDSketch). The joint search composes 3^5 = 243 choices for them, times every Pass 2 subset and every materialization, though some cheapest plan gives all five one family on one build.Before.
stage1_logical_asap_optimization_skippingenumerates every Pass 1 choice thatQueryCandidates::invalid_prefixaccepts.After.
Classes::offorms the classes: equal entries, no latency bound, own targets, plans alone equal up to the rank. Members of mixed ranks are kept only when no other query can share one member's rank node and no two members have one rank; duplicates form their own class.invalid_prefixalso rejects choices that give two members different candidates (Classes::invalid_prefix).Classes::members_read_same_builds).Invariant. The selected price equals the exhaustive search's. Swapping two members maps every plan to one of equal price. Each build's price is a minimum of materialization prices, each affine in the number of class readers, so it is concave in that number and a split class is never cheaper. Checked by
joint_search_equivalence:interchangeable_queries_select_the_exhaustive_price, which includes duplicates and averages of mixed ranks;random_workloads_with_interchangeable_queries_select_the_exhaustive_price;queries_over_other_windows_or_with_a_latency_bound_are_in_no_class;identical_sql_queries_are_one_class;Complexity.
Limits.
2. Components split by the nodes queries can share
Problem. RollupHeavy's 62 SQL queries all read
httporflows, so the source split gives one or two components, though most pairs can never share a node (different groupings and windows).Before.
source_componentsruns union-find on the sources each query reads.stage1_candidates_of_choice).After.
sharing_components: within each source component, union-find over pairs.can_share_a_nodecomposes each pair over their own choices and asks every Pass 2 rule (identical expression, also on widened scans; capability; window-composition chains; rollup; top-k reuse) whether a node of each becomes one.combined_pass2merges the selections' Pass 2 records. It falls back to searching as one when exact top-k reuse meets panes, or a component widened a scan (widens_a_scan).Invariant. Same price as the exhaustive search when no Pass 2 enumeration cap binds in the joint search but not per component. Every interaction goes through a node both queries can read; Stage 2 materializes any node two queries read. Checked by
random_workloads_over_disjoint_sources_combine_their_components,components_follow_shared_sourcesand the random suites.Complexity.
Limits.
Before this PR
After this PR
Measured on an idle 56-core machine, release build. Main is f870130; this PR is rebased on it.
quantiles-24h@60s, raw retainedBefore #855, #866 and #869, the same rules planned whole RollupHeavy in 5.7 s (cost 1219.209/s) and its 8-user version in 24 s (1463.051/s). On main, the shared scans (#855), SQL windows as time ranges (#866) and sketch rollup (#869) link most queries over one table into one component again. That is why whole RollupHeavy is still slow.
Not in this PR, as follow-ups:
A ratio or another interval that shares a member's rank keeps that member out of its class. Matched permutations of two classes (60 s and 300 s copies) are not exploited yet.
Verification
cargo test --release -p asap-integration-tests --test joint_search_equivalence -- --include-ignored: all 18 pass, including the ignored exhaustive runs under every cost model. New tests:interchangeable_queries_select_the_exhaustive_price;random_workloads_with_interchangeable_queries_select_the_exhaustive_price;queries_over_other_windows_or_with_a_latency_bound_are_in_no_class;identical_sql_queries_are_one_class.random_workloads_over_disjoint_sources_combine_their_componentsnow compares prices, not plans: the combination may select another plan of equal price.cargo test --release -p asap-plan-selection -p asap-logical-asap-optimizationpasses. So does-p asap-devtools --test stage_pipeline, with the three workload examples regenerated: same selections and prices, fewer candidates checked.cargo fmt --checkandcargo clippy -D warningsare clean on the touched crates.Replaces #880, which GitHub no longer synced to this branch after a force-push; same change, rebased onto main.