Measure Disk I/O Performance per Database File
I/O latency is where disk bottlenecks hide. SQL Server accumulates every millisecond of file-level read and write wait in sys.dm_io_virtual_file_stats — one row per database file, reset at each service restart. Joining that DMV to sys.master_files replaces raw file IDs with physical paths and file types, and dividing accumulated stall by operation count converts totals into the average milliseconds per read and per write that a storage or DBA team can act on.
Purpose and Overview
Pinpointing which files carry the most I/O wait is the starting point for any storage performance investigation in SQL Server. When a query stalls waiting for a disk read or write to complete, that wait is recorded against the specific database file where the I/O occurred. Over time those per-file totals expose patterns: a log file with disproportionate write stall points to a VLF or disk-subsystem issue; a data file with high read latency might indicate missing indexes forcing physical reads, or an underpowered storage tier.
sys.dm_io_virtual_file_stats is the DMV that surfaces these per-file I/O statistics. It is a table-valued function accepting a database ID and file ID — passing NULL, NULL for both returns every file across every online database on the instance. The key columns are io_stall_read_ms and io_stall_write_ms, which accumulate the total milliseconds SQL Server waited on reads and writes respectively, paired with num_of_reads and num_of_writes to calculate averages. The io_stall column sums all wait types across both reads and writes, including queued I/O when Resource Governor I/O limits are in effect.
The DMV joins to sys.master_files, a server-scope catalog view that stores one row per database file across the entire instance — data files, log files, and FILESTREAM containers. The join resolves database_id and file_id to the physical file path, logical name, and file type. Together, the two objects produce a report that identifies exactly where disk latency is accumulating and how much throughput each file has handled since the last SQL Server service restart.
Code Breakdown
The query below produces a per-file I/O performance report, ordered by total I/O stall descending so the worst-offending files surface first.
1SELECT
2 DB_NAME(fs.database_id) AS database_name,
3 mf.name AS logical_name,
4 mf.physical_name,
5 mf.type_desc AS file_type,
6 fs.num_of_reads,
7 fs.io_stall_read_ms,
8 CASE fs.num_of_reads
9 WHEN 0 THEN 0
10 ELSE fs.io_stall_read_ms / fs.num_of_reads
11 END AS avg_read_ms,
12 fs.num_of_writes,
13 fs.io_stall_write_ms,
14 CASE fs.num_of_writes
15 WHEN 0 THEN 0
16 ELSE fs.io_stall_write_ms / fs.num_of_writes
17 END AS avg_write_ms,
18 fs.io_stall AS total_io_stall_ms,
19 CAST(fs.num_of_bytes_read / 1048576.0 AS DECIMAL(18,2)) AS mb_read,
20 CAST(fs.num_of_bytes_written / 1048576.0 AS DECIMAL(18,2)) AS mb_written
21FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS fs
22INNER JOIN sys.master_files AS mf
23 ON fs.database_id = mf.database_id
24 AND fs.file_id = mf.file_id
25ORDER BY fs.io_stall DESC;
Calling sys.dm_io_virtual_file_stats
sys.dm_io_virtual_file_stats is a table-valued function, not a view, so it requires explicit parentheses and arguments. Passing NULL for both database_id and file_id returns every online database file on the instance. To scope the result to one database, pass the integer database ID — for example, sys.dm_io_virtual_file_stats(DB_ID(N'AdventureWorks'), NULL) — which limits output to the files of that database alone. This targeted form is useful when investigating a specific workload rather than surveying all databases at once.
Calculating average read and write latency
The raw io_stall_read_ms and io_stall_write_ms columns are cumulative totals since instance start. Dividing by the matching operation count gives average milliseconds per read or write — the metric that maps directly to storage-tier expectations. A conventional guideline is that average read or write latency above 20–25 ms on a data file signals a storage problem worth investigating; log files are more sensitive because writes are synchronous and sequential, and latency above 10 ms there can stall transactions directly. The CASE ... WHEN 0 THEN 0 guard eliminates a division-by-zero error on files that have seen no reads or writes since the last restart.
Joining to sys.master_files
sys.master_files is a server-scope catalog view — it is not database-scoped, so it does not require a USE statement and can be queried from any database context. It stores the physical path, logical name, and type_desc (ROWS, LOG, or FILESTREAM) for every file across every database on the instance. The join key is the composite (database_id, file_id), which uniquely identifies each file instance-wide. Including physical_name in the output shows the full path on the storage controller, which is essential when a server has multiple drives or mount points and the DBA needs to know which physical spindle or LUN the hot file resides on.
I/O throughput columns
num_of_bytes_read and num_of_bytes_written are cumulative byte counts since instance restart. Converting to megabytes (/ 1048576.0) yields a readable throughput figure that pairs with the latency columns: a file with very high bytes transferred and low stall is working efficiently; high stall relative to throughput signals an I/O subsystem that cannot keep pace with demand. Casting to DECIMAL(18,2) keeps the output readable without excessive decimal places.
Filtering and narrowing results
The ORDER BY fs.io_stall DESC surfaces the worst-offending files first. On a busy production instance with dozens of databases it can help to add WHERE fs.io_stall > 0 to exclude files with no activity, or to filter on mf.type_desc = 'LOG' to examine only transaction logs — the usual first suspects during heavy write workloads. To isolate a single database, add AND DB_NAME(fs.database_id) = N'YourDatabase' rather than changing the function arguments, which keeps the query readable.
Key Benefits and Use Cases
- Identifies high-latency files — average read and write milliseconds per file point directly to storage tiers or drives that are under-provisioned or overloaded.
- Separates read from write pressure —
io_stall_read_msvs.io_stall_write_msdistinguishes a read-heavy query workload from a write-heavy one, guiding whether to address missing indexes, increase buffer pool memory, or improve write throughput. - Compares data and log files in one result —
type_desclets you split I/O performance between data files and transaction logs without running separate queries. - Throughput baseline — cumulative bytes read and written establish a per-file throughput baseline useful before and after storage migrations or hardware upgrades.
- Instance-wide or scoped — the
NULL, NULLargument covers the full instance in one pass; a targeted database ID narrows the scope for focused investigations. - No configuration required — the DMV is available across all SQL Server editions and requires no special configuration or trace flags to populate.
Performance Considerations
- Counters reset on service restart: all values in
sys.dm_io_virtual_file_statsaccumulate from the last SQL Server service restart, not from a rolling window. A server running for 30 days carries 30 days of totals; a recently restarted instance shows only a short history. Capture snapshots at two points in time and compute the delta to get I/O rates for a specific interval. - Buffer pool reads are not physical reads: reads satisfied from the buffer pool do not register as
num_of_reads— only physical disk reads do. Highnum_of_readson a data file is therefore a direct signal that the working set is not cached, pointing toward a memory-pressure investigation alongside storage. - Indirect checkpoint inflates data-file write stall: on databases with indirect checkpoint enabled, the checkpoint process writes dirty pages more aggressively. Elevated
io_stall_write_mson data files may reflect checkpoint activity rather than query writes; cross-reference with checkpoint wait stats before attributing write stall solely to the storage tier. - Resource Governor queue time is included in io_stall: if Resource Governor is configured with
MAX_IOPS_PER_VOLUME, queued I/O time accumulates inio_stall_queued_read_msandio_stall_queued_write_ms, and both roll up intoio_stall. Unusually high stall on a governed workload may reflect throttling policy, not a hardware bottleneck. - Filegroups with multiple files appear as multiple rows: the query reports per-file; a filegroup containing several files returns one row per file. Sum the
io_stalland throughput columns grouped by database andtype_descto get a filegroup-level view.
Practical Tips
- Capture two snapshots several minutes apart and compute the delta to derive current I/O rates rather than lifetime totals; store results in a DBA monitoring table to build a trend baseline over time.
- Schedule the query as a SQL Server Agent job that writes output to a history table nightly — comparing this week's
avg_read_msandavg_write_msagainst last week's baseline catches gradual storage degradation before users report slowness. - Correlate high-stall files with
sys.dm_exec_query_statsto find the specific queries driving the I/O; a file with sustained high write stall often traces back to a small number of heavy-insert or batch-update statements. - Use
physical_nameto confirm that data and log files for each database reside on separate drives or LUNs — sharing a spindle between data and log is a common misconfiguration this report surfaces immediately. - For log files showing high
io_stall_write_ms, pair this report with a VLF count query: excessive VLF fragmentation compounds log write latency, and a shrink-and-grow cycle to right-size the log can cut write stall significantly on its own.
Conclusion
sys.dm_io_virtual_file_stats joined to sys.master_files delivers a per-file view of every read stall, write stall, and byte of I/O on the instance since the last service restart. The average latency columns — computed by dividing accumulated stall by operation count — translate raw DMV numbers into actionable storage-tier metrics. This query is the right first diagnostic when slow queries or elevated wait stats point toward disk, and it belongs in any SQL Server monitoring toolkit alongside wait-statistics and memory-pressure checks.
References
- sys.dm_io_virtual_file_stats (Transact-SQL) — Microsoft Learn — Full column reference including
io_stall,io_stall_read_ms,num_of_reads, and the queued-I/O columns added for Resource Governor workloads. - sys.master_files (Transact-SQL) — Microsoft Learn — Server-scope catalog view exposing
physical_name,type_desc, and the(database_id, file_id)composite join key used in this query. - sqlserver-kit on GitHub by Konstantin Taranov — Community collection of SQL Server DBA scripts including per-file I/O statistics queries and other performance-monitoring patterns.
Posts in this series
- SQL Server UPDATE STATISTICS and SELECT Dynamic Scripts
- SQL Server Wait Statistics Report: dm_os_wait_stats
- SQL Server Missing Indexes Report: dm_db_missing_index
- SQL Server Unused Indexes: sys.dm_db_index_usage_stats
- SQL Server Top Queries by CPU and IO: dm_exec_query_stats
- SQL Server CPU Utilization History Report
- SQL Server Identify Heap Tables Without Clustered Indexes
- SQL Server DBCC FREEPROCCACHE: Clear the Plan Cache Safely
- Measure Disk I/O Performance per Database File