Skip to content

Fix @wait_filter matching waits from all of Query Store history (#852) - #853

Merged
erikdarlingdata merged 1 commit into
devfrom
fix-852-wait-filter-interval-scope
Aug 4, 2026
Merged

Fix @wait_filter matching waits from all of Query Store history (#852)#853
erikdarlingdata merged 1 commit into
devfrom
fix-852-wait-filter-interval-scope

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Fixes #852 — the sibling of #850/#851, found while verifying that fix.

What was wrong

The #wait_filter populate aggregated sys.query_store_wait_stats per plan_id with no date or interval restriction at all, then fed the plan_ids into an EXISTS against the date-windowed runtime stats. Two consequences:

  1. A query whose waits of the requested category happened entirely outside the requested window still passed the filter, as long as it executed at all inside the window.
  2. The TOP (@top) was ranked by all-history wait totals, so plans with big waits long ago could crowd out plans that actually waited inside the window — wrong in both directions.

The fix

Join query_store_wait_stats to query_store_runtime_stats on (plan_id, runtime_stats_interval_id, execution_type) and filter qsrs.last_execution_time by @start_date/@end_date — the same join grain as #851 and the same date semantics as the proc's main @where_clause. The block's sp_executesql call previously only passed @top; it now passes the date parameters too.

Validation

SQL Server 2022 (RTM-CU26), Query Store with 1-minute intervals, two queries against the same table:

  • Q1: ~62 s of lock waits in interval 03:00 (phase A), then executed again unblocked in interval 03:08 — so it has an in-window execution but no in-window lock waits.
  • Q2: ~5 s of lock waits in interval 03:08 (phase B) only.

@wait_filter = 'lock' results:

window before after
phase B only Q1 + Q2 ❌ (Q1's lock waits were entirely in phase A) Q2 only ✅
both phases Q1 + Q2 Q1 + Q2 ✅

Tested

  • The changed code path executed on SQL Server 2022 with the window comparison above; ground truth confirmed per-interval from sys.query_store_wait_stats.
  • Proc installs clean.
  • A first draft of this fix scoped intervals by qsrsi.start_time >= @start_date and the repro caught it dropping the partial interval at the window's leading edge (false negative for a query that waited inside the window). The final shape filters on qsrs.last_execution_time instead, which has no such boundary mismatch and matches the rest of the proc.

Not tested

  • Regression mode and @get_all_databases = 1 (no code path difference — same dynamic SQL per database; #wait_filter is truncated per database iteration).
  • Older SQL Server versions locally; the join columns exist on every version that has query_store_wait_stats (2017+), and CI runs 2017/2019/2022/2025.

The #wait_filter populate aggregated query_store_wait_stats with no
date restriction, so queries whose waits happened entirely outside
the requested window still passed the filter, and the TOP was ranked
by all-history wait totals. Joining to query_store_runtime_stats on
(plan_id, runtime_stats_interval_id, execution_type) and filtering
last_execution_time by the date range matches the semantics of the
main where clause and the wait sort order joins from #851.

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

claude Bot commented Aug 4, 2026

Copy link
Copy Markdown

Claude finished @erikdarlingdata's task in 2m 5s —— View job


Review complete ✅

Summary

This is a clean, correct fix. The #wait_filter populate query now joins sys.query_store_wait_stats to sys.query_store_runtime_stats on (plan_id, runtime_stats_interval_id, execution_type) and filters on qsrs.last_execution_time, matching both:

  • The exact join grain used by the sibling fix in Fix wait sort orders counting waits across all of Query Store history (#850) #851 (sp_QuickieStore/sp_QuickieStore.sql:10660-10663, :10794-10796).
  • The exact date-filter semantics of the proc's main @where_clause construction (sp_QuickieStore/sp_QuickieStore.sql:8762-8763), which uses qsrs.last_execution_time rather than qsrsi.start_time — avoiding the partial-interval boundary bug the PR description says was caught in an earlier draft.

Correctness

  • Join cardinality: query_store_wait_stats has multiple rows per (plan_id, runtime_stats_interval_id, execution_type) (one per wait_category), and query_store_runtime_stats has exactly one. The new JOIN is therefore a simple filter on the qsws side (no fan-out), so the existing GROUP BY qsws.plan_id / HAVING SUM(...) > 0 / TOP (@top) ... ORDER BY SUM(...) DESC logic downstream is unaffected in shape, only in which rows are included — which is the intended fix.
  • Parameter type/placement: @start_date/@end_date are added to the sp_executesql call as datetimeoffset(7), matching the procedure's parameter declarations (sp_QuickieStore/sp_QuickieStore.sql:60-61) and every other parameterized date filter in the file (e.g. :5073-5076, :5644-5647, :5769-5775). This code runs well after the @start_date/@end_date UTC-normalization block (:3816-3894), so it's operating on the same normalized values the rest of the query uses.
  • ON clause table order: ON qsws.plan_id = qsrs.plan_id puts the previously-referenced table (qsws, from the FROM clause) first, consistent with the established pattern from Fix wait sort orders counting waits across all of Query Store history (#850) #851's qsrs/qsws joins elsewhere in this same procedure.

Style / repo constraints

  • Block comment (/* ... */) explaining the join rationale — correct per CLAUDE.md, and genuinely useful since the "why" (all-history wait matching + all-history TOP ranking, both fixed by one join) isn't obvious from the code alone.
  • No new dependencies (helper objects, views, etc.) — join is against existing system DMVs already used elsewhere in this proc the same way.
  • Install-All/DarlingData.sql untouched, @version/@version_date untouched — good.
  • No SQL Server version-compatibility concerns: query_store_runtime_stats and query_store_wait_stats and their referenced columns are unchanged from what's already used elsewhere in this file.

No blocking issues found. Nice catch on the false-negative in the leading-edge-of-window scenario mentioned in the PR description — good that the repro caught it before landing.

@erikdarlingdata
erikdarlingdata merged commit 0a03721 into dev Aug 4, 2026
6 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix-852-wait-filter-interval-scope branch August 4, 2026 04:41
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