When a SQL Server “feels slow”, the question is always the same: which queries, and why?
SQL Server already knows. Its Dynamic Management Views (DMVs) record what is running right
now and how every cached query has performed since it was compiled. This guide shows four
queries we use on client servers to go from “it’s slow” to a short list of statements worth
tuning.
1. What is running right now?
Start with the live picture. sys.dm_exec_requests lists every request currently executing.
Joining it to the statement text and the session tells you who is running what, for how long,
and what it is waiting on.
SELECT
r.session_id,
s.login_name,
s.host_name,
DB_NAME(r.database_id) AS database_name,
r.status,
r.command,
r.wait_type,
r.wait_time AS wait_ms,
r.blocking_session_id,
r.total_elapsed_time / 1000 AS elapsed_seconds,
r.cpu_time AS cpu_ms,
r.logical_reads,
SUBSTRING(t.text,
r.statement_start_offset / 2 + 1,
(CASE r.statement_end_offset
WHEN -1 THEN DATALENGTH(t.text)
ELSE r.statement_end_offset
END - r.statement_start_offset) / 2 + 1) AS running_statement
FROM sys.dm_exec_requests AS r
JOIN sys.dm_exec_sessions AS s ON s.session_id = r.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE s.is_user_process = 1
AND r.session_id <> @@SPID
ORDER BY r.total_elapsed_time DESC;
How to read it:
blocking_session_idis not 0: the request is stuck behind another session. Look at
that session first; the slow query may be the victim, not the cause.elapsed_secondsmuch larger thancpu_ms: the query spends most of its time waiting.
wait_typesays on what (PAGEIOLATCH_*= reading from disk,LCK_M_*= locks,
CXPACKET/CXCONSUMER= parallelism).- High
cpu_msandlogical_reads: the query is doing a lot of work, often because of a
missing index or a poor plan.
2. Which queries cost the most over time?
A query that runs for 50 milliseconds but executes 200,000 times a day can hurt more than a
single 30-second report. sys.dm_exec_query_stats keeps running totals for every cached
plan, so you can rank queries by their total and average cost.
SELECT TOP (20)
qs.execution_count,
qs.total_worker_time / 1000 AS total_cpu_ms,
qs.total_worker_time / qs.execution_count / 1000 AS avg_cpu_ms,
qs.total_elapsed_time / qs.execution_count / 1000 AS avg_elapsed_ms,
qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
qs.last_execution_time,
DB_NAME(t.dbid) AS database_name,
SUBSTRING(t.text,
qs.statement_start_offset / 2 + 1,
(CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(t.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset) / 2 + 1) AS statement_text,
qs.plan_handle
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS t
ORDER BY qs.total_worker_time DESC;
Change the ORDER BY to match the problem:
| Symptom | Sort by |
|---|---|
| CPU pegged | total_worker_time DESC |
| Disk / memory pressure | total_logical_reads DESC |
| Users complain a specific screen is slow | avg_elapsed_ms DESC |
The times in this DMV are stored in microseconds, which is why the query divides by 1,000.
3. Why is that query slow? Read its plan
Once you have a suspect, its execution plan shows how SQL Server is running it. Copy the
plan_handle from the previous result into this query:
SELECT qp.query_plan
FROM sys.dm_exec_query_plan(0x...) AS qp; -- paste the plan_handle here
In SQL Server Management Studio, click the XML in the result to open the graphical plan.
Look for:
- Missing index warnings (green text at the top of the plan). Treat them as hints, not
orders: review them against the indexes the table already has before creating anything. - Scans on large tables where you expected a seek.
- Thick arrows (many rows) going into a sort, hash, or key lookup.
- Big gaps between estimated and actual rows, a sign of stale statistics or parameter
sniffing.
4. Know the limits of the plan cache
The DMVs above only see plans that are still in the plan cache. They forget everything after
a restart, a failover, a plan recompile, or memory pressure. So “nothing shows up” does not
mean “nothing was slow last night”.
On SQL Server 2016 and later, turn on Query Store for databases you care about. It keeps
query performance history on disk, survives restarts, and lets you compare a query’s plans
over time:
ALTER DATABASE [YourDatabase] SET QUERY_STORE = ON;
Then use the built-in reports in SSMS (database → Query Store → Top Resource Consuming
Queries) to see what was slow at a specific time.
A simple routine
- When users report slowness, run query 1 to see what is running and what it is waiting on.
- Once a week, run query 2 to catch the expensive queries nobody complained about yet.
- Read the plan for the top two or three, fix one thing at a time, and measure again.
Done consistently, this catches most performance problems before they turn into outages.
It is also exactly the kind of weekly check our DBAs run for clients.