Skip to content

Instantly share code, notes, and snippets.

@JosiahSiegel
Last active July 24, 2025 21:06
Show Gist options
  • Save JosiahSiegel/85c4a59068b81d9715da4b3a2b8b715e to your computer and use it in GitHub Desktop.
Save JosiahSiegel/85c4a59068b81d9715da4b3a2b8b715e to your computer and use it in GitHub Desktop.
Azure SQL Database: Identify Query Issues
DECLARE
@last_execution_time DATETIME = DATEADD(MINUTE, -60, GETUTCDATE())
;WITH FilteredRuntimeStats AS (
SELECT
rs.runtime_stats_id,
rs.plan_id,
rs.last_execution_time,
rs.count_executions,
rs.avg_cpu_time,
rs.avg_duration,
rs.avg_physical_io_reads,
rs.avg_logical_io_reads,
rs.avg_query_max_used_memory
FROM sys.query_store_runtime_stats rs
WHERE rs.last_execution_time >= @last_execution_time
),
PlanMetrics AS (
SELECT
plan_id,
COUNT(DISTINCT runtime_stats_id) as execution_count,
AVG(avg_cpu_time) as avg_cpu,
MIN(avg_cpu_time) as min_cpu,
MAX(avg_cpu_time) as max_cpu,
STDEV(avg_cpu_time) as stdev_cpu,
AVG(avg_duration) as avg_duration,
AVG(avg_physical_io_reads) as avg_physical_io,
AVG(avg_logical_io_reads) as avg_logical_io,
AVG(avg_query_max_used_memory) as avg_memory,
SUM(avg_cpu_time * count_executions) as total_cpu_time,
SUM(avg_duration * count_executions) as total_duration_time,
SUM(avg_physical_io_reads * count_executions) as total_physical_io,
SUM(avg_logical_io_reads * count_executions) as total_logical_io,
SUM(count_executions) as total_executions,
MAX(last_execution_time) as last_execution
FROM FilteredRuntimeStats
GROUP BY plan_id
),
QueryPlans AS (
SELECT
q.query_id,
q.object_id,
q.query_text_id,
p.plan_id,
COUNT(p.plan_id) OVER (PARTITION BY q.query_id) as plan_count
FROM sys.query_store_query q
INNER JOIN sys.query_store_plan p ON q.query_id = p.query_id
),
BaseData AS (
SELECT
qp.query_id,
qp.object_id,
qp.query_text_id,
qt.query_sql_text,
qp.plan_count,
pm.execution_count,
pm.avg_cpu,
pm.min_cpu,
pm.max_cpu,
pm.stdev_cpu,
pm.avg_duration,
pm.avg_physical_io,
pm.avg_logical_io,
pm.avg_memory,
pm.total_cpu_time,
pm.total_duration_time,
pm.total_physical_io,
pm.total_logical_io,
pm.total_executions,
pm.last_execution,
CASE WHEN pm.min_cpu > 0 THEN pm.max_cpu / pm.min_cpu ELSE NULL END as cpu_variance_ratio,
CASE WHEN pm.avg_cpu > 0 THEN pm.stdev_cpu / pm.avg_cpu ELSE NULL END as coefficient_of_variation,
-- Calculate wait time ratio
CASE WHEN pm.avg_cpu > 0 THEN (pm.avg_duration - pm.avg_cpu) / pm.avg_cpu ELSE NULL END as wait_ratio
FROM QueryPlans qp
INNER JOIN PlanMetrics pm ON qp.plan_id = pm.plan_id
INNER JOIN sys.query_store_query_text qt ON qp.query_text_id = qt.query_text_id
WHERE
qp.plan_count > 1
OR pm.stdev_cpu / NULLIF(pm.avg_cpu, 0) > 0.5
OR pm.total_cpu_time > 1000000
OR pm.total_logical_io > 10000000
),
RankedQueries AS (
SELECT
*,
CASE
WHEN object_id = 0 THEN 'ADHOC_' + CAST(query_text_id as varchar(50))
ELSE CAST(object_id as varchar(50))
END as group_key,
ROW_NUMBER() OVER (
PARTITION BY
CASE
WHEN object_id = 0 THEN 'ADHOC_' + CAST(query_text_id as varchar(50))
ELSE CAST(object_id as varchar(50))
END
ORDER BY total_cpu_time DESC
) as query_rank
FROM BaseData
),
AggregatedIssues AS (
SELECT
group_key,
MAX(object_id) as object_id,
MAX(query_text_id) as query_text_id,
MAX(query_sql_text) as query_sql_text,
MAX(CASE WHEN query_rank = 1 THEN query_id END) as query_id_1,
MAX(CASE WHEN query_rank = 2 THEN query_id END) as query_id_2,
MAX(CASE WHEN query_rank = 3 THEN query_id END) as query_id_3,
COUNT(DISTINCT query_id) as query_count,
SUM(total_executions) as total_executions,
MAX(last_execution) as last_execution,
AVG(avg_cpu) as avg_cpu_time,
AVG(avg_duration) as avg_duration_time,
AVG(avg_physical_io) as avg_physical_io,
AVG(avg_logical_io) as avg_logical_io,
AVG(avg_memory) as avg_memory,
AVG(wait_ratio) as avg_wait_ratio,
SUM(total_cpu_time) as total_cpu_time,
SUM(total_duration_time) as total_duration_time,
SUM(total_physical_io) as total_physical_io,
SUM(total_logical_io) as total_logical_io,
MAX(plan_count) as max_plan_count,
MAX(coefficient_of_variation) as max_cv,
CONCAT(
-- Primary issue
CASE
WHEN SUM(total_cpu_time) > 10000000 THEN
'CRITICAL: CPU ' + CAST(CAST(SUM(total_cpu_time)/1000000.0 as decimal(10,1)) as varchar) + 's'
WHEN SUM(total_logical_io) > 100000000 THEN
'CRITICAL: IO ' + CAST(CAST(SUM(total_logical_io)/1000000.0 as decimal(10,1)) as varchar) + 'M'
WHEN MAX(plan_count) > 5 THEN
'HIGH: ' + CAST(MAX(plan_count) as varchar) + ' plans'
WHEN MAX(coefficient_of_variation) > 2 THEN
'HIGH: Variance (CoV=' + CAST(CAST(MAX(coefficient_of_variation) as decimal(5,2)) as varchar) + ')'
WHEN SUM(total_cpu_time) > 1000000 THEN
'MEDIUM: CPU ' + CAST(CAST(SUM(total_cpu_time)/1000000.0 as decimal(10,1)) as varchar) + 's'
WHEN SUM(total_logical_io) > 10000000 THEN
'MEDIUM: IO ' + CAST(CAST(SUM(total_logical_io)/1000000.0 as decimal(10,1)) as varchar) + 'M'
WHEN MAX(plan_count) > 1 THEN
'LOW: ' + CAST(MAX(plan_count) as varchar) + ' plans'
WHEN MAX(coefficient_of_variation) > 0.5 THEN
'LOW: Variance (CoV=' + CAST(CAST(MAX(coefficient_of_variation) as decimal(5,2)) as varchar) + ')'
ELSE ''
END,
-- Root cause indicators
CASE
WHEN SUM(total_executions) > 100000 THEN
' | High frequency (' + CAST(SUM(total_executions) as varchar) + ' exec)'
ELSE ''
END,
CASE
WHEN AVG(avg_cpu) > 1000000 THEN
' | Complex query (' + CAST(CAST(AVG(avg_cpu)/1000000.0 as decimal(5,1)) as varchar) + 's/exec)'
ELSE ''
END,
CASE
WHEN SUM(total_physical_io) > 1000000 THEN
' | Missing index? (' + CAST(CAST(SUM(total_physical_io)/1000000.0 as decimal(10,1)) as varchar) + 'M phys reads)'
ELSE ''
END,
CASE
WHEN AVG(avg_memory) > 1024 THEN
' | Memory intensive (' + CAST(CAST(AVG(avg_memory)/1024.0 as decimal(10,1)) as varchar) + 'MB)'
ELSE ''
END,
CASE
WHEN AVG(wait_ratio) > 2 THEN
' | Blocking/waits (wait ' + CAST(CAST(AVG(wait_ratio) as decimal(5,1)) as varchar) + 'x CPU)'
ELSE ''
END,
CASE
WHEN MAX(plan_count) > 1 AND MAX(coefficient_of_variation) > 0.5 THEN
' | Param sniffing'
ELSE ''
END,
-- Anti-patterns
CASE
WHEN MAX(CASE WHEN query_sql_text LIKE '%CURSOR%' THEN 1 ELSE 0 END) > 0
THEN ' | Cursor' ELSE ''
END,
CASE
WHEN MAX(CASE WHEN query_sql_text LIKE '%IF%@%' OR query_sql_text LIKE '%CASE%WHEN%@%' THEN 1 ELSE 0 END) > 0
THEN ' | Conditional' ELSE ''
END,
CASE
WHEN MAX(CASE WHEN query_sql_text LIKE '%SELECT *%' THEN 1 ELSE 0 END) > 0
THEN ' | SELECT *' ELSE ''
END,
CASE
WHEN MAX(CASE WHEN query_sql_text LIKE '%NOT IN%' THEN 1 ELSE 0 END) > 0
THEN ' | NOT IN' ELSE ''
END,
CASE
WHEN MAX(CASE WHEN query_sql_text LIKE '%OR%OR%OR%' THEN 1 ELSE 0 END) > 0
THEN ' | Multiple ORs' ELSE ''
END,
CASE
WHEN MAX(CASE WHEN query_sql_text LIKE '%DISTINCT%' AND query_sql_text LIKE '%JOIN%' THEN 1 ELSE 0 END) > 0
THEN ' | DISTINCT+JOIN' ELSE ''
END
) as issue_summary,
-- Resource impact-based severity scoring
CASE
-- Extreme resource consumption (90-100)
WHEN SUM(total_cpu_time) > 100000000 THEN 100 -- >100s CPU
WHEN SUM(total_logical_io) > 1000000000 THEN 95 -- >1B reads
WHEN SUM(total_cpu_time) > 50000000 THEN 90 -- >50s CPU
-- High resource consumption (70-89)
WHEN SUM(total_logical_io) > 500000000 THEN 85 -- >500M reads
WHEN SUM(total_cpu_time) > 10000000 THEN 80 -- >10s CPU
WHEN SUM(total_logical_io) > 100000000 THEN 75 -- >100M reads
WHEN SUM(total_cpu_time) > 5000000 THEN 70 -- >5s CPU
-- Moderate resource consumption with issues (50-69)
WHEN SUM(total_cpu_time) > 1000000 AND MAX(plan_count) > 5 THEN 65
WHEN SUM(total_logical_io) > 10000000 AND MAX(plan_count) > 5 THEN 60
WHEN SUM(total_cpu_time) > 1000000 AND MAX(coefficient_of_variation) > 2 THEN 55
WHEN SUM(total_logical_io) > 10000000 AND MAX(coefficient_of_variation) > 2 THEN 50
-- Lower resource consumption with issues (30-49)
WHEN SUM(total_cpu_time) > 1000000 THEN 45 -- >1s CPU
WHEN SUM(total_logical_io) > 10000000 THEN 40 -- >10M reads
WHEN MAX(plan_count) > 5 THEN 35
WHEN MAX(coefficient_of_variation) > 2 THEN 30
-- Minor issues (10-29)
WHEN MAX(plan_count) > 1 THEN 20
WHEN MAX(coefficient_of_variation) > 0.5 THEN 10
ELSE 0
END as severity_score
FROM RankedQueries
GROUP BY group_key
)
SELECT TOP 100
-- Query type column
CASE
WHEN object_id = 0 THEN 'Ad-hoc'
ELSE 'Stored Proc'
END as query_type,
-- Query/procedure identifier
CASE
WHEN object_id = 0 THEN
LEFT(REPLACE(REPLACE(query_sql_text, CHAR(13), ' '), CHAR(10), ' '), 80) +
CASE WHEN LEN(query_sql_text) > 80 THEN '...' ELSE '' END
WHEN object_id > 0 AND OBJECT_NAME(object_id) IS NULL THEN
'Dropped (id=' + CAST(object_id as varchar) + ')'
ELSE
OBJECT_NAME(object_id)
END as query_name,
query_text_id,
query_id_1,
query_id_2,
query_id_3,
query_count,
total_executions,
last_execution AT TIME ZONE 'UTC' AT TIME ZONE 'Eastern Standard Time' as last_execution_est,
CAST(avg_cpu_time/1000000.0 as DECIMAL(18,2)) as avg_cpu_seconds,
CAST(total_cpu_time/1000000.0 as DECIMAL(18,2)) as total_cpu_seconds,
CAST(total_logical_io/1000000.0 as DECIMAL(18,2)) as total_io_million_reads,
severity_score,
issue_summary as issues
FROM AggregatedIssues
WHERE issue_summary != ''
ORDER BY severity_score DESC, total_cpu_time DESC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment