feat(remote): offer the rewrite when no index helps - #236
Open
veksen wants to merge 1 commit into
Open
Conversation
The analyzer derived index candidates and nothing else, so a query whose only real fix is a rewrite reported no_improvement_found and stopped there. That state means the index search came back empty, which is narrower than it sounds. deriveImprovements now derives the rewrites the query's shape allows, plans each one under the same statistics the original was planned under, and ranks both kinds on the one number a reader acts on. The improvements ride beside the state rather than changing it, so the CI gate still fails on what it failed on before. Rewrites are derived from the query the optimizer costed, not from the earlier analysis, which ran before the LIMIT substitution and the pg_stat_statements rewrite. The costing pass gets its own flat budget rather than the query's. Reusing options.timeoutMs would double how long one query can hold the single worker, and spending what the index search left over would starve the expensive queries a rewrite is the only fix for. onOptimizeReady now builds the optimization once. withOptimization mutates the query in place, so an event listener reads the object handed to the emit, and the second literal would have left the live relay without the improvements CI receives. Needs @query-doctor/core 0.29.0, which exports costRewrites and indexCandidates. Co-Authored-By: Claude <noreply@anthropic.com>
veksen
force-pushed
the
feat-rewrite-improvements
branch
from
August 14, 2026 11:16
dc5b5f3 to
3411a30
Compare
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.
Goal
A query whose only real fix is a rewrite reports
no_improvement_foundand stops there, because the analyzer derives index candidates and nothing else. That is Query-Doctor/Site#4013, which carries the measured evidence: 13 of one project's 73 queries match a rewrite rule and carry 97% of the run's cost, and the largest drops from 165,054,297.01 to 445,527.16.This is the producer. The rewrite rules and the ranking already shipped in
@query-doctor/core; nothing called them.Blocked on core 0.29.0. Query-Doctor/Site#4015 adds
costRewritesandindexCandidatesto core, and achore(core): release 0.29.0PR publishes them.package.jsonhere asks for^0.29.0and the lockfile is unchanged, sonpm cifails until both land.What
Before: a CI run's queries carried no
improvementsarray, whatever their shape.After: a settled query carries every proven way to make it cheaper, ranked by measured saving — the index set and any rewrite, together. The state is unchanged, so the gate fails on what it failed on before. A query that reports
no_improvement_foundcan now carry a rewrite beside it, which is the point.How
Read
src/remote/query-optimizer.tsfirst.deriveImprovementsis a module-level function beside the file's other pure derivations. It builds the index candidate from the result the optimizer already produced, derives the rewrites the query's shape allows, costs each against the same statistics, and ranks both kinds throughrankImprovements.Rewrites come from
recent.query, the string the optimizer costed.RecentQuery.analyzeruns beforereplaceLimit(50)and thePssRewriter, soanalysis.rewritesdescribes a different string; costing those would compare two queries rather than two shapes.The costing pass gets a flat budget from
queryTimeoutMs. Reusingoptions.timeoutMs, which the retry ladder grows to 80s, would let one query hold the single worker for twice that. Spending only what the index search left over would starve the expensive queries a rewrite is the only fix for. Flat costs a first attempt 5s it did not spend before, and caps the worst case at the attempt's own budget plus 5s.The deadline is armed only once a query yields a candidate, so a query matching no rule pays a parse and nothing else.
onOptimizeReadybuilds the optimization once and both emits and returns it.withOptimizationisObject.assign(this, …), so a listener reads the object handed to the emit; the previous second literal would have given CI the improvements and left the live relay without them.resultToImprovementsAvailablewas that second literal and is gone.src/remote/optimization.tsandsrc/reporters/site-api.tscarry the field. Both use core's ownImprovementtype. Site's zod schema already acceptsimprovementson both settled states, so nothing changes there.Tests
src/remote/rewrite-improvements.test.tsseeds the correlated aggregateEXISTSshape with the index that serves it already present, so the index search comes back empty and the query settles onno_improvement_found. It then asserts the optimization carries aHOIST_CORRELATED_EXISTSrewrite, gradedunconditional, cheaper than the original by more than 90%. It fails onmain.The query goes through
RecentQuery.analyzerather than the constructor, so the string costed is the one the pipeline produces after the LIMIT substitution and the pg_stat_statements rewrite — the divergencederiveImprovementsexists to handle.The fixture holds real rows. An empty one measured a base cost of 0.01, because stock Postgres sizes a relation from its own page count and ignores injected
reltuples, and the rewrite was then correctly dropped for costing more.npm run typecheckclean,npm test446 passed across 45 files.Follow-ups
Query-Doctor/Site#4014:
withTimeoutisPromise.raceand cannot stopcostRewrites, so an expired pass keeps planning outside_inflight. Pre-existing foroptimizer.runthrough the same helper; this adds a second racer.Not filed, for whoever next touches
RecentQuery: rewrite derivation probably belongs there rather than in the optimizer.computeDisplayQueryalready parses the exact post-substitution string and discards the AST, andrewritesis the same kind of derived property astags,nudgesandtableReferences, which the class already carries. That would also make "does this query admit a hoist" an in-process assertion instead of a container test.