Skip to content

Rewrite Query Store grid query using sp_QuickieStore patterns (#143)#145

Merged
erikdarlingdata merged 1 commit intodevfrom
fix/143-query-store-perf-v2
Mar 25, 2026
Merged

Rewrite Query Store grid query using sp_QuickieStore patterns (#143)#145
erikdarlingdata merged 1 commit intodevfrom
fix/143-query-store-perf-v2

Conversation

@erikdarlingdata
Copy link
Owner

Summary

Follow-up to PR #144 — the initial two-phase approach wasn't enough. This rewrites the query using patterns from sp_QuickieStore for significantly better performance on large Query Store datasets.

Four-phase materialization approach:

Phase What Why
1. #intervals Pre-filter interval IDs (clustered PK) Tiny table; all subsequent phases use EXISTS semi-join against it
2. #plan_stats Aggregate runtime_stats by plan_id (clustered PK) EXISTS against #intervals avoids full interval join; PK enables seeks
3. #top_plans Rank + TOP N (numeric columns only) No nvarchar(max) touched yet
4. Final SELECT Hydrate winners with text/plan/metadata Only N rows joined to expensive tables

Additional improvements:

  • Both time filter paths now use rsi.start_time (indexed) — the hoursBack fallback previously filtered on rs.last_execution_time which required scanning all runtime_stats
  • OPTION(RECOMPILE) on aggregation phases — prevents parameter sniffing on date range params
  • Clustered PKs on temp tables — index seeks in subsequent joins
  • TRY_CONVERT for plan XML — safe handling of malformed plans

Performance

Tested on SQL2022 with PerformanceMonitor Query Store data: ~22ms total for all 4 phases (24 intervals, 546 plan aggregations, 5 winners hydrated).

Test plan

  • Build succeeds (0 errors)
  • SQL batch tested on SQL2022 — correct results, ~22ms
  • Verify grid loads correctly in app
  • Test with different time ranges (1h, 24h, 30d)
  • Test with search filters (query_id, query_hash, module)

🤖 Generated with Claude Code

Multi-phase materialization approach for drastically better performance
on large Query Store datasets:

Phase 1: Pre-filter matching interval IDs into #intervals (clustered PK).
  All subsequent phases use EXISTS semi-join against this tiny table
  instead of re-evaluating the time predicate.

Phase 2: Aggregate runtime_stats by plan_id into #plan_stats (clustered
  PK on plan_id). Uses EXISTS against #intervals — semi-join avoids
  materializing the full interval join.

Phase 3: Rank best plan per query_id, materialize TOP N into #top_plans.
  Still no nvarchar(max) columns touched.

Phase 4: Hydrate only the TOP N winners with text, plan XML, and metadata.
  Uses TRY_CONVERT for safe plan XML handling.

Additional improvements:
- Both time filter paths now use rsi.start_time (indexed) instead of
  rs.last_execution_time (requires scanning all runtime_stats rows)
- OPTION(RECOMPILE) on aggregation phases prevents parameter sniffing
  on date range parameters producing stale plans
- Clustered PKs on temp tables enable index seeks in subsequent joins

Tested on SQL2022: ~22ms total for all 4 phases (24 intervals, 546 plans).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@erikdarlingdata erikdarlingdata merged commit 7d259f2 into dev Mar 25, 2026
2 checks passed
@erikdarlingdata erikdarlingdata deleted the fix/143-query-store-perf-v2 branch March 25, 2026 16:51
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