sqlwait-review — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sqlwait-review (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Analyze SQL Server wait statistics and identify the dominant bottleneck using the Waits and Queues methodology. Applies 44 checks (V1–V44): V1–V18 classify each significant wait type into its root cause and produce a prioritized remediation plan; V19–V26 perform multi-snapshot trend analysis when 3+ time windows are provided — detecting worsening trends, spikes, peak periods, and emerging bottlenecks; V27–V29 cover specialized scenarios (PAGELATCH on user databases, backup I/O, cumulative skew from outlier events); V30–V36 cover modern feature wait types (In-Memory OLTP, Columnstore, Query Store, Transaction/DTC, Service Broker, Full Text Search, Parallel Redo); V37–V40 add DMV-level memory and I/O detail — forced memory grants, grant timeouts, stolen memory, and file-level I/O latency (requires optional capture queries); V41–V44 cover SQL 2019/2022 IQP/PSP/ADR feature-specific wait types and TempDB memory-optimized metadata contention (SQL 2019+).
The Waits and Queues methodology is based on how SQL Server's thread scheduler works: threads are always in one of three states — RUNNING (on CPU), RUNNABLE (queued for CPU), or SUSPENDED (waiting for a resource). Every time a thread suspends, SQL Server records the wait type and duration. Analyzing the top accumulated waits reveals the dominant bottleneck — not by guessing, but by measuring exactly what the server spent its time waiting for.
Wait analysis answers the question execution plans cannot: why is the server slow when no individual query has a bad plan? The answer is almost always in the wait types — I/O, locks, CPU, memory, or network.
Accept any of:
sys.dm_os_wait_stats capture query below (paste the result grid)sys.dm_exec_requests for current active session waits.txt or .csv file containing either of the aboveRun on the SQL Server instance and paste the results:
-- Wait statistics since last SQL Server restart or DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR)
-- Benign exclusion list based on community wait statistics methodology
SELECT TOP 20
wait_type,
waiting_tasks_count,
wait_time_ms,
max_wait_time_ms,
signal_wait_time_ms,
CAST(100.0 * wait_time_ms
/ NULLIF(SUM(wait_time_ms) OVER (), 0) AS DECIMAL(5,2)) AS pct_total
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
-- Broker / Service Broker
'BROKER_EVENTHANDLER','BROKER_RECEIVE_WAITFOR','BROKER_TASK_STOP',
'BROKER_TO_FLUSH','BROKER_TRANSMITTER',
-- Checkpoint / CLR
'CHECKPOINT_QUEUE','CHKPT','CLR_AUTO_EVENT','CLR_MANUAL_EVENT','CLR_SEMAPHORE',
-- Mirroring / HADR background (idle components only — not HADR_SYNC_COMMIT)
'DBMIRROR_DBM_EVENT','DBMIRROR_DBM_MUTEX','DBMIRROR_EVENTS_QUEUE',
'DBMIRROR_WORKER_QUEUE','DBMIRRORING_CMD',
'HADR_CLUSAPI_CALL','HADR_FABRIC_CALLBACK','HADR_FILESTREAM_IOMGR_IOCOMPLETION',
'HADR_LOGCAPTURE_WAIT','HADR_NOTIFICATION_DEQUEUE','HADR_TIMER_TASK',
'HADR_WORK_QUEUE',
-- Background / dispatcher
'DIRTY_PAGE_POLL','DISPATCHER_QUEUE_SEMAPHORE',
'EXECSYNC','FSAGENT',
'FT_IFTS_SCHEDULER_IDLE_WAIT','FT_IFTSHC_MUTEX',
'KSOURCE_WAKEUP','LAZYWRITER_SLEEP','LOGMGR_QUEUE',
'MEMORY_ALLOCATION_EXT',
'ONDEMAND_TASK_QUEUE',
'PARALLEL_REDO_DRAIN_WORKER','PARALLEL_REDO_LOG_CACHE',
'PARALLEL_REDO_TRAN_LIST','PARALLEL_REDO_WORKER_SYNC',
'PARALLEL_REDO_WORKER_WAIT_WORK','POPULATE_LOCK_ORDINALS',
'PREEMPTIVE_HADR_LEASE_MECHANISM','PREEMPTIVE_OS_FLUSHFILEBUFFERS',
'PREEMPTIVE_SP_SERVER_DIAGNOSTICS','PREEMPTIVE_XE_GETTARGETSTATE',
'PVS_PREALLOCATE',
'PWAIT_ALL_COMPONENTS_INITIALIZED','PWAIT_DIRECTLOGCONSUMER_GETNEXT',
'PWAIT_EXTENSIBILITY_CLEANUP_TASK',
'QDS_ASYNC_QUEUE','QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP',
'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP','QDS_SHUTDOWN_QUEUE',
'REDO_THREAD_PENDING_WORK',
'REQUEST_FOR_DEADLOCK_SEARCH','RESOURCE_QUEUE',
'SERVER_IDLE_CHECK','SLEEP_BPOOL_FLUSH',
'SLEEP_DBSTARTUP','SLEEP_DBTASK','SLEEP_DCOMSTARTUP',
'SLEEP_MASTERDBREADY','SLEEP_MASTERMDREADY','SLEEP_MASTERUPGRADED',
'SLEEP_MSDBSTARTUP','SLEEP_SYSTEMTASK','SLEEP_TASK','SLEEP_TEMPDBSTARTUP',
'SNI_HTTP_ACCEPT','SOS_WORK_DISPATCHER',
'SP_SERVER_DIAGNOSTICS_SLEEP',
'SQLTRACE_BUFFER_FLUSH','SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
'UCS_SESSION_REGISTRATION','VDI_CLIENT_OTHER',
'WAIT_FOR_RESULTS','WAIT_XTP_OFFLINE_CKPT_NEW_LOG',
'WAITFOR','WAITFOR_TASKSHUTDOWN',
'XE_DISPATCHER_WAIT','XE_LIVE_TARGET_TVF','XE_TIMER_EVENT'
)
ORDER BY wait_time_ms DESC;Cumulative waits since restart can be misleading — a busy nightly backup from 2 weeks ago dominates. Capture a differential over 30 minutes instead:
-- Snapshot 1 (run at T0)
-- Note: shorter exclusion list is acceptable here because delta subtraction between identical
-- snapshots cancels out idle waits. For non-differential capture, use the full list above.
SELECT wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count
INTO #waits_before FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN ('SLEEP_TASK','WAITFOR','LAZYWRITER_SLEEP',
'CHECKPOINT_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH','XE_DISPATCHER_WAIT');
WAITFOR DELAY '00:30:00'; -- wait 30 minutes (adjust as needed)
-- Snapshot 2 (run at T30)
SELECT
a.wait_type,
b.wait_time_ms - a.wait_time_ms AS wait_time_ms_delta,
b.signal_wait_time_ms - a.signal_wait_time_ms AS signal_wait_ms_delta,
b.waiting_tasks_count - a.waiting_tasks_count AS tasks_delta,
CAST(100.0 * (b.wait_time_ms - a.wait_time_ms)
/ NULLIF(SUM(b.wait_time_ms - a.wait_time_ms) OVER (), 0)
AS DECIMAL(5,2)) AS pct_of_period
FROM #waits_before a
JOIN sys.dm_os_wait_stats b ON b.wait_type = a.wait_type
WHERE b.wait_time_ms > a.wait_time_ms
ORDER BY wait_time_ms_delta DESC;
DROP TABLE #waits_before;SELECT
r.session_id,
r.wait_type,
r.wait_time / 1000.0 AS wait_sec,
r.blocking_session_id,
r.status,
DB_NAME(r.database_id) AS database_name,
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 current_statement
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.session_id > 50
AND r.session_id <> @@SPID
ORDER BY r.wait_time DESC;Paste this alongside your wait statistics. The skill uses these values to adjust check interpretations — e.g., CXPACKET is interpreted differently based on MAXDOP and Cost Threshold for Parallelism; LCK_M_* changes based on RCSI state.
-- sp_configure values
SELECT name AS config_name, CAST(value_in_use AS INT) AS current_value
FROM sys.configurations
WHERE name IN (
'max degree of parallelism',
'cost threshold for parallelism',
'max server memory (MB)',
'optimize for ad hoc workloads',
'max worker threads',
'xp_cmdshell',
'clr enabled',
'lightweight pooling',
'blocked process threshold (s)',
'query governor cost limit'
);
-- Per-database settings (run for the database under investigation)
SELECT
name AS database_name,
is_read_committed_snapshot_on,
recovery_model_desc,
delayed_durability_desc
FROM sys.databases
WHERE database_id = DB_ID();
-- TempDB file count
SELECT COUNT(*) AS tempdb_data_file_count
FROM sys.master_files
WHERE database_id = 2 AND type = 0;
-- Always On commit mode (if configured)
SELECT ag.name AS ag_name, ar.availability_mode_desc AS commit_mode, ars.role_desc
FROM sys.availability_replicas ar
JOIN sys.availability_groups ag ON ag.group_id = ar.group_id
JOIN sys.dm_hadr_availability_replica_states ars ON ars.replica_id = ar.replica_id
WHERE ars.is_local = 1;If configuration is not provided, the skill still runs all 26 checks and notes where config would change the interpretation.
Trend mode activates automatically when the input contains 3 or more distinct timestamps. Single-snapshot mode (V1–V18) is unchanged when only one time window is present.
Approach A — Staging table with SQL Agent job (recommended for automated capture)
-- Create once per server (or use tempdb.dbo for session-scoped capture)
CREATE TABLE dbo.WaitStatsTrend (
snapshot_time DATETIME2 NOT NULL DEFAULT SYSDATETIME(),
wait_type NVARCHAR(120) NOT NULL,
wait_time_ms BIGINT NOT NULL,
signal_wait_time_ms BIGINT NOT NULL,
waiting_tasks_count BIGINT NOT NULL
);
-- Run every N minutes via SQL Agent job (or execute manually N times)
INSERT INTO dbo.WaitStatsTrend (wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count)
SELECT wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
'SLEEP_TASK','WAITFOR','LAZYWRITER_SLEEP','CHECKPOINT_QUEUE',
'REQUEST_FOR_DEADLOCK_SEARCH','XE_DISPATCHER_WAIT','XE_TIMER_EVENT',
'BROKER_TO_FLUSH','BROKER_TRANSMITTER','SLEEP_DBSTARTUP','SLEEP_DBTASK',
'SLEEP_MASTERDBREADY','SLEEP_MASTERMDREADY','SLEEP_MASTERUPGRADED',
'SLEEP_MSDBSTARTUP','SLEEP_SYSTEMTASK','SLEEP_TEMPDBSTARTUP',
'SNI_HTTP_ACCEPT','SOS_WORK_DISPATCHER','SP_SERVER_DIAGNOSTICS_SLEEP',
'SQLTRACE_BUFFER_FLUSH','SQLTRACE_INCREMENTAL_FLUSH_SLEEP'
);
-- Query for trend analysis — paste result to /sqlwait-review alongside configuration
SELECT
snapshot_time,
wait_type,
wait_time_ms - LAG(wait_time_ms) OVER (PARTITION BY wait_type ORDER BY snapshot_time) AS delta_wait_ms,
signal_wait_time_ms - LAG(signal_wait_time_ms) OVER (PARTITION BY wait_type ORDER BY snapshot_time) AS delta_signal_ms,
waiting_tasks_count - LAG(waiting_tasks_count) OVER (PARTITION BY wait_type ORDER BY snapshot_time) AS delta_tasks
FROM dbo.WaitStatsTrend
WHERE snapshot_time >= DATEADD(HOUR, -2, SYSDATETIME())
ORDER BY snapshot_time, delta_wait_ms DESC;Approach B — Manual multi-run (no staging table)
-- Run every N minutes and paste all result sets together (labeled with a comment for each run)
-- The skill detects multiple timestamp values and activates trend mode automatically
-- Note: shorter exclusion list is acceptable for differential trend mode; delta subtraction
-- between consecutive cumulative snapshots cancels out idle waits. For the full exclusion
-- list, use the staging-table approach (Approach A) above.
SELECT
CONVERT(NVARCHAR(20), SYSDATETIME(), 120) AS snapshot_time,
wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count,
CAST(100.0 * wait_time_ms / NULLIF(SUM(wait_time_ms) OVER(), 0) AS DECIMAL(5,2)) AS pct_total
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
'SLEEP_TASK','WAITFOR','LAZYWRITER_SLEEP','CHECKPOINT_QUEUE',
'REQUEST_FOR_DEADLOCK_SEARCH','XE_DISPATCHER_WAIT','XE_TIMER_EVENT'
)
ORDER BY wait_time_ms DESC;With Approach B, the skill computes per-period deltas by subtracting consecutive cumulative values within each wait_type across snapshots.
Minimum snapshots: 2 periods for V20/V21/V23; 3+ periods for V19/V22/V24/V25/V26 (full trend analysis).
wait_type, wait_time_ms, waiting_tasks_count, signal_wait_time_ms, pct_total.1a. Detect capture window duration — this determines whether absolute ms thresholds and the "In context" metric are computable:
snapshot_time values for each wait_type. Report the median interval in minutes as the period length. If any consecutive pair differs by more than 20% from the median, flag unequal intervals — V21 and V22 must use per-minute normalization in that case.delta_wait_ms and compute pct_of_period = delta_wait_ms / SUM(delta_wait_ms per snapshot) × 100 per time window.delta = value[T] − value[T−1] per wait_type; then compute pct_of_period per window from those deltas.### Passed Checks.Paste these alongside wait stats for richer memory-pressure and file-I/O analysis (enables V37–V40):
Memory grant detail — forced grants and timeouts
SELECT
resource_semaphore_id,
target_memory_kb / 1024.0 / 1024.0 AS target_memory_gb,
max_target_memory_kb / 1024.0 / 1024.0 AS max_target_memory_gb,
total_memory_kb / 1024.0 / 1024.0 AS total_memory_gb,
available_memory_kb / 1024.0 / 1024.0 AS available_memory_gb,
granted_memory_kb / 1024.0 / 1024.0 AS granted_memory_gb,
used_memory_kb / 1024.0 / 1024.0 AS used_memory_gb,
grantee_count,
waiter_count,
forced_grant_count,
timeout_error_count
FROM sys.dm_exec_query_resource_semaphores;Memory clerk breakdown — stolen memory check
SELECT
type,
name,
pages_kb / 1024.0 / 1024.0 AS pages_gb,
virtual_memory_reserved_kb / 1024.0 / 1024.0 AS virtual_gb,
virtual_memory_committed_kb / 1024.0 / 1024.0 AS committed_gb
FROM sys.dm_os_memory_clerks
WHERE pages_kb > 1048576 -- > 1 GB
ORDER BY pages_kb DESC;File I/O latency
SELECT
DB_NAME(database_id) AS database_name,
file_id,
name AS file_name,
type_desc,
num_of_reads,
num_of_writes,
io_stall_read_ms,
io_stall_write_ms,
CAST(io_stall_read_ms / NULLIF(num_of_reads, 0) AS decimal(18, 2)) AS avg_read_latency_ms,
CAST(io_stall_write_ms / NULLIF(num_of_writes, 0) AS decimal(18, 2)) AS avg_write_latency_ms,
size_on_disk_bytes / 1024.0 / 1024.0 / 1024.0 AS size_gb
FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS fs
JOIN sys.master_files AS mf
ON fs.database_id = mf.database_id AND fs.file_id = mf.file_id
ORDER BY io_stall_read_ms + io_stall_write_ms DESC;Important: There are no universal thresholds for wait statistics. Compare against your own system's baseline, not industry averages. A CXPACKET percentage that is normal for a large analytics workload would be alarming on a pure OLTP system. The values below are investigative triggers — always verify with context.
Window dependency: All percentage thresholds are window-independent — they reflect the proportion of wait time and are valid at any capture interval (5 min, 30 min, cumulative). Absolute ms thresholds scale with the window; per-minute rate equivalents are provided where applicable.
| Metric | Investigative threshold |
|---|---|
| PAGEIOLATCH — I/O pressure | ≥ 10% investigate; ≥ 40% critical |
| LCK_M — lock wait | any presence; ≥ 20% critical |
| CXPACKET alone (not CXCONSUMER) | ≥ 15% investigate; ≥ 40% critical |
| CXCONSUMER (SQL 2016 SP2+ / SQL 2017 CU3+) | generally benign — only investigate alongside very high CXPACKET |
| RESOURCE_SEMAPHORE — memory grant queue | any presence > 0 ms |
| RESOURCE_SEMAPHORE — critical | ≥ 5% of total wait time |
| WRITELOG — log I/O | ≥ 10% investigate |
| ASYNC_NETWORK_IO | ≥ 20% — but this is almost always a client-side problem, not SQL Server |
| SOS_SCHEDULER_YIELD | ≥ 15% investigate — requires context; VM environments inflate this |
| Signal wait ratio — CPU saturation | ≥ 15% warning; ≥ 25% critical |
| THREADPOOL — thread exhaustion | any presence = Critical |
| PAGELATCH (TempDB pages 1/2/3) | any presence = Warning |
| LATCH_EX/SH (non-page latches) | ≥ 5% investigate |
| LOGMGR_RESERVE_APPEND | any presence = Critical |
| Single wait type dominance | ≥ 60% = focus all effort on this type |
| Poison waits — window-scaled (V18) | wait_time_ms > 1,000 × window_minutes — e.g., > 5,000 ms for 5-min, > 30,000 ms for 30-min, > 60,000 ms for 60-min. If window unknown, use > 10,000 ms (conservative minimum). Cumulative: threshold formula > 60,000 ms AND > (5,000 × hours_since_startup). |
| "In context" concurrent sessions | total_wait_ms ÷ window_ms; requires known window — report N/A if window is unknown or cumulative |
| Trend — spike (V20) | Single period ≥ 200% of that wait type's own average across all periods |
| Trend — worsening (V19) | Delta % increases monotonically across ≥ 3 consecutive periods |
| Trend — emerging (V23) | < 0.5% in period 1, ≥ 2.0% in any later period |
| Trend — correlated (V24) | 2+ wait types each ≥ 150% of own average in the same period |
| Forced memory grant (V37) | any forced_grant_count > 0 warning; > 10 critical |
| Memory grant timeout (V38) | any timeout_error_count > 0 = Critical |
| Stolen memory (V39) | ≥ 15% of max server memory warning; > 30% critical |
| File I/O latency (V40) | avg read/write latency ≥ 100 ms warning; ≥ 500 ms critical |
PAGEIOLATCH_SH, PAGEIOLATCH_EX, or PAGEIOLATCH_UP present AND combined ≥ 10% of total wait time/sqlstats-review or /sqltrace-review and add covering indexes; (2) Add RAM to expand the buffer pool after addressing query efficiency; (3) Move data files to faster storage (SSD/NVMe) as a tertiary fix; (4) Identify hot tables with sys.dm_os_buffer_descriptors.LCK_M_* wait type present AND combined ≥ 1% of total wait timeLCK_M_IX (Intent Exclusive) — the most worrying lock wait, often caused by lock escalation or schema modification conflicts; LCK_M_RS_*, LCK_M_RIn_*, LCK_M_RX_* — range lock waits that indicate SERIALIZABLE isolation level is in use, holding range locks to prevent phantom reads. Fix options: (1) Use sys.dm_os_waiting_tasks to identify the blocking resource and head blocker; (2) Add indexes on WHERE clause columns to reduce scan-based lock scope; (3) Enable READ_COMMITTED_SNAPSHOT (ALTER DATABASE ... SET READ_COMMITTED_SNAPSHOT ON) to eliminate reader/writer shared lock conflicts; (4) For SERIALIZABLE range locks specifically: switch to SNAPSHOT isolation (ALTER DATABASE ... SET ALLOW_SNAPSHOT_ISOLATION ON; SET TRANSACTION ISOLATION LEVEL SNAPSHOT) — it provides consistent reads without range locks; (5) Use /sqlblock-review for the full blocking chain analysis.LCK_M_S and shared-lock conflicts in a single command; this is the highest-leverage fix and should be the first action. If RCSI is already ON — the remaining LCK_M waits come from explicit writers or lock escalation, which RCSI cannot resolve; focus on reducing scan scope with indexes and shortening transaction duration.CXPACKET ≥ 15% of total wait time. CXCONSUMER alone is generally benign — only investigate if CXPACKET is also elevated. CXSYNC_PORT or CXSYNC_CONSUMER ≥ 5% (SQL 2022+ / Azure SQL only — see version note). HTBUILD, HTDELETE, HTMEMO, HTREINIT, HTREPARTITION (batch-mode hash build/repartition waits) — treat the same as CXPACKET; investigate skew before adjusting MAXDOP.CXCONSUMER was separated out — CXPACKET now represents the producer thread wait and is more actionable. Fix options when CXPACKET is genuinely problematic: (1) Raise Cost Threshold for Parallelism from default 5 to 25–50 — reduces unnecessary parallelism on medium-cost queries; (2) Update statistics — data skew causes uneven thread distribution; (3) Investigate specific queries via sys.dm_exec_requests (not sys.dm_os_waiting_tasks — CXPACKET threads may not appear there); (4) Only reduce MAXDOP after confirming parallelism is hurting, not helping.sys.dm_exec_requests before making any changes. Never reduce MAXDOP as a first response.RESOURCE_SEMAPHORE present with any wait time > 0; RESOURCE_SEMAPHORE_QUERY_COMPILE present with any wait time > 0 AND ≥ 0.5% of total (lower threshold because compile-memory waits are usually small but impactful)OPTION (MIN_GRANT_PERCENT = n) to cap individual grants; (4) Use Resource Governor to limit grant size per workload group; (5) Add RAM. Check /sqlplan-review S2/S3/S4 for the specific queries driving large grants.sp_configure 'optimize for ad hoc workloads', 1; RECONFIGURE) — prevents storing full compiled plans for single-use ad-hoc queries, freeing compile memory; (2) Simplify complex queries — deeply nested views, very long IN lists, or queries referencing hundreds of tables consume disproportionate compile memory; (3) Use OPTION (KEEPFIXED PLAN) on queries that recompile unnecessarily — it suppresses recompilation from statistics changes; (4) If RESOURCE_SEMAPHORE_QUERY_COMPILE is the dominant wait (≥ 2%) while RESOURCE_SEMAPHORE is low, the bottleneck is compile-bound, not data-bound — optimize for ad hoc workloads is the highest-leverage fix.RESOURCE_SEMAPHORE_QUERY_COMPILE is high and optimize for ad hoc workloads is OFF — enabling it is the single most effective fix for compile-memory pressure.WRITELOG or LOGBUFFER ≥ 10% of total wait time combinedWRITELOG — every COMMIT flushes the transaction log synchronously. LOGBUFFER — threads waiting for space in the log buffer before writing; indicates the log buffer is full, often from very high DML rates. Both indicate log I/O pressure. Every COMMIT requires SQL Server to harden the log to disk before returning. Note: on faster storage, WRITELOG waits may increase as higher throughput generates more commits — this is not necessarily a problem, just higher transaction volume. Fix options when WRITELOG is genuinely the bottleneck: (1) Move the transaction log to dedicated fast storage (NVMe with low write latency — the log is sequential write, so IOPS matter less than latency); (2) Separate the log from data files so I/O does not compete; (3) Batch small transactions — reducing commit frequency reduces log flush frequency; (4) Delayed Durability (SQL Server 2014+) — ALTER DATABASE YourDb SET DELAYED_DURABILITY = FORCED batches log flushes; trade-off is potential data loss of the last batch on crash; (5) SQL Server 2012+ raised the per-database limit on outstanding log write I/Os (from 32 to 112 [Unverified]) — ensure you are not on SQL 2008.ALTER DATABASE YourDb SET DELAYED_DURABILITY = ALLOWED, which lets applications opt into batched log flushes for workloads that can tolerate up to ~1 ms of committed-but-not-hardened data on a crash. If Delayed Durability is already FORCED and WRITELOG is still high — the issue is raw log file I/O throughput (too many commits even after batching), not commit frequency; move the log to dedicated faster storage.ASYNC_NETWORK_IO ≥ 20% of total wait timeSET NOCOUNT ON, explicit column lists, pagination. Do not tune SQL Server to fix ASYNC_NETWORK_IO.SOS_SCHEDULER_YIELD ≥ 15% of total wait timeSleep() which is invisible in wait statistics; (2) On virtual machines, this wait is often artificially elevated because the VM clock counter includes hypervisor scheduling delay, making threads appear to burn longer quanta than they actually do. Fix options: (1) Identify the specific queries via sys.dm_exec_requests (threads with this wait are RUNNABLE, not SUSPENDED — they may not appear in sys.dm_os_waiting_tasks); (2) Add indexes to eliminate in-memory scans; (3) If running in a VM, check host oversubscription before assuming a SQL Server problem.THREADPOOL present with any wait timeKILL spid); (2) Increase max worker threads (sp_configure) — but investigate root cause first; (3) Root causes: many long-running blocking chains consuming threads, many parallel queries consuming multiple threads each (reduce MAXDOP), application creating too many connections (use connection pooling). Investigate with sys.dm_os_workers and sys.dm_exec_sessions.PAGELATCH_EX or PAGELATCH_SH present, especially on database ID 2 (TempDB) pages 1, 2, or 3 (PFS, GAM, SGAM allocation pages)ALTER DATABASE ... SET MIXED_PAGE_ALLOCATION OFF controls this); (3) Use table variables instead of temp tables for small, single-row data sets; (4) Avoid dropping and recreating temp tables in loops.min(logical CPU count, 8). If files < target — adding the missing files is the direct fix (this is the most common TempDB contention remedy). If already at 8 files and PAGELATCH persists — verify all files are equal size; SQL Server uses proportional fill, so a larger file receives more allocations and re-centralises contention. Also confirm Trace Flag 1118 / Mixed Extent Allocations is set correctly for the SQL Server version.signal_wait_time_ms / wait_time_ms across all wait types ≥ 15%OLEDB ≥ 5% of total wait time — but duration matters: short waits may be benign/sqltrace-review; (2) Replicate remote data locally and query locally; (3) Use OPENQUERY to push filters to the remote server; (4) Reduce monitoring poll frequency if monitoring tools are the cause.HADR_*, PWAIT_HADR_*, or DBMIRROR_* wait type ≥ 5% of total wait timeHADR_SYNC_COMMIT is the primary synchronous-commit latency wait — if this type dominates HADR waits, the secondary log I/O or network is the direct bottleneck. Fix options: (1) Switch non-critical databases to asynchronous commit mode; (2) Investigate network latency between primary and secondary; (3) Move secondary replicas to faster storage for log writes; (4) Use sys.dm_hadr_database_replica_states to identify the lagging secondary.SELECT availability_mode_desc FROM sys.availability_replicas.PREEMPTIVE_* wait type ≥ 10% of total wait timePREEMPTIVE_OS_WRITEFILEGATHERER is prominent alongside V5 (WRITELOG), check for frequent autogrowth events — query sys.dm_os_performance_counters for the Log Growths counter per database, or review the default trace for autogrowth events. Autogrowth is a common trigger of PREEMPTIVE_OS_WRITEFILEGATHERER + WRITELOG co-occurrence.LATCH_EX or LATCH_SH ≥ 5% of total wait time. Distinguish from PAGELATCH (V9): PAGELATCH protects in-memory data pages; LATCH_EX/SH protects internal SQL Server non-page data structures.sys.dm_os_latch_stats to identify the specific latch class: SELECT * FROM sys.dm_os_latch_stats WHERE latch_class NOT IN ('BUFFER','ACCESS_METHODS_HOBT_COUNT') ORDER BY wait_time_ms DESC; (2) Common latch classes and fixes: ACCESS_METHODS_DATASET_PARENT / ACCESS_METHODS_SCAN_RANGE_GENERATOR — parallel scan contention, often co-occurs with CXPACKET; LOG_MANAGER — transaction log growth contention (pre-size the log); TRACE_CONTROLLER — SQL Trace is enabled and generating excessive overhead (switch to Extended Events); FGCB_ADD_REMOVE — file auto-growth is triggering (pre-size data files); DATABASE_MIRRORING_CONNECTION — mirroring message throughput (check network).LOGMGR_RESERVE_APPEND present with any wait timeDBCC SQLPERF('LOGSPACE') and SELECT log_reuse_wait_desc FROM sys.databases; (2) If SIMPLE recovery: the log cannot be backed up — it only frees space via checkpoint. A long-running active transaction may be preventing checkpoint from truncating the log. (3) Fix: increase log autogrowth size, or switch to FULL recovery with regular log backups so space is regularly reclaimed; (4) Never set autogrowth to 0 — that prevents the log from growing at all.BACKUP LOG). SIMPLE recovery — log space is freed only by automatic checkpoint; if a long-running transaction is open it prevents checkpoint from advancing the log's minimum LSN; find and kill it via sys.dm_tran_active_transactions. BULK_LOGGED recovery — bulk operations hold log space until the next log backup; take a log backup immediately or temporarily switch to SIMPLE if BULK_LOGGED is not required.wait_time_ms > 1,000 × window_minutes (e.g., > 5,000 ms for 5-min window, > 30,000 ms for 30-min window, > 60,000 ms for 60-min window). If window is unknown, use > 10,000 ms as conservative minimum. For cumulative-since-restart data, use the proportional formula: wait_time_ms > 60,000 AND wait_time_ms > (5,000 × hours_since_startup). Universal wait types: IO_RETRY, RESMGR_THROTTLED. SQL 2016+ wait types (skip on pre-2016): IO_QUEUE_LIMIT, HADR_THROTTLE_LOG_RATE_GOVERNOR, LOG_RATE_GOVERNOR, POOL_LOG_RATE_GOVERNOR, INSTANCE_LOG_RATE_GOVERNOR (log-rate governor waits are primarily observable in Azure SQL Database / Managed Instance). Azure SQL Database–specific (per sys.dm_db_wait_stats): SE_REPL_CATCHUP_THROTTLE, SE_REPL_COMMIT_ACK, SE_REPL_COMMIT_TURN, SE_REPL_ROLLBACK_ACK, SE_REPL_SLOW_SECONDARY_THROTTLEIO_QUEUE_LIMIT — the I/O subsystem queue is full; SQL Server is generating more I/O than the storage can accept. Emergency: check disk throughput, reduce I/O via indexes, add faster storage.IO_RETRY — a SQL Server I/O operation failed and is being retried. Indicates hardware or driver errors. Check Windows Event Log and SQL Server error log immediately.RESMGR_THROTTLED — Resource Governor is actively throttling a workload group's CPU. Review Resource Governor pool configuration; the pool's MAX_CPU_PERCENT may be set too low.LOG_RATE_GOVERNOR / POOL_LOG_RATE_GOVERNOR / INSTANCE_LOG_RATE_GOVERNOR (SQL Server 2016+; primarily observable in Azure SQL Database / Managed Instance where log generation rate is tier-enforced; on-premises these are internal-use waits) — transaction log generation rate is being actively throttled. Reduce write volume, optimize large DML, check secondary replica health and tier limits.HADR_THROTTLE_LOG_RATE_GOVERNOR — log rate throttled specifically because an Always On secondary replica is lagging. Investigate secondary replica I/O and network latency.SE_REPL_* (Azure SQL Database, per sys.dm_db_wait_stats) — secondary replica replication throttle. The primary is generating logs faster than the secondary can apply them. Check sys.dm_hadr_database_replica_states for redo_queue_size and redo_rate on the lagging secondary.These checks activate only when the input contains 3 or more distinct time windows (2 for V20/V21/V23). They operate on the per-period delta series derived from multi-snapshot input. V18 (poison waits) is re-evaluated in each period independently.
delta_wait_ms per period directly. When intervals are unequal (> 20% variance): normalize to wait_ms per minute = delta_wait_ms ÷ period_minutes before comparing — a 30-minute period will naturally accumulate more ms than a 5-minute period at the same load, and raw comparison would always favour the longer window. Report: the timestamp range, the total accumulated wait, the per-minute rate, and how much worse it was vs the average period."+N% per P-minute period" (e.g., "PAGEIOLATCH_SH +4.3% per 15-min period"). When intervals are unequal, report the per-minute rate instead: "+0.29 pp/min". Velocity identifies which bottleneck is accelerating fastest. A wait type at 10% growing 5 pp/period will overtake a static 30% type in 4 periods. Report the top 3 with their rate, trend direction, and the corresponding V1–V18 check for root cause./sqltrace-review trace around the same time to identify the specific query responsible.Consistently degrading (V19 worsening for dominant wait type), Single spike then recovery (V20 + V25 for same wait type), Steadily elevated (all periods above baseline, no clear trend), Multi-spike (V20 fires for 2+ non-overlapping periods), Improving (V19 improving for dominant wait type), Multi-bottleneck (2+ wait types both worsening). Report which wait types drive the pattern and what root cause the pattern implies.These checks complement V1–V26 for both single-snapshot and trend mode.
PAGELATCH_EX or PAGELATCH_SH present on a database that is NOT TempDB (database_id ≠ 2); combined ≥ 2% of total wait time. Distinguish from V9 (TempDB allocation contention on pages 1/2/3) — V9 addresses PFS/GAM/SGAM contention across sessions creating temp objects; V27 addresses latch contention on user database data pages.NEWSEQUENTIALID()). All INSERT operations target the same last page, contending for the exclusive page latch. Secondary cause: page splits when inserting into full pages — the split operation holds the latch longer. Fix options ranked: (1) For last-page contention on IDENTITY keys: use OPTIMIZE_FOR_SEQUENTIAL_KEY = ON (SQL Server 2019+) — an index-level option that improves last-page insertion throughput without redesigning the key; (2) For IDENTITY-based clustered indexes: consider a different clustered key (non-sequential GUID, business key) to spread inserts across pages — trade-off is index fragmentation; (3) Use SEQUENCE with a cache size (CACHE 1000) instead of IDENTITY — reduces metadata contention but not page-level; (4) Reduce fill factor (e.g., FILLFACTOR = 80) on insert-heavy indexes — leaves free space per page to delay page splits; (5) Hash-partition the inserting table via PARTITION BY RANGE on a computed hash column to spread inserts across multiple partitions (and therefore multiple B-tree last pages). Verify by querying sys.dm_os_waiting_tasks where resource_description indicates the specific page.BACKUPIO or BACKUPBUFFER combined ≥ 5% of total wait timeBACKUPIO is the I/O wait for reading database pages into backup buffers; BACKUPBUFFER is the wait for backup buffer space to become available (the backup is generating buffers faster than the backup device can consume them). Unlike most wait types, these are expected during backup windows. Fix options when backups impact production: (1) Schedule backups during low-activity periods (off-peak hours) so these waits don't compete with user queries; (2) Use backup compression — reduces backup size, I/O volume, and buffer consumption (WITH COMPRESSION in BACKUP DATABASE); (3) Use backup striping — write to multiple backup files/devices in parallel (TO DISK = 'file1.bak',..., 'fileN.bak' with MAXTRANSFERSIZE tuned); (4) For BACKUPBUFFER specifically: increase BUFFERCOUNT in BACKUP DATABASE to allocate more buffers, reducing buffer-full contention; (5) Move backups to faster backup media (faster disk or dedicated backup network). If BACKUPIO consistently appears outside backup windows, check for rogue backup processes or verify backup jobs complete within their scheduled window.waiting_tasks_count > 0, compute avg_wait_ms = wait_time_ms / waiting_tasks_count. If max_wait_time_ms > 100 × avg_wait_ms, flag the wait type as "skewed by outliers."PAGEIOLATCH_SH from a nightly DBCC, vs average 50 ms per task) can dominate cumulative wait totals, giving a false impression of chronic I/O problems. This check identifies when a small number of outlier events disproportionately inflate a wait type's total — the max_wait_time_ms is so large relative to the average that the total is unreliable without investigating the outliers. Action: (1) Note the specific wait type, its max_wait_time_ms, and waiting_tasks_count in the report; (2) If using cumulative data, re-capture a differential snapshot to get a window that excludes the outlier; (3) Identify the outlier event — correlate the high max_wait_time_ms with known maintenance windows (index rebuilds, DBCC CHECKDB, large bulk operations, nightly ETL); (4) If the outlier is a recurring maintenance operation, document it for the baseline and exclude it when evaluating query performance — the wait is real but not actionable for query tuning. In trend mode (V19–V26), this check is less relevant because per-period deltas automatically isolate the outlier to a single window via V20 and V25. In single-snapshot mode it prevents wasted effort tuning a wait type dominated by one-time events.These checks fire when wait types associated with modern SQL Server features are present in the wait statistics. They complement V1–V29 in both single-snapshot and trend mode.
XTP* or WAIT_XTP* wait types present at ≥ 2% of total wait timesys.dm_xtp_transaction_stats and sys.dm_db_xtp_checkpoint_stats; (2) Review tables for off-row columns (LOB/varchar(max) columns stored off-row bypass the in-memory optimized path); (3) Consider natively compiled stored procedures for hot code paths; (4) If WAIT_XTP_CKPT_CLOSE or WAIT_XTP_OFFLINE_CKPT_LOG_IO are prominent, XTP checkpoint I/O is the bottleneck — move XTP checkpoint files to faster storage.COLUMNSTORE* wait types present at ≥ 2% of total wait timesys.dm_db_column_store_row_group_physical_stats — a large number of OPEN or CLOSED delta rowgroups indicates the tuple mover is falling behind; (2) For tuple mover lag: trigger manual compression with ALTER INDEX ... REORGANIZE WITH (COMPRESS_DELAY = 0) or increase tuple mover frequency; (3) If memory grant pressure co-occurs (V4 also fires): add indexes to reduce scan input sizes and update statistics so grant estimates are accurate.QDS* wait types present at ≥ 1% of total wait timeALTER DATABASE [YourDb] SET QUERY_STORE (DATA_FLUSH_INTERVAL_SECONDS = 900) (default 900, increase to 1800–3600); (2) Switch capture mode: ALTER DATABASE [YourDb] SET QUERY_STORE (QUERY_CAPTURE_MODE = AUTO) or CUSTOM with a QUERY_CAPTURE_POLICY that filters low-value queries; (3) Increase MAX_STORAGE_SIZE_MB if the store is near capacity and auto-cleanup is running continuously; (4) If QDS_ASYNC_QUEUE is prominent: the async flush thread is a bottleneck — set QUERY_STORE = OFF temporarily to confirm, then tune retention/flush settings.XACT*, DTC*, TRAN_MARKLATCH_*, MSQL_XACT_*, or TRANSACTION_MUTEX wait types present at ≥ 2% of total wait timeDTC_* waits explicitly indicate MS DTC involvement — cross-server transactions. Fix: (1) Eliminate distributed transactions where possible — consolidate operations onto a single server; (2) If DTC is required: ensure DTC is installed on all participating servers and configured correctly; (3) Identify long-running distributed transactions: SELECT * FROM sys.dm_tran_active_transactions WHERE transaction_type = 2 ORDER BY transaction_begin_time; (4) TRANSACTION_MUTEX or MSQL_XACT_* waits indicate transaction manager internal contention — investigate with sys.dm_tran_locks for the specific transaction ids.BROKER_* wait types (excluding background idle waits filtered from the capture query) at ≥ 3% of total wait timeSELECT name, is_receive_enabled, activation_procedure FROM sys.service_queues; SELECT COUNT(*) FROM sys.transmission_queue; (2) Verify activation procedures are running: SELECT * FROM sys.dm_broker_activated_tasks; (3) Check for poison messages blocking a queue: SELECT * FROM sys.conversation_endpoints WHERE state_desc = 'CONVERSING' — a rollback loop from a failing activation proc blocks the queue; end the conversation or fix the proc; (4) BROKER_WAIT_RESULT waits may indicate dialogs waiting for a reply — check for unmatched request/reply conversation patterns.FT_*, FULLTEXT GATHERER, MSSEARCH, or PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC wait types at ≥ 3% of total wait timeSELECT * FROM sys.dm_fts_index_population; (2) Schedule full populations off-peak and reduce master merge parallelism via sp_fulltext_service 'master_merge_dop', N (note: the older resource_usage action has no function in SQL Server 2008 and later); (3) If PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC is dominant: FT parallel query memory is saturated — consider reducing max full-text crawl range; (4) Evaluate offloading full-text search to a dedicated search engine (Elasticsearch, Azure Cognitive Search) for high-volume workloads.PARALLEL_REDO* wait types present at ≥ 2% of total wait timeSELECT database_name, redo_queue_size, redo_rate FROM sys.dm_hadr_database_replica_states WHERE is_local = 1; (2) Parallel redo threads are allocated automatically — SQL Server 2016–2019 use up to 100 threads instance-wide (databases beyond that limit fall back to single-threaded redo); SQL Server 2022+ assigns redo workers based on workload. Check for single-threaded redo (sys.dm_exec_requests command DB STARTUP with no PARALLEL REDO TASK rows) and for redo blocked by readers (sqlserver.lock_redo_blocked XE, Redo blocked/sec counter). Trace flag 3459 disables parallel redo if serial redo proves faster under contention; (3) Move secondary replica log and data files to faster storage — redo throughput is bounded by the secondary's write I/O; (4) If the primary write workload has recently increased, consider switching less-critical replicas to asynchronous commit mode to eliminate redo lag blocking the primary.These checks require the optional Memory and I/O detail capture queries (see Input section). They complement V1 (PAGEIOLATCH) and V4 (RESOURCE_SEMAPHORE) with DMV-level detail that wait statistics alone cannot provide. Omit these checks if the optional queries were not provided — note "Cannot evaluate — Memory/I/O detail queries not provided."
forced_grant_count > 0 in sys.dm_exec_query_resource_semaphoressys.dm_exec_query_memory_grants and run /sqlplan-review on their plans (focus on S2, S3, S4); (3) Cap individual grants with Resource Governor REQUEST_MAX_MEMORY_GRANT_PERCENT or OPTION (MIN_GRANT_PERCENT = 1); (4) Increase max server memory if the instance is under-provisioned. Note: forced_grant_count is cumulative since server startup — a rapidly growing value signals persistent memory undersizing.timeout_error_count > 0 in sys.dm_exec_query_resource_semaphoresresource_semaphore_id identifies which semaphore is starving: 0 = regular resource semaphore, 1 = small-query resource semaphore (grants < 5 MB and query cost < 3). Fix: (1) If concentrated in one pool, redistribute workload or increase memory; (2) Kill long-running queries holding memory grants (sys.dm_exec_query_memory_grants); (3) Apply the cumulative fixes from V4 and V37 — timeouts mean the problem has escalated past waiting and forced grants. For immediate relief, set query wait (s) via sp_configure to a lower value to fail fast rather than hold connections open.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.