Repository navigation
Joint planning search: stream Stage 1 to Stage 3 and reject permanently invalid query candidates (methods 2 + incumbent) - #792
Merged
Conversation
…y invalid query candidates Implements methods 2 and the streaming incumbent of docs/design_docs/proposals/joint-planning-search.md. - Stage 3 becomes a streaming selector (PlanSelector): each physical candidate is checked and priced as Stage 2 builds it, and only the best so far (cost model 3 pareto-front: the front so far) is kept. select_plan keeps every candidate, as before. - Each query's own candidates are checked planned alone before composition; a whole-workload choice that picks one failing a check no remaining choice can repair (no executor kernel, Count-Min weights not proven non-negative, no guarantee or an unknown bound, with accuracy models that judge only the guarantee) is skipped with its Pass 2 candidates. - Stage1ToStage3Search selects the exhaustive search and keeping every candidate for tests and developer tools; the selection reports what the search checked and discarded per query. - Equivalence tests against the exhaustive search on the planner-layering examples and seeded random workloads under every cost model.
zzylol
force-pushed
the
feat/joint-search-1-streaming
branch
from
October 21, 2026 01:42
216d60f to
d757aa1
Compare
This was referenced Oct 21, 2026
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
Stage 1 to Stage 3 build every whole-workload candidate, price each one, and keep all of them in
Vecs inStage1ToStage3Run. A five-query prefix of ASAPQuery's random mixed workload (seed-862, workload-01) takes 146 s and 26 GB, and six queries do not finish. This PR implements the first steps ofdocs/design_docs/proposals/joint-planning-search.md:The selected plan does not change.
Part of #673.
What
pareto-front, the Pareto front so far is kept instead. The run and the selection keep their full candidate lists only when the caller asks for every candidate (developer tools, tests).Stage1ToStage3Search { exhaustive: true, .. }composes every choice, as before. The equivalence tests compare the default search against it.Selection::searchandSelectedPlanReport::searchgive:NoValidCandidatenames that query (q<n>) with its first discard.Selection.rejectedkeeps only the first rejection of each cause, plus the losing members of the Pareto front.Selection.costsandSelection.acceptedkeep only the selected candidate, or the Pareto front underpareto-front. Onlykeep_every_candidate(developer tools,select_plan) lists every candidate as before.How
select_planis nowPlanSelector::new+offerper candidate +finish. The checks, pricing and tie order are those of the old loop.pareto-frontdrops an offered candidate that a front member dominates, and drops front members the new candidate dominates. It then selects with the oldchoose, over the front in candidate order.select_plankeeps every candidate, so its output is unchanged.QueryCandidates::checkchecks each query's own candidates:stage1_candidates_of_choice(Pass 1 and Pass 2), thenoptimize_physical_asap, thenchecked_candidate.CheckFailure::size_independent.invalid_prefix(choice)returns the shortest prefix of digits that decides an invalid query.stage1_logical_asap_optimization_skippingthen jumps past every choice that shares that prefix, so the product is not walked one choice at a time. The order of the remaining candidates is unchanged, so ties resolve as before.AccuracyModel::may_accept_regardless_of_guaranteeandAccuracyEvidenceProvider::may_provide_evidenceboth default totrue. With either attrue, no accuracy failure counts as permanent, because a deployment model or its evidence could accept a larger summary of the same plan, and sharing can give it one. Only the built-in model andNoAccuracyEvidencereturnfalse.run_stage1_to_stage3(and the facade'sStage1ToStage3Pass) use the default search. Tests and helpers that inspect every candidate now callrun_stage1_to_stage3_with(.., Stage1ToStage3Search::exhaustive()), which is the old behavior.Key code interfaces added / modified
crates/plan-selection/src/pass/stage1_to_stage3.rscrates/plan-selection/src/lib.rscrates/plan-selection/src/accuracy.rscrates/logical-asap-optimization/src/workload_candidates.rscrates/planner/src/installable.rsUsage
The user plans five queries of seed-862 workload-01 with
plan_installable, each repeating at its interval with a 5% error target, under cost model 1 (Fargate prices):(The numbers come from running the local converter against this commit. The converter isn't part of this PR.)
Before this PR
Each query's invalid summary candidates multiply the product, and every candidate is held until the end.
After this PR
Same machine, release build of the same converter,
plan_installableend to end. Measured on the rebased head (after #787 and #789); the 9-query row is the 10-query prefix without its SQL query:Plans match. For prefixes 3, 4 and 5, the installable plan JSON is byte-identical to main's, apart from the selection report: same DAG, same stored outputs, and the same cost (
3.7117181201488165e-6for 5). Only the candidate ids differ (for 5,P61-m8vsP4813-m8), because skipped choices are no longer numbered.Streaming alone, without method 2, against the default search. The exhaustive search with only the best kept (
Stage1ToStage3Search { exhaustive: true, keep_every_candidate: false }) selects the same plan and physical DAG, at the same cost, as the default search:The 9-query PromQL prefix still takes 128 s. The remaining Cartesian product is the work for methods 5, 6, 3 and 4 (following PRs).
Kubernetes mixin recording rules (
kubernetes_mixin_rules.txt, rules that plan alone, 5% target, 1,000 series per metric):These rules have many targets per query, so the product is large, and this PR does not reduce it. They were not measured on main.
Verification
cargo test --workspace --locked: passes on the rebased head.crates/integration-tests/tests/joint_search_equivalence.rs. Each check compares the default search withStage1ToStage3Search::exhaustive(), the pre-PR path: every candidate priced, then selected at once byselect_plan. It asserts the same plan (flattened), the same cost, the same physical DAG and materialization, and the same Pareto front (or bothNoValidCandidate).planner_layering_examples_select_the_exhaustive_plan: Examples 1 and 2 and Pattern B under cost model 1, cost model 2, and cost model 3 with each objective (64 GB, which rejects Pattern B's ingestion-time panes), plus Pattern A as given under cost model 1.random_small_workloads_select_the_exhaustive_plan: 60 seeded random workloads of 2 to 4 PromQL queries, mixing sum, avg-of-quantile, quantile, topk, rate, max, count, increase,stddev_over_timeandtopk(k, sum by (...) (rate(c_total[w]))), with random targets. Each runs under one cost model in turn, with 1 GB cluster memory so that limits reject some candidates. The test also asserts that every early-rejection cause occurs (no executor kernel, Count-Min weights not proven non-negative, no guarantee, unknown bound).a_calibrated_cost_model_selects_the_exhaustive_plan: random workloads under a measured cost table, so the calibration is fitted over every candidate.Ignored in debug and run here in release:
pattern_a_selects_the_exhaustive_plan_under_every_cost_model: Pattern A as given and monthly, under every cost model, plus Pattern B; 439 s.random_small_workloads_select_the_exhaustive_plan_under_every_cost_model: all 60 seeds under all 5 cost models, 300 comparisons; 14 s.Both pass. Pattern A at rest is left out because its exhaustive search takes hours.
Method 2 behavior:
a_query_candidate_without_a_guarantee_is_discarded_before_composition;a_known_bound_that_misses_is_not_discarded_before_composition(a resize could repair it);a_model_that_may_accept_any_output_discards_nothing(a custom accuracy model);a_query_with_no_valid_candidate_is_named_in_the_failure.streaming_selection_selects_what_select_plan_selects: same selection, cost and front under every cost model, keeping at most the front.skipping_enumeration_drops_only_the_invalid_prefixes.deployment_accuracy_modelnow reads its candidate lists from an exhaustive run instead of the facade.e2e_plancheckssearch.candidates_checked./usr/bin/timearound the converter. Plans were compared as JSON.Limitations and follow-up
devtoolsstage_pipelinestill enumerates exhaustively throughrun_stage2_and_stage3. It keeps every candidate, so its output is unchanged.