Repository navigation
fix(plan-selection): evaluate the queries' separate plans first in the seed round - #973
Merged
Merged
Conversation
Two per-city p90 queries cost 0.1048378/s planned separately but 0.2084510/s planned together; the joint search stops at a query-time plan. Fails on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e seed round The model's lower bounds price query-time scans and builds at zero, so the joint search can spend its seed-round budget on shared plans and stop above what planning each query alone gives. Each query is now searched over the candidates only it can reach; their combination, when valid, is evaluated before any proposal and becomes the first incumbent. Two per-city p90 queries: 0.2084510/s before, 0.1048378/s after (the sum of their separate plans). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… scan, evaluated last Review fixes for the separate-plans seed: - A query whose own candidates hold no plan (queries share a group, such as one scan) is searched over every candidate it reaches, up to 128, and its plan is used only when that search proves it optimal. Shared candidates appear once in the combination, whose modeled cost counts each candidate once. - The combination is evaluated after the joint search, only when the search ended above the queries' summed cost, so it never takes the place of one of the search's evaluations; an eighth of the remaining time is kept for it. - It runs in the first round of a search (the seed-grid round, or the single complete round of a window-free workload) via the new CandidatePlanningContext::seed_separate_plans. - independent_plan is renamed separate_plans_combined; the search loop is unchanged from main; a debug assertion checks components are closed. Tests: two shared-scan pairs (p90 + MAX, p90 + grouped COUNT) that cost 2.79x and 2.21x their separate plans before; the weak grouped/filtered test is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
force-pushed
the
fix/seed-independent-plan
branch
from
October 27, 2026 23:42
9c5cf27 to
32fe000
Compare
…d optimum The size cap already keeps the large shared graphs out; a plan found within the time limit is still each query's own, and the combination is kept only if its whole-workload evaluation is cheaper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ompute the gap after a replacement Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 28, 2026
Draft
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 joint search can return a plan costlier than planning each query on its own:
MAXover the same 5-minute window ofmetricscost 0.0578418/s planned separately; main selects 0.1614550/s for the two together (2.79×). A p90 plus a per-cityCOUNT(*): 0.0796515/s separately, 0.1762188/s together (2.21×).The cause is in the first round of the candidate-connection search. The MILP proposes plans in order of their lower bound, and
costing/lower_bounds.rsprices query-time scans, windows, aggregates and sketch builds at 0, while an ingestion-time scan has a positive bound. So every query-time combination ranks before the cheaper ingestion plans, and the round's cap (min(total/4, 5 s)and 32 evaluations in the seed-grid round) can end the search there.What
In the first round of a search, each query is planned alone, and the combination of those plans is evaluated for the whole workload after the joint search, when the joint search ended above the queries' summed cost. The selected plan then costs no more than that combination.
city = 'NYC'and p90 forcity = 'SF'MAX(value)(one shared scan)city, COUNT(*) … GROUP BY city(one shared scan)city IN ('NYC', 'SF')On the finer-aggregate reuse branch (#952), whose reuse candidates make example C's queries share groups, see Verification.
How
optimize_configured_candidates, in the first round of a multi-query search (CandidatePlanningContext::seed_separate_plans), callsseparate_plans_combinedwith a quarter of the round's time before the joint search.separate_plans_combined, for each query:optimize_configured_candidatesover aprojected graph, evaluated for that query alone) over the candidates no other query can reach;MAX_SHARED_SEARCH_CANDIDATES); larger graphs only take the joint search's time;It returns the combination with the sum of the queries' own costs, or
Noneif any query finds no plan or the combination is not a valid selection.The joint search runs unchanged, with seven eighths of the remaining time when there is a combination. Afterwards, if it did not prove optimality and its incumbent costs more than the queries' summed cost, the combination is evaluated by the same evaluator (Stage 2 + PlanSelector for the whole workload) and becomes the incumbent if cheaper. It never takes the place of one of the search's own evaluations.
The first round is the seed-grid round, or the single complete round of a workload without a pane window (
route.rs: no earlier round in that search).projectaccepts a candidate set that is not closed under producers: it drops producers and root candidates outside the set, and a candidate whose required copy is outside the set getsAllowedOutputs { outputs: [] }, so it can never be selected. Components fromcomponents()are closed (a debug assertion checks it), so their projection is unchanged.Key code interfaces added / modified
crates/plan-selection/src/search/refinement/components.rscrates/plan-selection/src/search/refinement/planner.rsThe search loop (
refinement/search.rs) is unchanged from main. All are crate-internal; no public API changes.Usage, end to end:
SELECT percentile_cont(0.9) WITHIN GROUP (ORDER BY value) FROM metrics WHERE ts > now() - INTERVAL '5' MINUTEandSELECT MAX(value) FROM metrics WHERE ts > now() - INTERVAL '5' MINUTE, each every 10 s (fixture902.json:metricshas 666.67 rows/s, 100 series).run_stage1_to_stage3_with→candidate_connection_search→ seed-grid round →search_domain(first round:seed_separate_plans = true) →optimize_configured_candidates.percentile_and_max_over_one_scan_cost_no_more_than_separate_plans).Major algorithm changes
Evaluate the queries' separate plans when the joint search ends above them
validate_selected_graph), evaluated by the same whole-workload evaluator, and kept only if cheaper, so no unvalidated plan is selected and the certified lower bound is untouched.merge_validated_roundskeeps the first round's plan anyway.refinement/objectives.rs, which does not calloptimize_configured_candidateswith this flag; they get no seed.Before this PR
On workloads where the query-time plans rank first, the joint search could return a plan up to 2.8 times the cost of the queries' separate plans, including when the queries share a scan.
After this PR
In the first round, whether or not the queries share candidates, the joint plan costs no more than the queries' separate plans combined, when all of these hold:
The comparison is with each query's plan in the first round's (seed-grid) domain, not with planning it alone over the complete pane-width domain and the full budget. Otherwise the search runs as on main.
Verification
per_city_percentiles_cost_no_more_than_separate_plans(committed alone in the first commit) fails on main:joint 0.2084510 above separate plans 0.1048378.percentile_and_max_over_one_scan_…andpercentile_and_grouped_count_over_one_scan_…fail on main and on this PR's first version (seed from own candidates only): 0.1614550 vs 0.0578418 and 0.1762188 vs 0.0796515. They replacegrouped_and_filtered_percentiles_…, which passed without the fix.cargo test -p asap-plan-selection --lib: 374 (as on main; the earlierseed_plans_are_evaluated_firstunit test went with the search-loop change);independent_plan_seed3,joint_search_equivalence14,sharing_issue_regressions12,production_rollup_workloads4,identical_exact_queries2,rollup_controlled2.exact_coarse_sum_reuses_fine_sum([Bug] An exact coarser grouping does not roll up the finer exact result #923) passes alone (0.0066901 vs base 0.0066551, tolerance 0.00004; twice), but failed once while another planner test ran on the same machine. Its margin is tight and its seed round is time-bound; on this PR its queries get no seed (643 shared candidates), so only the own-candidate search (infeasible in milliseconds) runs before the joint search.filtered_kll_states_cost_no_more_than_separate_planspasses: together 0.2284651/s (seed-joint-plan-7, the separate plans), grouped alone 0.1760462/s, filtered alone 0.0524189/s; with the first version of this PR it stayed at 0.3320576/s. The grouped query has no plan of its own 38 candidates; over its 64 reachable ones it finds 0.1760462/s.cargo fmt --all --checkandcargo clippy -p asap-plan-selection -p asap-integration-tests --all-targets -- -D warningsare clean.🤖 Generated with Claude Code