Quick Scan Report – Slow Disk Reads
What this check looks for
Average read stall per drive, from sys.dm_io_virtual_file_stats(NULL, NULL) joined to sys.master_files, grouped by the first character of the physical file path. The message reports the average in milliseconds and the drive letter.
The figure is an average since the instance started. On a long running instance a recent problem is diluted; on a recently restarted one there may not be enough operations to mean much yet.
Why it matters
Slow reads mean every query that touches disk is slower, and the buffer pool being too small makes it worse in a way that looks like a storage problem.
Two things produce this finding and they need separating:
- The storage is genuinely slow. The path from SQL Server to the platter has a problem, or the storage is simply not fast enough for the workload.
- SQL Server is reading more than it should. A missing index turning a seek into a scan, or a buffer pool too small to hold the working set, means far more physical reads. The disk is performing normally and is being asked to do too much.
The second is more common than people expect, and the fix is in the database rather than in the storage.
Rules of thumb for read latency, per operation:
| Average read | Reading |
|---|---|
| Under 5 ms | Good. |
| 5 to 10 ms | Acceptable for data files. |
| 10 to 20 ms | Worth investigating. |
| Over 20 ms | A problem. |
| Over 50 ms | Something is wrong. |
What SQL Server measures is not what the array measures. dm_io_virtual_file_stats records the time from the engine issuing the I/O to it completing, which includes the HBA, the switch fabric, the multipath driver, the file system filter stack and any antivirus product sitting in it. A storage team reporting sub-millisecond latency at the array and SQL Server reporting 30 ms are frequently both correct, and the difference is the finding.
How to confirm it yourself
Per drive, which matches what the check reports:
SELECT LEFT(mf.[physical_name], 1) AS [drive],
SUM(vfs.[num_of_reads]) AS [reads],
CAST(SUM(vfs.[io_stall_read_ms]) * 1.0 / NULLIF(SUM(vfs.[num_of_reads]), 0) AS DECIMAL(10,1)) AS [avg_read_ms],
SUM(vfs.[num_of_writes]) AS [writes],
CAST(SUM(vfs.[io_stall_write_ms]) * 1.0 / NULLIF(SUM(vfs.[num_of_writes]), 0) AS DECIMAL(10,1)) AS [avg_write_ms],
CAST(SUM(vfs.[num_of_bytes_read]) / 1073741824.0 AS DECIMAL(12,1)) AS [gb_read]
FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS vfs
INNER JOIN sys.master_files AS mf WITH (NOLOCK)
ON mf.[database_id] = vfs.[database_id] AND mf.[file_id] = vfs.[file_id]
GROUP BY LEFT(mf.[physical_name], 1)
ORDER BY [avg_read_ms] DESC;
Per file, to find which database is driving it:
SELECT DB_NAME(vfs.[database_id]) AS [database_name],
mf.[name], mf.[type_desc],
CAST(vfs.[io_stall_read_ms] * 1.0 / NULLIF(vfs.[num_of_reads], 0) AS DECIMAL(10,1)) AS [avg_read_ms],
vfs.[num_of_reads],
mf.[physical_name]
FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS vfs
INNER JOIN sys.master_files AS mf WITH (NOLOCK)
ON mf.[database_id] = vfs.[database_id] AND mf.[file_id] = vfs.[file_id]
WHERE vfs.[num_of_reads] > 1000
ORDER BY [avg_read_ms] DESC;
Whether it is actually hurting, from the waits:
SELECT [wait_type], [waiting_tasks_count],
[wait_time_ms] / 1000 AS [wait_seconds],
[wait_time_ms] / NULLIF([waiting_tasks_count], 0) AS [avg_ms]
FROM sys.dm_os_wait_stats WITH (NOLOCK)
WHERE [wait_type] LIKE 'PAGEIOLATCH%'
ORDER BY [wait_time_ms] DESC;
And whether memory is the real cause:
SELECT [object_name], [counter_name], [cntr_value]
FROM sys.dm_os_performance_counters WITH (NOLOCK)
WHERE [counter_name] IN ('Page life expectancy', 'Buffer cache hit ratio')
AND [object_name] LIKE '%Buffer Manager%';
A low page life expectancy alongside high PAGEIOLATCH_SH means pages are being evicted and read back, so the disk is busy because memory is short. Adding memory or fixing a query that scans a large table will do more than faster storage.
How to fix it
Work out which of the two you have before spending anything.
If SQL Server is reading too much:
- Look at the top queries by reads. The CPU by Query and Page Reads by Query reports rank them. One scan of a large table repeated frequently is a common single cause.
- Check for missing indexes on the tables being scanned.
- Check
max server memory. An instance with a small buffer pool re-reads the same pages endlessly. That has its own check. - Check for spills, which generate tempdb I/O from bad estimates.
If the storage is genuinely slow:
- Give the storage team SQL Server’s numbers, per file and per drive, with the timestamps. The difference between their measurement and this one is the diagnostic.
- Check antivirus exclusions.
.mdf,.ndf,.ldfand.bakshould be excluded from real time scanning. This is free and it is regularly the answer. - Check the multipath configuration if this is a SAN, and the queue depth settings on the HBA.
- Separate the workloads. Tempdb, data and log competing on one volume is a common cause, and each has its own placement check on this report.
- Look at instant file initialization. Without it, every data file growth zeroes the new space, which shows up as a stall while it happens.
Measure before and after. DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR) resets the wait statistics so a comparison starts from a known point.
How long it takes
About four hours to separate a workload cause from a storage cause and gather the evidence. Acting on either is a separate piece of work.
Related reports
| Report | Why you would go there |
|---|---|
| I/O by Drive | Latency per volume, with more detail than the check. |
| I/O by Database | Which database is generating the reads. |
| Disk Latency by Hour by Day | Whether it is constant or has a daily shape. |
| Page Reads by Query | The queries doing the reading. |
| Waits | PAGEIOLATCH against everything else. |
| Memory | Whether a small buffer pool is the real cause. |
| Missing Indexes | Scans that could be seeks. |
Related checks
| Check | |
|---|---|
| TempDB file shows slow IO | The same measurement on tempdb, which matters more. |
| Log files showing slow I/O | The same on log files, where it shows as commit time. |
| Max server memory | A small buffer pool producing avoidable reads. |
| Data and log files on the same drive | Workloads competing on one volume. |
| Instant file initialization | Growth events adding stalls. |
Frequently asked questions
Our storage is all flash. Why is this firing? Because the measurement includes everything between the engine and the flash. Antivirus, a filter driver or a saturated path all add latency the array never sees.
The average is skewed by one bad period. Likely, since it is cumulative since startup. Snapshot the DMV twice a few minutes apart and compare deltas for a current reading.
Should I just add memory? If page life expectancy is low, that may genuinely be the cheapest fix, because it removes reads rather than making them faster. Check before buying either.
What about writes? Read latency is what this check reports. The queries above show both, and slow writes on a log file have their own check because they show up as commit time.