How to Check Query Wait Statistics in SQL Server’s Query Store (T-SQL)

SQL Server’s Query Store can capture wait statistics that show what queries are waiting on during execution. This includes things like locks, I/O, CPU, memory, and other resources. This can help you understand not just that a query is slow, but why it’s slow. Wait statistics are available in SQL Server 2017 and later.

Finding the Query ID

To get the wait statistics of a given query, you’ll need the query ID. You can use the following query to get that:

SELECT 
    q.query_id,
    qt.query_sql_text,
    q.last_execution_time
FROM sys.query_store_query q
JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
WHERE qt.query_sql_text LIKE '%your_table_name%'
ORDER BY q.last_execution_time DESC;

In this example I used WHERE qt.query_sql_text LIKE '%your_table_name%', but you’ll need to replace your_table_name with text or a table name that you know is in the query. You’ll need to adjust the filter if it returns too many results.

Once you identify the right query, you can use its query_id in the following queries.

Viewing Wait Statistics

Once you’ve got the query ID, you can use the following query to view wait statistics for that query:

DECLARE @query_id INT = 123;  -- Replace with your query_id

SELECT 
    ws.wait_category_desc,
    SUM(ws.total_query_wait_time_ms) AS total_wait_time_ms,
    AVG(ws.avg_query_wait_time_ms) AS avg_wait_time_ms,
    MAX(ws.max_query_wait_time_ms) AS max_wait_time_ms
FROM sys.query_store_plan p
JOIN sys.query_store_wait_stats ws ON p.plan_id = ws.plan_id
WHERE p.query_id = @query_id
GROUP BY ws.wait_category_desc
ORDER BY total_wait_time_ms DESC;

This shows which wait categories are consuming the most time for the query, helping you identify the primary bottleneck.

Here’s an example of output from this query:

wait_category_desc  total_wait_time_ms  avg_wait_time_ms  max_wait_time_ms
------------------ ------------------ ---------------- ----------------
Parallelism 643 160.75 310
Preemptive 6 1.2 2
CPU 3 1.5 2
Memory 1 1 1

Finding Queries with the Most Wait Time

To identify which queries have the highest total wait time across your database:

SELECT TOP 20
    q.query_id,
    LEFT(qt.query_sql_text, 100) + '...' AS query_preview,
    ws.wait_category_desc,
    SUM(ws.total_query_wait_time_ms) AS total_wait_ms
FROM sys.query_store_query q
JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
JOIN sys.query_store_plan p ON q.query_id = p.query_id
JOIN sys.query_store_wait_stats ws ON p.plan_id = ws.plan_id
GROUP BY q.query_id, qt.query_sql_text, ws.wait_category_desc
ORDER BY total_wait_ms DESC;

This helps you prioritize which queries to investigate based on their overall wait time impact.

Viewing Wait Statistics Over Time

To see how wait patterns change over time for a query:

DECLARE @query_id INT = 123;

SELECT 
    rsi.start_time,
    rsi.end_time,
    ws.wait_category_desc,
    SUM(ws.total_query_wait_time_ms) AS total_wait_ms
FROM sys.query_store_plan p
JOIN sys.query_store_wait_stats ws ON p.plan_id = ws.plan_id
JOIN sys.query_store_runtime_stats_interval rsi ON ws.runtime_stats_interval_id = rsi.runtime_stats_interval_id
WHERE p.query_id = @query_id
GROUP BY rsi.start_time, rsi.end_time, ws.wait_category_desc
ORDER BY rsi.start_time, total_wait_ms DESC;

This shows wait statistics broken down by Query Store’s time intervals, helping you identify when specific wait types became problematic.

Comparing Wait Statistics Across Plans

When a query has multiple plans, you can compare their wait characteristics:

DECLARE @query_id INT = 123;

SELECT 
    p.plan_id,
    ws.wait_category_desc,
    SUM(ws.total_query_wait_time_ms) AS total_wait_ms,
    AVG(ws.avg_query_wait_time_ms) AS avg_wait_ms
FROM sys.query_store_plan p
JOIN sys.query_store_wait_stats ws ON p.plan_id = ws.plan_id
WHERE p.query_id = @query_id
GROUP BY p.plan_id, ws.wait_category_desc
ORDER BY p.plan_id, total_wait_ms DESC;

Different plans may exhibit different wait patterns. A plan with high Lock waits might perform better if changed to one with more CPU waits, indicating less blocking.

Here’s an example of output from this query:

plan_id  wait_category_desc  total_wait_ms  avg_wait_ms
------- ------------------ ------------- -----------
29 CPU 1 1
31 Parallelism 310 310
31 Memory 1 1
31 Preemptive 1 1
33 Parallelism 117 117
33 Preemptive 1 1
34 Parallelism 120 120
34 Preemptive 2 2
35 CPU 2 2
35 Preemptive 1 1
36 Parallelism 96 96
36 Preemptive 1 1

Enabling Wait Statistics Capture

Wait statistics capture is controlled by the WAIT_STATS_CAPTURE_MODE setting. To verify it’s enabled:

SELECT wait_stats_capture_mode_desc
FROM sys.database_query_store_options;

If it shows OFF, you can enable it by running this:

ALTER DATABASE YourDatabaseName
SET QUERY_STORE (WAIT_STATS_CAPTURE_MODE = ON);

Note that wait statistics capture adds slight overhead compared to basic Query Store tracking, but the insights are usually worth it for troubleshooting performance issues.

Filtering by Specific Wait Types

You can focus on queries with specific wait types. For example, to find queries with significant lock waits:

SELECT TOP 20
    q.query_id,
    LEFT(qt.query_sql_text, 100) + '...' AS query_preview,
    SUM(ws.total_query_wait_time_ms) AS total_lock_wait_ms
FROM sys.query_store_query q
JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
JOIN sys.query_store_plan p ON q.query_id = p.query_id
JOIN sys.query_store_wait_stats ws ON p.plan_id = ws.plan_id
WHERE ws.wait_category_desc = 'Lock'
GROUP BY q.query_id, qt.query_sql_text
ORDER BY total_lock_wait_ms DESC;

Replace 'Lock' with other wait categories to investigate different types of bottlenecks.

Understanding Wait Categories

Here are the categories that SQL Server groups waits into:

Wait Type CategoryDescription
UnknownWait type doesn’t fit into other categories
CPUQuery is waiting for CPU time to become available
Worker ThreadQuery is waiting for a worker thread to become available
LockQuery is blocked by locks held by other transactions
LatchQuery is waiting on internal SQL Server synchronization objects
Buffer LatchQuery is waiting to access pages in the buffer pool
Buffer IOQuery is waiting for data pages to be read from disk into memory
CompilationQuery is waiting during query compilation or recompilation
SQL CLRQuery is waiting on CLR operations
MirroringQuery is waiting on database mirroring operations
TransactionQuery is waiting on transaction-related operations
IdleQuery session is idle
PreemptiveQuery is waiting on preemptive tasks
Service BrokerQuery is waiting on Service Broker operations
Tran Log IOQuery is waiting for transaction log writes
Network IOQuery is waiting to send results to the client
ParallelismQuery is waiting on parallel query coordination
MemoryQuery is waiting for memory grants
User WaitQuery is waiting on user-defined waits
TracingQuery is waiting on tracing operations
Full Text SearchQuery is waiting on full-text search operations
Other Disk IOQuery is waiting on disk I/O not covered by other categories
ReplicationQuery is waiting on replication operations
Log Rate GovernorQuery is waiting due to log rate governance (Azure SQL Database)

High waits in specific categories point to specific types of issues. For example, high Lock waits suggest blocking problems, while high Buffer IO waits indicate disk I/O bottlenecks.