Repository navigation
Force-plan recommendation is replica-blind now that plan regression splits by replica_role #1882
Description
Spun out of #1850, which made the analysis-layer Query Store dedups key on replica_role and carry it through the drill-down row shape. That change is correct on its own terms, and it surfaces a question the remediation layer has never had to answer.
What #1850 changed
On a SQL Server 2022+ AG with Query Store for secondary replicas enabled, one query can now legitimately appear in the regressed_queries drill-down TWICE — once as PRIMARY, once as SECONDARY — with its own best plan on each, because the two replicas run genuinely different workloads against the same shared Query Store.
The mechanical half, already fixed in #1850
FactRemediation.ExtractPlanRegressionTargets reads a fixed set of keys and does not know about replica_role. Two rows for one query would have produced two ForcePlanTargets naming the SAME (database, query_id) with DIFFERENT best_plan_id, which renders as two sp_query_store_force_plan calls for one query — mutually exclusive instructions where whichever the operator runs last silently wins. #1850 dedups on (database, query_id), keeping the first, which the SQL orders regression_factor DESC so it is the worst. That stops the contradiction and nothing more.
What is left, and why it is not mechanical
- Should a regression observed only on a read-only SECONDARY recommend forcing a plan at all? Forcing is done on the primary. A recommendation driven by read-only reporting workload changes the plan the primary's write workload gets.
- When both replicas regressed, is "the worse regression factor" the right tiebreak? Today's dedup keeps it because it is consistent with the existing cap-of-5-worst behavior, not because anyone decided a secondary's 12x should outrank a primary's 3x.
- Should
ForcePlanTargetand the rendered recommendation NAME the replica? It is a positional record and the renderer is pinned by a render-stability golden test, so adding the field is a deliberate change rather than an additive one. The drill-down and the MCP payload already carryreplica_role(AnalysisNotificationService.FlattenIntoenumerates every property, so it renders in email/webhook/toast automatically) — it is only the typed remediation path that drops it.
Anyone picking this up should confirm the current SQL Server 2022+ semantics for plan forcing under Query Store for secondary replicas against MS Learn first, rather than reasoning from the row shape — whether forcing is per-query or can be scoped per replica is the fact the whole decision turns on, and #1850 deliberately did not assume an answer.
Impact bound
Zero on any standalone or non-AG server, and zero below SQL Server 2022: replica_role is NULL everywhere else, so the drill-down produces one row per query exactly as before and the dedup is a no-op.
Activity
Merge pull request #1917 from erikdarlingdata/fix/1881-1882-alert-ana…
erikdarlingdata commentedon Jul 31, 2026
Shipped in #1917, and the docs verification the issue demanded overturned its premise twice: Query Store for secondaries is GA in SQL Server 2025 (2022 = trace-flagged preview, not production), and forcing IS replica-scopeable via @replica_group_id - which made the old behavior WRONG rather than undecided (a primary-scoped force justified by a secondary's workload). Now: the primary's row wins, regression factor tie-breaks only among equals, secondary-derived recommendations disclose in the copy-paste SQL itself, and the back-out line's unforce asymmetry (defaults to the LOCAL replica) is documented. All four MS Learn citations in the code. Collecting replica_group_id so the recommendation can BE scoped is #1914.
Dash state-only alert values (erikdarlingdata#1846); split analysis Q…
Fix alert history's semantically-wrong stored numbers and the replica…
Correct the force-plan scoping guidance to the call form that actuall…