Diagnóstico de I/O en SQL Server: cómo distinguir hardware lento de saturación por volumen
Guía evergreen basada en patrones reales observados en data warehouses con replicación CDC, AlwaysOn y cargas BI concurrentes. Las métricas y umbrales son los que aplican en storage empresarial moderno (SAN/All-Flash); ajustar para arquitecturas distintas.
Por qué el I/O es el wait type más malinterpretado
Cuando una aplicación va lenta, lo primero que casi todo el mundo culpa es el storage. Es fácil de afirmar, difícil de refutar — los discos siempre pueden ir más rápido. Pero la realidad es que la mayoría de los problemas que se atribuyen al I/O son en realidad problemas de carga, índices o estadísticas que manifiestan síntoma de I/O.
Distinguir entre un problema real de hardware (la cabina está mal, hay un cable suelto, el firmware está desactualizado) y un problema de saturación por volumen (la cabina va bien pero le pides más IOPS de los que puede) es lo que separa un buen diagnóstico de uno que termina culpando al equipo de infraestructura sin razón.
Esta guía recoge los patrones que uso para hacer ese diagnóstico en menos de 30 minutos.
1. Acumulado vs delta: la primera regla
sys.dm_io_virtual_file_stats muestra valores acumulados desde el último reinicio del servicio SQL Server. Si tu servidor lleva 60 días sin reiniciar, un valor alto de io_stall_read_ms no significa que el I/O esté lento ahora — puede reflejar un pico de hace 3 semanas.
Para diagnóstico en caliente, siempre usa delta entre dos snapshots separados por 30-60 segundos:
-- Snapshot 1
SELECT DB_NAME(vfs.database_id) AS db,
mf.physical_name,
vfs.io_stall_read_ms, vfs.num_of_reads,
vfs.io_stall_write_ms, vfs.num_of_writes
INTO #io_snap1
FROM sys.dm_io_virtual_file_stats(NULL, NULL) vfs
JOIN sys.master_files mf
ON mf.database_id = vfs.database_id
AND mf.file_id = vfs.file_id;
WAITFOR DELAY '00:00:30';
SELECT DB_NAME(vfs.database_id) AS db,
mf.physical_name,
vfs.io_stall_read_ms, vfs.num_of_reads,
vfs.io_stall_write_ms, vfs.num_of_writes
INTO #io_snap2
FROM sys.dm_io_virtual_file_stats(NULL, NULL) vfs
JOIN sys.master_files mf
ON mf.database_id = vfs.database_id
AND mf.file_id = vfs.file_id;
-- Delta: latencia media de los últimos 30 segundos
SELECT s2.db,
s2.physical_name,
(s2.num_of_reads - s1.num_of_reads) AS reads_30s,
(s2.io_stall_read_ms - s1.io_stall_read_ms) /
NULLIF(s2.num_of_reads - s1.num_of_reads, 0) AS avg_read_ms,
(s2.num_of_writes - s1.num_of_writes) AS writes_30s,
(s2.io_stall_write_ms - s1.io_stall_write_ms) /
NULLIF(s2.num_of_writes - s1.num_of_writes, 0) AS avg_write_ms
FROM #io_snap2 s2
JOIN #io_snap1 s1 ON s1.db = s2.db AND s1.physical_name = s2.physical_name
ORDER BY avg_read_ms DESC;
DROP TABLE #io_snap1, #io_snap2;
Esta query cuesta menos de un segundo de coordinación y da la foto exacta del momento. Si el resultado dice avg_read_ms = 2, tu storage está sano ahora aunque el acumulado lleve 60 días con valores horribles.
2. Wait types de I/O y qué significan
PAGEIOLATCH_SH y PAGEIOLATCH_EX
Espera para leer/escribir páginas de datos físicos al/desde el buffer pool. Por sí solo no indica problema — es el wait type más común en cargas con mucha lectura/escritura física.
< 2mspor wait: completamente normal, incluso con carga CDC/replicación intensa.2-10mssostenido: aceptable.> 10mssostenido: empezar a investigar storage (saturación, firmware, configuración de RAID).
HADR_SYNC_COMMIT
Overhead del commit síncrono en AlwaysOn. El primario espera confirmación del secundario antes de confirmar la transacción.
~4ms por commit: esperable en redes de datacenter con SSD.5-10ms: revisar la red entre nodos.> 10ms: revisar latencia de storage del secundario, posibles drops en switches.
WRITELOG
Espera para escribir el log de transacciones. Es uno de los waits más sensibles a storage mal configurado.
< 1ms: log en SSD dedicado, configuración óptima.1-5ms: aceptable.> 10ms: revisar volumen de log, fragmentación de VLF, tamaño inicial del fichero, tipo de disco.
IO_COMPLETION
Espera de I/O no asociada a páginas de datos (típicamente backups, restore, sort spills a tempdb).
- Si aparece dominante con
> 10ms, sospechar tempdb mal configurado o un backup mal programado durante carga BI.
3. Umbrales de referencia para storage empresarial
| Métrica | Aceptable | Investigar | Crítico |
|---|---|---|---|
| Latencia lectura | < 5ms | 5-15ms | > 15ms |
| Latencia escritura | < 5ms | 5-10ms | > 10ms |
| HADR_SYNC_COMMIT | < 5ms | 5-10ms | > 10ms |
| PAGEIOLATCH | < 2ms | 2-5ms | > 5ms |
| WRITELOG | < 1ms | 1-5ms | > 5ms |
Umbrales para storage empresarial (SAN o All-Flash con conexión de fibra). Para entornos cloud (Azure SQL Managed Instance, AWS RDS), añade ~3ms a cada umbral por la latencia de red. Para storage local con HDD tradicional, multiplica por 5-10 los valores.
4. El patrón día/noche: la pista más diagnóstica
Si la latencia de I/O es baja de noche (sub-ms) y alta de día (7-15ms) con los mismos datos:
Interpretación: el storage está sano pero saturado por volumen. No es un fallo de hardware.
Confirmación rápida: ejecuta el snapshot delta a las 3 AM y a las 11 AM. Si los números difieren en un orden de magnitud, es saturación. Si son similares, es problema estructural.
Causas típicas de saturación día/noche: - Carga de CDC/replicación + queries BI + mantenimientos concurrentes durante el día. - Backups mal programados que se solapan con horario laboral. - Anti-virus haciendo scan en discos de SQL Server (sí, sigue pasando en 2026).
Solución: capacidad (más IOPS), no cambio de configuración. Si tu cabina tiene SSD pero el QoS está limitado a 5.000 IOPS y tú estás pidiendo 20.000, no hay magia que arregle eso desde SQL Server.
5. Diagnóstico por sesión activa
Para perfilar el I/O de una sesión concreta sin perder de vista el conjunto:
SELECT
wait_type,
waiting_tasks_count,
wait_time_ms,
wait_time_ms / NULLIF(waiting_tasks_count, 0) AS avg_wait_ms
FROM sys.dm_exec_session_wait_stats
WHERE session_id = @spid
ORDER BY wait_time_ms DESC;
Esta DMV (disponible desde SQL 2016) es clave para confirmar si una sesión específica tiene problemas de I/O, CPU o bloqueos. Si la sesión está en un wait que no es de I/O y solo aparecen waits del tipo LCK_M_* o SOS_SCHEDULER_YIELD, tu problema no es storage por mucho que el ticket diga "queries lentas".
6. Ratios y comprobaciones complementarias
Buffer cache hit ratio
Aunque obsoleto como métrica primaria (modernos SSD lo hacen menos relevante), un valor consistentemente por debajo del 90% indica que el buffer pool no está reteniendo páginas calientes. Causas: RAM insuficiente, queries que escanean tablas enteras, índices que el motor descarta.
Page Life Expectancy (PLE)
Tiempo medio que una página permanece en el buffer pool antes de ser desalojada. La regla "300 segundos" del año 2008 ya no aplica — para un servidor moderno con 256+ GB, los valores normales son 5.000-50.000+ segundos. PLE de 22 segundos (lo he visto en producción) significa que el buffer pool se está rotando más de dos veces por minuto, normalmente porque alguien escaneó una tabla mucho mayor que la RAM disponible.
Read-Ahead vs lectura aleatoria
SELECT
DB_NAME(database_id) AS db,
SUM(num_of_reads) AS total_reads,
SUM(num_of_bytes_read) / 1024 / 1024 AS mb_read,
SUM(num_of_bytes_read) / NULLIF(SUM(num_of_reads), 0) AS bytes_per_read
FROM sys.dm_io_virtual_file_stats(NULL, NULL)
WHERE database_id > 4
GROUP BY database_id
ORDER BY mb_read DESC;
Si bytes_per_read está por debajo de 8.192 (1 página), tienes lecturas aleatorias dominantes — síntoma de queries que tocan páginas dispersas (índices fragmentados, falta de cobertura, scans pequeños). Si está cerca de 65.536 (8 páginas, lectura extent), el motor está haciendo read-ahead, lo cual es eficiente.
7. Casos reales de referencia
- Data warehouse: PAGEIOLATCH alto en horas de trabajo con CDC activo. Delta a mediodía: 7ms write. Delta a las 3 AM: sub-ms con los mismos datos. Diagnóstico: saturación por volumen, no fallo hardware. Solución: aumentar QoS de la cabina + reprogramar backups.
- OLTP con AlwaysOn: HADR_SYNC_COMMIT consistentemente en 12-15ms. Diagnóstico: latencia de red elevada entre nodos por una saturación del switch principal. Solución: equipo de redes movió el VLAN del cluster a switches dedicados.
- Servidor con tablas de 300M+ filas: WRITELOG de 18ms durante cargas masivas. Diagnóstico: un único fichero de log de 250 GB con 8.000 VLFs. Solución: shrink + recrear el log con
INITIAL_SIZE = 200GBpara reducir VLFs a ~64.
Conclusión
Diagnosticar I/O bien es 80% saber leer las DMV correctas y 20% disciplina para usar deltas en lugar de acumulados. La diferencia entre culpar al equipo de storage y arreglar tu propia cadena ETL suele estar a tres queries de distancia.
Las preguntas que debes responderte antes de abrir un ticket de hardware:
- ¿La latencia delta a las 3 AM con el mismo volumen es la misma que ahora?
- ¿El wait type dominante es realmente de I/O o es bloqueos disfrazados?
- ¿Hay un patrón día/noche claro que indica saturación, no fallo?
Si las tres tienen respuesta, ya sabes a quién pertenece el problema.