Created
April 21, 2026 18:05
-
-
Save possebon/b9d43231983adf7330f251171910a73c to your computer and use it in GitHub Desktop.
SQL Server Query Store: week-over-week avg_duration regression diff
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
| -- SQL Server Query Store week-over-week regression diff | |
| -- Compares avg_duration between the last 7 days and the 7 days before. | |
| -- Returns queries that slowed by >= 2x and ran >= 100 times in each window. | |
| -- | |
| -- Works on SQL Server 2016+ with Query Store enabled. | |
| -- Blog post: https://www.linkedin.com/in/fernando-possebon/ | |
| DECLARE @now DATETIME2 = SYSUTCDATETIME(); | |
| DECLARE @this_week_start DATETIME2 = DATEADD(day, -7, @now); | |
| DECLARE @last_week_start DATETIME2 = DATEADD(day, -14, @now); | |
| WITH base AS ( | |
| SELECT p.query_id, | |
| rs.plan_id, | |
| i.start_time, | |
| rs.count_executions, | |
| rs.avg_duration AS avg_dur_us | |
| FROM sys.query_store_runtime_stats rs | |
| JOIN sys.query_store_runtime_stats_interval i | |
| ON i.runtime_stats_interval_id = rs.runtime_stats_interval_id | |
| JOIN sys.query_store_plan p | |
| ON p.plan_id = rs.plan_id | |
| WHERE rs.execution_type = 0 -- regular executions only (not aborted/exception) | |
| ), | |
| this_week AS ( | |
| SELECT query_id, | |
| SUM(CAST(count_executions AS BIGINT)) AS execs, | |
| SUM(avg_dur_us * count_executions) / NULLIF(SUM(count_executions), 0) AS avg_dur_us | |
| FROM base | |
| WHERE start_time >= @this_week_start | |
| GROUP BY query_id | |
| ), | |
| last_week AS ( | |
| SELECT query_id, | |
| SUM(CAST(count_executions AS BIGINT)) AS execs, | |
| SUM(avg_dur_us * count_executions) / NULLIF(SUM(count_executions), 0) AS avg_dur_us | |
| FROM base | |
| WHERE start_time >= @last_week_start | |
| AND start_time < @this_week_start | |
| GROUP BY query_id | |
| ) | |
| SELECT tw.query_id, | |
| lw.avg_dur_us / 1000.0 AS last_wk_avg_ms, | |
| tw.avg_dur_us / 1000.0 AS this_wk_avg_ms, | |
| CAST(tw.avg_dur_us AS FLOAT) / NULLIF(lw.avg_dur_us, 0) AS slowdown_ratio, | |
| lw.execs AS last_wk_execs, | |
| tw.execs AS this_wk_execs, | |
| LEFT(qt.query_sql_text, 200) AS query_preview | |
| FROM this_week tw | |
| JOIN last_week lw ON lw.query_id = tw.query_id | |
| JOIN sys.query_store_query q ON q.query_id = tw.query_id | |
| JOIN sys.query_store_query_text qt ON qt.query_text_id = q.query_text_id | |
| WHERE tw.execs > 100 | |
| AND lw.execs > 100 | |
| AND tw.avg_dur_us > 2 * lw.avg_dur_us | |
| ORDER BY slowdown_ratio DESC; | |
| -- Knobs worth tuning to your workload: | |
| -- execution_type = 0 : regular executions only. Flip to compare error-rate shifts. | |
| -- execs > 100 : noise floor. Lower it for ad-hoc-heavy workloads. | |
| -- avg_dur_us > 2 * .. : slowdown ratio. Try 1.5x to catch more, 3x for the screaming ones. | |
| -- | |
| -- For plan-change detection, swap query_id for plan_id and add query_plan_hash | |
| -- to the output. Regressions from a recompile then show up as *new* rows | |
| -- instead of row-level deltas. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment