Repository navigation
A PostgreSQL operator now learns which statement got slower when its plan changed, and why: PG_PLAN_REGRESSION joins the captured plan_hash flip to the statement's per-call step, PG_PARAMETER_SENSITIVITY names the skewed predicate column behind a multi-plan statement, and the server-wide per-call mean is baselined (partial #3691, lane 27) - #3798
Merged
Merged
Conversation
…plan changed: PG_PLAN_REGRESSION joins pg_plan_capture's hash flip to pg_statement_stats' per-call step, PG_PARAMETER_SENSITIVITY names the skewed predicate column behind a multi-plan statement, and ANOMALY_PG_PLAN_REGRESSION baselines the server-wide per-call mean (partial #3691, lane 27)
This was referenced Sep 20, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 20, 2026
This was referenced Sep 20, 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.
What does this PR do?
Headline mechanism. A PostgreSQL target's
analyze_serverpass now names the statement whose plan changed and says what the change cost.PgTargetFactCollector.CollectPlanFactsAsync(the v3 plan family,pg_plans) joinspg_plan_capture'splan_hashflips topg_statement_stats' STORED per-call deltas split at the flip and emitsPG_PLAN_REGRESSION— "statement queryid N got 10× slower per call when its plan changed (20 ms → 200 ms)" — graded on the after ÷ before ratio behind an absolute per-call delta and a call floor on each side;PG_PARAMETER_SENSITIVITYnames the mechanism when onequery_idwas captured under three or more plans andpg_qualstats(pg_predicate_stats) names a predicate column whosepg_column_stats.top_value_frequencysays one value covers half the table; andANOMALY_PG_PLAN_REGRESSIONbaselines the SERVER-WIDE per-call mean (Σ stored exec-time deltas ÷ Σ stored call deltas per collection) hour-of-week and folds onto the regression. PARTIAL for #3691 — lane 27. The checklist line this PR completes, verbatim: "Plan regression (statement step-change ×plan_hashflip …); parameter sensitivity (…)" — the whole line is this lane's. Do not close.What the family concludes, and from which table
PG_PLAN_REGRESSION(pg_plan_capture×pg_statement_stats). Per (query_id,plan_hash) the captures collapse to a first-seen time; ordered by first-seen, the plan the window OPENED with is "before" and the plan it CLOSED with is "after". The statement's stored deltas (delta_calls,delta_total_exec_time_ms— lane 7's discipline: the collector's deltas, never a re-differencing) are summed before the second hash first appeared and from the last hash's first appearance on; with three or more hashes the middle plans' minutes belong to neither side, counted (plan_hash_count) but never averaged into a plan they were not run under.Fact.Valueis the ratio; metadata carries both means, both call counts, the two hashes as their leading 48 bits (12 hex characters, exact in a double — a prefix the reader matches againstget_pg_plans), whether the top node changed and the node-count delta, the ages of the first and last change, and how many statements flipped.query_idrides onObjectName(lane 7's 2^53 reason). Orphans (query_id = 0, no%Q) are excluded. ONE fact per pass — the worst flip that clears every bar, else the largest-ratio flip with enough calls to compare (graded 0, shown), else nothing. Absence of a capture is "not slow enough to log", never "no plan" — except whenpg_plan_capture_readiness'library_loadedfacet saysauto_explainis NOT loaded, when the fact is emittedunavailablewithreason_auto_explain_off(the write family's shape) and the advice says how to load it and what it costs.PG_PARAMETER_SENSITIVITY(pg_plan_capture×pg_predicate_stats×pg_column_stats, joined by (database, schema, table, column) NAME per database — both tables are per-database by design). The most-skewed statement with a skewed column; when statements flipped three ways but no predicate row exists, ONEunavailablefact with the reason read frompg_extension_availability(reason_pg_qualstats_absent) or, installed but unsampled,reason_no_predicate_sampled(its defaultsample_rateis 1/max_connections — V98's warning).DatabaseNameis the skewed column's database.ANOMALY_PG_PLAN_REGRESSION(pg_statement_stats, server-wide). The coordinator's v3 ruling on the decision the stub left to this lane: the baseline seam keys one series per (server, metric), so the arm is the SERVER-WIDE per-call mean — honest but blunt, and the detector's doc, the advice ("not which one"), andserver_wide = 1in the metadata all say so. Pair gate (Brains-review campaign: deferred structural residue (from #3538 / #3539 / #3540 / #3541) #3653, peak AND window mean), heavy-tail robust cutoff by reference (a per-call mean is a query-duration quantity; the PG population is unmeasured),pg_statement_statsjoinsBaselineServingRawCollectors(D10 — the family's heaviest read and the retention an operator is likeliest to shorten).The lineage shape of every bar
All unmeasured, each with the marker within six lines and the calibrating read named;
threshold_lineage = 0stamped on every graded fact (= 1on theunavailableshapes, where no bar decided — the write family's rule). The 2026-09-19 calibration read statement shares and busy fractions, never a plan-hash or per-collection mean distribution, so no bar here may claim measured.PlanRegressionRatioConcerning 2.0 / Critical 5.0(self-gated to 0 under 2×);PlanRegressionMinDeltaMs 50;PlanRegressionMinCallsPerSide 20;PlanCoFireBoost 0.3(two corroborators → 1.6, over the notify floor; one → 1.3, under);ParameterSensitivityMinHashes 3;ParameterSensitivitySkewFrequency 0.5;ParameterSensitivityBase 0.4 / CoFireBoost 0.5(D5: 0.4 × 1.5 = 0.6);AnomalyThresholds.PgStatementMeanMsFloor 10 ms / Fallback 250 ms(the// lane 27block; pinned not equal to the SQL ServerQueryDurationFloorUsby value). The scorer's bars are passed to the sensitivity read as parameters and referenced by the collector's pick, so the read, the pick and the grade cannot disagree.Co-fire gating
PG_PLAN_REGRESSION: +0.3 whenPG_BAD_ACTOR_<queryid>fired for the SAME statement (string match onObjectNameagainst lane 7's key shape — a different statement's bad actor, however hot, lifts nothing), +0.3 whenANOMALY_PG_PLAN_REGRESSIONfired (an amplifier reading the anomaly's verdict, lane 15's pattern — never an edge from a base-0 fact).PG_PARAMETER_SENSITIVITY: +0.5 when the regression fired on the same statement. Edges:PG_PLAN_REGRESSION → PG_BAD_ACTOR(the alias) opens only when the pass's top bad actor IS the regressed statement —ResolveBadActorhas no per-statement input and pointing the story at a different statement's text would be worse than no edge; the amplifier still lifts (stated in the graph file);PG_PARAMETER_SENSITIVITY → PG_PLAN_REGRESSION(same id);ANOMALY_PG_PLAN_REGRESSION → PG_PLAN_REGRESSION.PgTargetScorer.SameStatementFireddecides "the same statement" once for amplifiers and edges.Advice
Value-stated: the step, the ratio and delta, calls on each side, the hash prefixes, the node change, the flip's age, the co-fires, the skew frequency,
n_distinct(ratio-shaped negatives rendered as such), the worst estimate error. Levers with counter-objectives, none emitted as a statement:plan_cache_mode = force_custom_plan(planning time on every execution), a larger per-column statistics target (described as the knob; ANALYZE time and statistics space),ANALYZE(one sample read), a rewrite (application change). Thequeryidre-key caveat on every block. NoCREATE INDEXanywhere (D8) — pinned on every composed and static block and on every string literal in the family's files.PostgreSQL has no plan cache to force the earlier plan back into— said, not implied.What it does NOT do
No
PG_SEQ_SCAN_ADVISORY(lane 30 — its scorer arm, advice arm, graph edge and the marked/* lane 30: Seq-Scan read */region at the end ofCollectPlanFactsAsyncare left for it; nothing here forces a rewrite). No per-statement baseline (the seam's key dimension is a shared-file item, reported). No same-id alias resolution (reported). No tool-row edit inPgTargetToolRecommendations(the plumbing rows already nameget_pg_plans/get_pg_top_queries/get_pg_column_stats/get_pg_predicate_stats; the prose names all five tools). No rung, no shared-switch edit, noPgTargetFactKeysedit. The SQL Server pass is untouched: every new arm sits behind thepg_planssource or aPG_/ANOMALY_PG_key, and the routing pins pass unchanged.Censuses moved deliberately (each edited, none deleted)
PgTargetSharedSwitchRoutingTests(threeNull→NotNull, the anomaly's equality + no "Anomalous spike");PgTargetAnomalyTests(served metrics 9 → 10 with the (name, table) row; inert loop losesPgStatementMeanMs; fence test nine → ten filled with the plan partial in the filled list and only the kernel stub exempt;StatementMeanWindowSqlin the detector roster; the two constants in theAnomalyThresholdsroster);PgTargetV3PlumbingTests(the two filled keys pinned self-gating + composing, the seq-scan / kernel / memory stubs still inert);BaselineSupplyTests(9 → 10 sources,pg_statement_statsin the derived set);PgTargetBetweenWavesV2Tests(floor set 11 → 12);PgTargetMeasuredLineageTests.s_stillUnmeasured(every plan bar and both anomaly bars).PgTargetFactCollectorTests,PgTargetThresholdLineageTests,LocalClockBucketKeyTests,StoreSqlClockDisciplineTests,DocCommentHygiene,RepoFileAdoptionTests,CommentFilterAdoptionTests,LivePostgresCollectionHygieneTests,LiveCleanupConversionRatchetTests: green, no edit. No file mentions anaurora_stat_*function.Test plan
New
Darling/Darling.Tests/PgTargetPlanTests.cs(22 tests, 1 gated): lineage; the ramp and every self-gate; the collector's PURE picks (PickRegression/PickSensitivity, exposed internal, pinned on arranged rows); amplifiers through the realScoreAll(same-id vs other-id); edges incl. the alias opening only for the top bad actor; traversal; advice by clause for every branch and bothunavailableshapes; the three reads by text; baseline arm + window read sharing one grain; gated e2e — 31 days of one-minutepg_statement_statsfor five statements (223,205 rows, set-based), three captured hashes on the hot statement, its mean 20 → 200 ms from the first flip, a skewed predicate column, readiness + extension rows; the REALanalyze_serverreturnsPG_PLAN_REGRESSIONat 1.6 naming the id, leafPG_BAD_ACTOR_<id>, headline exact, hash prefixes in the prose;PG_PARAMETER_SENSITIVITYat 0.6 chained;ANOMALY_PG_PLAN_REGRESSIONin the regression's incident with the family's prose;get_analysis_facts source=pg_plans→ two facts, boththreshold_lineage 0; then a second server withlibrary_loaded = false→ the oneunavailablefact with its reason.Executed here, not only compiled: a net10.0 harness (
AssemblyName=Darling.Tests, inside the worktree underobj/, deleted before commit) compiling everyPgTarget*.csplusFactSourceRegistryTests,CollectionCaveatsTests,BaselineSupplyTests,LocalClockBucketKeyTests,BaselineTierJoinShapeTests,StoreSqlClockDisciplineTests,DocCommentHygieneTests,RepoFileAdoptionTests,CommentFilterAdoptionTests,LivePostgresCollectionHygieneTests,LiveCleanupConversionRatchetTests,FleetIdentifierScrubTests,FactCollector*Tests,AnalysisPass*Tests,CommandDeadlineScannerAdoptionTests,AsOfWindowAnchorTests— 873 / 873 green, 0 skipped — against a throwawaytimescale/timescaledb:2.28.1-pg18(Docker contextdesktop-linux, role and databasedarling, migrated byPgMigrations.MigrateAsync, torn down after), every gated e2e in the set included. The harness caught three of my own first-draft defects (a theory row under the delta bar; a prose clause the test forgot; a pin reading a stripped literal) — each was the test, not the product. Rebased ontoorigin/devafter lanes 29 / R5 landed (no conflicts; file-disjoint), compile re-verified. First Windows execution is CI's.Builds, zero warnings:
PerformanceMonitor.Analysis,PerformanceMonitor.Darling.Analysis,PerformanceMonitor.Darling.Service,Darling.Tests,Lite.Tests,deprecated/Dashboard.Tests(-p:EnableWindowsTargeting=trueon the test projects).Which component(s) does this affect?
How was this tested?
Above. No production store or fleet server was read; the rig was a local container, torn down.
Checklist
dotnet build -c Debug)Partial for #3691 (lane 27). CHANGELOG deliberately untouched — the coordinator splices this wave's entries.