Skip to content

Instantly share code, notes, and snippets.

@possebon
Created April 21, 2026 18:05
Show Gist options
  • Select an option

  • Save possebon/b9d43231983adf7330f251171910a73c to your computer and use it in GitHub Desktop.

Select an option

Save possebon/b9d43231983adf7330f251171910a73c to your computer and use it in GitHub Desktop.
SQL Server Query Store: week-over-week avg_duration regression diff
-- 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