Skip to content

Restore OPTION(RECOMPILE) to plan_correction's payload statements - #2767

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/restore-plan-correction-recompile
Sep 1, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
fix/restore-plan-correction-recompile

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 1, 2026 •

Copy link
Copy Markdown
Owner

Fixes #2766.

Problem

PR #2762 removed OPTION(RECOMPILE) from both statements in PlanCorrectionCollector.PayloadBodyText, attributing ~4,237ms of avg CPU on multi-45/Demo to compile cost.

That attribution was never verified. It was inferred from a telemetry signature — high CPU against 381 logical reads and 18 rows — with nothing isolating compile time from execution time. The actual cost was the live Query Store catalog-view joins in the shipping SELECT, which PR #2765 then replaced with staged temps. Staging fixed it; removing RECOMPILE never addressed it.

Measurement

SET STATISTICS TIME ON against SQL Server 2025, current dev payload text — compile CPU per statement: 50 + 13 + 6 + 4 + 11 = 84ms total, clean, no errors. Taken on a Rosetta-emulated container, so 84ms is a ceiling; real hardware compiles faster.

Fix

OPTION(RECOMPILE) restored to the two statements #2762 removed it from, in the post-#2765 query shape — for plan cache hygiene, not performance. This body runs once per database, per server, every cycle; caching a plan for each accumulates cache bulk across the fleet for plans nobody reuses. At ~84ms total, keeping them out of cache costs effectively nothing.

Doc comment rewritten to state that reason and correct the record, so the same "optimization" isn't attempted again.

PayloadBody_DoesNotCarryRecompile (added by #2762) asserted the opposite of intended behavior and is replaced by PayloadBody_CarriesRecompile_ForPlanCacheHygiene, which anchors each hint to its own statement rather than a bare Contains — the enumeration query carries its own RECOMPILE and would otherwise mask a regression — and pins the payload count at exactly 2. The regexes use \s* across the newline because the source is CRLF on disk.

#2765's materialization is untouched

Verified against the built assembly (not source) with a throwaway net10.0 harness:

PASS recs SELECT INTO carries RECOMPILE
PASS final SELECT carries RECOMPILE
PASS exactly 2 RECOMPILE in payload -> 2
PASS staged temp present: #pm_plan_correction_recs
PASS staged temp present: #pm_plan_correction_plans
PASS staged temp present: #pm_plan_correction_queries
PASS staged temp present: #pm_plan_correction_query_text
PASS 4 SELECT INTO staging statements -> 4
PASS final SELECT joins no live QS view -> 0 live refs
PASS final SELECT LEFT JOINs the 3 QS temps -> 3
PASS all 4 temps dropped -> 4

Pin proven red without the fix: against 9168e75c (pre-change dev) the same three RECOMPILE assertions all fail — payload RECOMPILE count 0, both statement anchors unmatched.

What the harness did not cover: it inspects the generated SQL text and structure, not execution. The Windows-only Lite.Tests suite is CI's job. No live-target execution of the modified query was performed.

Test plan

  • dotnet build PerformanceMonitor.Collectors — 0 errors, 0 warnings
  • dotnet build Lite.Tests — 0 errors (8 warnings, all pre-existing in unrelated files)
  • Harness against built assembly — all checks pass, pin proven red on pre-change dev
  • CHANGELOG staged via git hash-object --no-filters + update-index --cacheinfo; --cached --stat shows +2 lines, no CRLF renormalization

#2760 removed both hints believing ~4,237ms of avg CPU on one prod
database was compile cost. That was inferred from a telemetry signature
(high CPU, 381 logical reads, 18 rows), never measured. The cost was the
live Query Store catalog-view joins, which #2764 fixed by staging them
into temps.

Measured compile for the post-#2764 payload, SET STATISTICS TIME ON
against SQL Server 2025: ~84ms CPU total across all five statements
(50 + 13 + 6 + 4 + 11), and that is a ceiling -- taken under emulation.

The hints are back for their real reason: plan cache hygiene. This body
runs once per database, per server, every cycle, so caching a plan for
each accumulates cache bulk fleet-wide for plans nobody reuses.

#2764's staging is untouched: four #pm_plan_correction_* temps, the
EXISTS-gated SELECT INTOs, and the shipping SELECT joining temps rather
than catalog views all verified unchanged against the built assembly.

Fixes #2766.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@claude

claude Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review: #2767 (restore OPTION(RECOMPILE) to plan_correction payload)

Went through the diff against CONTRIBUTING.md's T-SQL style rules and the Lite/Darling parity requirement.

Correctness / style — clean. OPTION(RECOMPILE) is placed on exactly the two statements described (the #pm_plan_correction_recs staging SELECT INTO and the shipping SELECT), matches the house OPTION(RECOMPILE); formatting used elsewhere in this same file (see OnPremDatabaseListQueryText), and the rewritten doc comment's statement count (5 statements, ~84ms total) matches what's actually in PayloadBodyText (4 SELECT INTOs + 1 final SELECT).

Test quality — the new PayloadBody_CarriesRecompile_ForPlanCacheHygiene is a real improvement over the assertion it replaces: anchoring each OPTION(RECOMPILE) to its own statement (rather than a bare Contains) correctly avoids a false pass from the enumeration query's own RECOMPILE, and pinning the count at exactly 2 would have caught this exact regression. Verified the two regexes match the actual CRLF source (\s* correctly spans the statement/hint newline).

Lite/Darling parity — no drift risk here: PlanCorrectionCollector.cs lives in the shared PerformanceMonitor.Collectors project referenced by both Lite/PerformanceMonitorLite.csproj and the Darling service/storage/viewer projects, so there's only one copy of this SQL and both apps pick up the fix automatically. No Darling-side test references this collector's RECOMPILE hints, so nothing there needs a matching update.

Non-blocking observation (pre-existing, not introduced by this PR): the three Query Store staging SELECT INTOs added by #2764 (#pm_plan_correction_plans, #pm_plan_correction_queries, #pm_plan_correction_query_text) still don't carry their own OPTION(RECOMPILE). CONTRIBUTING.md is explicit that 'a statement added to an existing batch needs its own hint — one on a neighbouring statement does not cover it,' and these three sit in the same batch as the two statements this PR just re-hardened. Since #2764 (not this PR) introduced them, and their selectivity is driven by a tiny, already-materialized recommendation set rather than raw catalog cardinality, this is likely low-risk — but worth a follow-up glance since the reasoning in this PR (plan cache hygiene, once-per-database-per-cycle) applies equally to those three.

No correctness, security, or Lite/Darling parity issues found in the actual diff.

@erikdarlingdata
erikdarlingdata merged commit 2501a40 into dev Sep 1, 2026
6 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/restore-plan-correction-recompile branch September 1, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant