highI/O

PAGEIOLATCH_DT Wait Type Explained

Fix SQL Server PAGEIOLATCH_DT waits caused by buffer pool pressure and slow disk I/O. Diagnostic queries, fix scripts, and prevention strategies included.

Quick Answer

PAGEIOLATCH_DT occurs when SQL Server needs to destroy a page in the buffer pool while that page is still being read from disk. This wait indicates inefficient disk I/O performance or buffer pool pressure causing pages to be flushed prematurely. Extended waits signal serious storage subsystem problems or memory pressure.

Root Cause Analysis

PAGEIOLATCH_DT waits occur during buffer pool page lifecycle management when the buffer manager attempts to destroy a page (DT = Destroy) that is currently subject to an I/O operation. The page latch system protects buffer pool pages during I/O operations, and the Destroy mode latch is the most restrictive latch type.

When SQL Server needs memory for new pages or during checkpoint operations, the lazy writer or checkpoint process attempts to flush dirty pages to disk and reclaim buffer pool space. If a page is selected for destruction while it's being read from storage (typically due to another session requesting that same page), the destroy operation must wait for the I/O completion.

This wait type specifically indicates buffer pool churn where pages are being evicted from memory while simultaneously being requested by other operations. The buffer manager uses a clock algorithm to select victim pages for eviction, but if the working set exceeds available buffer pool memory, recently flushed pages may be immediately re-requested.

SQL Server 2016 introduced changes to buffer pool management, and accelerated database recovery in SQL Server 2019 can increase the frequency of page destroy operations as versioned pages are processed. These improvements reduce some contention scenarios but do not eliminate the underlying I/O performance issues that cause PAGEIOLATCH_DT.

The wait accumulates when the storage subsystem cannot complete I/O operations quickly enough to match the rate at which the buffer manager needs to reclaim pages. This creates a cascading effect where more sessions queue up waiting for page latches, eventually blocking user queries.

AutoDBA checks buffer pool configuration, storage performance metrics, and memory pressure indicators across your entire SQL Server instance in 60 seconds. Download the free diagnostic script and see what else needs attention.

Diagnostic Queries

-- Current PAGEIOLATCH_DT waits by session
SELECT 
    r.session_id,
    r.wait_type,
    r.wait_time,
    r.blocking_session_id,
    r.database_id,
    DB_NAME(r.database_id) as database_name,
    r.wait_resource,
    t.text as current_statement
FROM sys.dm_exec_requests r
INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.wait_type = 'PAGEIOLATCH_DT'
ORDER BY r.wait_time DESC;
-- Buffer pool pressure indicators (all memory values in MB for consistency)
SELECT 
    (bpool_committed_kb/1024) as buffer_pool_committed_mb,
    (target_server_memory_kb/1024) as target_server_memory_mb,
    page_life_expectancy,
    buffer_cache_hit_ratio,
    checkpoint_pages_per_sec,
    lazy_writes_per_sec
FROM 
(
    SELECT 
        SUM(CASE WHEN counter_name = 'Buffer cache hit ratio' THEN cntr_value END) as buffer_cache_hit_ratio,
        SUM(CASE WHEN counter_name = 'Page life expectancy' THEN cntr_value END) as page_life_expectancy,
        SUM(CASE WHEN counter_name = 'Checkpoint pages/sec' THEN cntr_value END) as checkpoint_pages_per_sec,
        SUM(CASE WHEN counter_name = 'Lazy writes/sec' THEN cntr_value END) as lazy_writes_per_sec
    FROM sys.dm_os_performance_counters 
    WHERE object_name LIKE '%Buffer Manager%'
) pc
CROSS JOIN 
(
    SELECT 
        SUM(CASE WHEN type = 'MEMORYCLERK_SQLBUFFERPOOL' THEN pages_kb END) as bpool_committed_kb
    FROM sys.dm_os_memory_clerks
) mc
CROSS JOIN
(
    SELECT cntr_value as target_server_memory_kb
    FROM sys.dm_os_performance_counters
    WHERE counter_name = 'Target Server Memory (KB)'
) tm;
-- Historical wait statistics for PAGEIOLATCH waits
SELECT 
    wait_type,
    waiting_tasks_count,
    wait_time_ms,
    max_wait_time_ms,
    signal_wait_time_ms,
    wait_time_ms / waiting_tasks_count as avg_wait_ms,
    (wait_time_ms - signal_wait_time_ms) / waiting_tasks_count as avg_resource_wait_ms
FROM sys.dm_os_wait_stats 
WHERE wait_type LIKE 'PAGEIOLATCH%'
ORDER BY wait_time_ms DESC;
-- Database file I/O performance metrics
SELECT 
    DB_NAME(vfs.database_id) as database_name,
    vfs.file_id,
    mf.physical_name,
    vfs.num_of_reads,
    vfs.num_of_bytes_read,
    vfs.io_stall_read_ms,
    vfs.num_of_writes,
    vfs.num_of_bytes_written,
    vfs.io_stall_write_ms,
    CASE WHEN vfs.num_of_reads = 0 THEN 0 ELSE vfs.io_stall_read_ms / vfs.num_of_reads END as avg_read_latency_ms,
    CASE WHEN vfs.num_of_writes = 0 THEN 0 ELSE vfs.io_stall_write_ms / vfs.num_of_writes END as avg_write_latency_ms
FROM sys.dm_io_virtual_file_stats(NULL, NULL) vfs
INNER JOIN sys.master_files mf ON vfs.database_id = mf.database_id AND vfs.file_id = mf.file_id
WHERE vfs.io_stall_read_ms > 0 OR vfs.io_stall_write_ms > 0
ORDER BY (vfs.io_stall_read_ms + vfs.io_stall_write_ms) DESC;
-- Buffer pool page allocation by database
SELECT 
    DB_NAME(database_id) as database_name,
    COUNT(*) * 8 / 1024 as buffer_mb,
    COUNT(*) as page_count
FROM sys.dm_os_buffer_descriptors
WHERE database_id > 4 -- Exclude system databases
GROUP BY database_id
ORDER BY COUNT(*) DESC;

Fix Scripts

Increase buffer pool memory allocation

-- Calculate recommended max server memory
-- Leave 4GB + 10% of total RAM for OS and other processes
DECLARE @total_physical_memory_mb BIGINT;
SELECT @total_physical_memory_mb = total_physical_memory_kb / 1024 
FROM sys.dm_os_sys_memory;

DECLARE @recommended_max_memory BIGINT = @total_physical_memory_mb - 4096 - (@total_physical_memory_mb * 0.1);

PRINT 'Current max server memory: ' + CAST((SELECT CAST(value_in_use as INT) FROM sys.configurations WHERE name = 'max server memory (MB)') as VARCHAR(10)) + ' MB';
PRINT 'Recommended max server memory: ' + CAST(@recommended_max_memory as VARCHAR(10)) + ' MB';

-- Uncomment to apply (TEST IN DEV FIRST)
-- EXEC sp_configure 'max server memory (MB)', @recommended_max_memory;
-- RECONFIGURE WITH OVERRIDE;

WARNING: Changing max server memory requires careful planning. Test in development and monitor after implementation. Consider other applications on the server.

Optimize checkpoint frequency for high-throughput databases

-- Set indirect checkpoint for databases with high write activity
-- This reduces checkpoint I/O spikes that can cause buffer pool pressure
USE master;
GO

-- Review current indirect checkpoint settings
SELECT 
    name,
    target_recovery_time_in_seconds,
    is_auto_create_stats_on,
    is_auto_update_stats_on
FROM sys.databases 
WHERE database_id > 4;

-- Example: Set 60-second recovery target for high-activity database
-- Uncomment and modify database name (TEST IN DEV FIRST)
/*
ALTER DATABASE [YourDatabaseName] 
SET TARGET_RECOVERY_TIME = 60 SECONDS;
*/

WARNING: Indirect checkpoint changes recovery behavior. Coordinate with backup and HA strategies. Monitor transaction log growth after implementation.

Clear buffer pool to eliminate stale statistics

-- Emergency buffer pool flush for diagnostic purposes
-- Only use during maintenance windows or severe performance issues
-- This will cause temporary performance degradation as pages reload

-- Check current buffer pool utilization first
SELECT 
    COUNT(*) * 8 / 1024 as current_buffer_mb,
    (SELECT cntr_value/1024 FROM sys.dm_os_performance_counters 
     WHERE counter_name = 'Target Server Memory (MB)') as target_mb
FROM sys.dm_os_buffer_descriptors;

-- Uncomment to flush buffer pool (MAINTENANCE WINDOW ONLY)
-- DBCC DROPCLEANBUFFERS;
-- DBCC FREEPROCCACHE;

PRINT 'Buffer pool flush completed. Monitor page life expectancy and I/O latency.';

CRITICAL WARNING: Only execute during planned maintenance. Will cause immediate performance impact as all data must reload from disk.

AutoDBA generates fix scripts like these automatically, with impact estimates and rollback SQL included.

Prevention

Configure max server memory to leave adequate RAM for the operating system, typically total RAM minus 4GB for servers under 32GB, or 90% of total RAM for larger systems. Monitor page life expectancy, targeting values above 300 seconds for stable workloads.

Implement storage performance monitoring with alerts for average disk latency exceeding 20ms for data files and 5ms for log files. Use SQL Server 2019+ Accelerated Database Recovery to reduce checkpoint I/O pressure, or configure indirect checkpoints with 60-120 second recovery targets for high-throughput databases.

Place tempdb on fast storage separate from user databases since tempdb churn significantly contributes to buffer pool pressure. On pre-2019 versions running NUMA systems, trace flag 8048 changes memory object partitioning from per-NUMA-node to per-logical-processor, which can reduce latch contention on CMEMTHREAD-sensitive workloads. In SQL Server 2019 and later, per-logical-processor partitioning is largely the default for affected memory objects, so TF 8048 is no longer necessary in most cases.

Monitor buffer cache hit ratio (target >95%), lazy writes per second (<20), and checkpoint pages per second. High values indicate insufficient memory or storage performance issues. Implement automated statistics updates during maintenance windows to prevent excessive page scanning during business hours.

Consider database-level optimization: partition large tables to reduce full table scans, implement proper indexing strategies to minimize logical reads, and use columnstore indexes for analytical workloads to improve compression ratios and reduce memory pressure.

Need hands-on help?

Dealing with persistent pageiolatch_dt issues across your environment? Samix Technology provides hands-on SQL Server performance consulting with 15+ years of production DBA experience.

Related Pages