WAIT_ON_SYNC_STATISTICS_REFRESH: cuando auto-stats bloquea tu ETL durante 90 minutos
Caso real de un data warehouse en producción. Nombres de bases de datos y tablas enmascarados; los volúmenes, tiempos y tamaños de memoria descritos son los reales del incidente.
La incidencia: el SP "que normalmente tarda 15 segundos"
Lunes a las 7:30 de la mañana. El responsable de BI escribe: "el cuadro de mando comercial no carga, el ETL nocturno parece haber acabado pero el procedimiento pr_actualiza_kpis sigue ejecutándose desde las 4 AM". Habitualmente ese SP termina en 15 segundos.
Una mirada rápida a sys.dm_exec_requests muestra una sesión esperando el wait type más confuso de SQL Server:
session_id: 87
wait_type: WAIT_ON_SYNC_STATISTICS_REFRESH
wait_time: 4.987.300 ms (≈83 minutos)
status: suspended
Llevaba 83 minutos esperando. No bloqueada por nadie. Sin actividad de I/O. Solo esperando a que terminase una actualización de estadísticas que SQL Server había decidido lanzar de forma síncrona.
Qué es WAIT_ON_SYNC_STATISTICS_REFRESH y por qué es peligroso
Cuando una query toca una tabla cuyas estadísticas están "obsoletas" según el umbral del motor (modification_counter por encima del threshold), SQL Server puede tomar dos caminos:
- Async (preferido): ejecuta la query con las estadísticas viejas y dispara una actualización en background. Las próximas ejecuciones usarán las nuevas.
- Sync (por defecto): bloquea la compilación de la query hasta que las estadísticas se actualicen.
Si la BD está con AUTO_UPDATE_STATISTICS_ASYNC OFF (el default desde SQL 2008), el motor toma la ruta síncrona. En tablas pequeñas no se nota — el sample por defecto tarda milisegundos. Pero en tablas con 300, 400 o 500 millones de filas, la cosa cambia drásticamente.
El motor decide hacer FULLSCAN síncrono, calcula el grant de memoria necesario, y cuando ese grant excede los gigas de buffer disponibles, la operación tarda decenas de minutos.
Y mientras dura ese FULLSCAN, ninguna ejecución de ese SP puede compilar. Todas las sesiones nuevas se acumulan esperando.
El diagnóstico paso a paso
Lo primero, ver quién está esperando y qué tabla bloquea la compilación:
SELECT
r.session_id,
r.wait_type,
r.wait_time / 1000 AS wait_seg,
r.wait_time / 60000 AS wait_min,
DB_NAME(r.database_id) AS bd,
LEFT(t.text, 250) AS query_actual
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.wait_type = 'WAIT_ON_SYNC_STATISTICS_REFRESH'
ORDER BY r.wait_time DESC;
Resultado: 12 sesiones, todas esperando entre 60 y 90 minutos sobre el mismo SP. La query del header apuntaba a la tabla dbo.fact_clientes_movimientos — 312 millones de filas.
El siguiente paso, ver qué estadística está actualizándose:
SELECT
OBJECT_NAME(s.object_id) AS tabla,
s.name AS stat_name,
sp.last_updated,
sp.rows AS filas_total,
sp.rows_sampled,
CAST(sp.rows_sampled * 100.0 / NULLIF(sp.rows, 0) AS DECIMAL(5,2)) AS pct_sampled,
sp.modification_counter
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE OBJECT_NAME(s.object_id) = 'fact_clientes_movimientos'
ORDER BY sp.modification_counter DESC;
| stat_name | last_updated | filas_total | rows_sampled | pct_sampled | modification_counter |
|---|---|---|---|---|---|
_WA_Sys_00000004_... | 2026-04-01 04:11:22 | 312.847.291 | 312.847.291 | 100,00 | 25.300.000 |
La auto-stat se había disparado a las 4:11 AM porque el ETL de la noche había modificado más de 25 millones de filas. SQL Server decidió FULLSCAN, lo que sobre 312M filas requiere un grant de memoria proporcional al tamaño de los índices implicados. En este caso concreto: 107 GB de grant.
El servidor tenía 128 GB de RAM. El motor estaba leyendo y procesando casi todos los datos en memoria, presionando el buffer pool, generando lecturas físicas adicionales, y todo el sistema se ralentizaba en cadena.
Por qué FULLSCAN sobre 300 millones de filas es así de caro
Una actualización de estadísticas con FULLSCAN en una tabla grande ejecuta esencialmente:
-- Lo que hace SQL Server por debajo
SELECT
columna_clave,
COUNT(*) OVER (PARTITION BY columna_clave) AS frecuencia,
NTILE(200) OVER (ORDER BY columna_clave) AS bucket
FROM fact_clientes_movimientos;
Es un scan completo del índice clusterizado, ordenación, agrupación por claves, y construcción de un histograma de 200 buckets. El motor calcula el grant de memoria asumiendo el peor caso: ordenar todas las filas en memoria.
Sobre 312 millones de filas con un índice clusterizado de ~700 bytes por fila, hablamos de ~218 GB de datos a procesar. Como el grant físico no llega, parte del sort se va a tempdb, lo que añade I/O adicional y multiplica el tiempo.
Mientras tanto, las sesiones que querían compilar SPs que tocaban esa tabla quedan en cola, esperando.
La solución de emergencia y la solución estructural
Emergencia (no recomendado pero necesario en mitad del incidente): forzar al SP a usar el plan en caché ignorando la actualización de stats:
-- Recompilar el SP con un hint para evitar usar las stats nuevas
ALTER PROCEDURE pr_actualiza_kpis WITH RECOMPILE
-- ...y dentro del SP, en cada query:
SELECT ... FROM fact_clientes_movimientos OPTION (USE HINT('DISABLE_OPTIMIZED_NESTED_LOOP'));
Esto puede ayudar puntualmente pero no soluciona la raíz del problema.
La solución estructural es activar AUTO_UPDATE_STATISTICS_ASYNC en la BD:
ALTER DATABASE BD_DW SET AUTO_UPDATE_STATISTICS_ASYNC ON;
A partir de ahí, cuando el threshold se dispara, SQL Server lanza la actualización en background y permite que la query siga ejecutándose con las estadísticas viejas. La próxima vez que el SP se ejecute, ya tendrá las nuevas estadísticas. Sin bloqueos. Sin esperas.
Las únicas razones para mantener async OFF son escenarios muy específicos (queries con cardinalidades extremadamente sensibles a la frescura de las estadísticas), y en data warehouses con tablas grandes esos casos son la excepción, no la regla.
La opción intermedia: pre-actualización controlada
Si por algún motivo el cliente no quiere activar async (algunos vendors de software certifican la BD con configuraciones específicas), la alternativa es pre-actualizar las estadísticas durante la ventana de mantenimiento, con SAMPLE en lugar de FULLSCAN:
-- Al final del ETL nocturno, antes de que los SPs de BI se ejecuten:
UPDATE STATISTICS dbo.fact_clientes_movimientos WITH SAMPLE 30 PERCENT;
Una muestra del 30% sobre 312M filas tarda 8-12 minutos contra los 90+ del FULLSCAN, y el histograma resultante es lo suficientemente bueno para el 99% de los planes. La diferencia es nada despreciable: cuando SQL Server ya ha actualizado las estadísticas hace 2 horas, el threshold de modificación no se dispara y no hay auto-stat síncrona durante el día.
Para tablas críticas con muchas modificaciones, programar este UPDATE STATISTICS al final del ETL es lo que evita los pasos en falso.
Cómo monitorizar este patrón antes de que pase
Una alerta semanal que detecte estadísticas grandes con FULLSCAN reciente te avisa antes de que el problema salga a producción:
-- Estadísticas grandes con FULLSCAN reciente
-- Candidatas a generar WAIT_ON_SYNC_STATISTICS_REFRESH si la BD está en async OFF
SELECT
DB_NAME() AS bd,
OBJECT_NAME(s.object_id) AS tabla,
s.name AS stat_name,
sp.last_updated,
sp.rows,
CAST(sp.rows_sampled * 100.0 / NULLIF(sp.rows, 0) AS DECIMAL(5,2)) AS pct,
DATABASEPROPERTYEX(DB_NAME(), 'IsAutoUpdateStatisticsAsync') AS async_on
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE sp.rows > 100000000
AND sp.rows_sampled = sp.rows
AND sp.last_updated > DATEADD(DAY, -7, GETUTCDATE())
ORDER BY sp.rows DESC;
Si async_on = 0 y aparecen filas en este resultado, has identificado el siguiente incidente que va a ocurrir.
Conclusión
WAIT_ON_SYNC_STATISTICS_REFRESH es un wait type silencioso pero devastador. No aparece en alertas estándar de SQL Server, no genera bloqueos visibles, y muchos DBAs nunca lo han visto en su carrera. Pero en cualquier instancia con tablas de 100M+ filas y AUTO_UPDATE_STATISTICS_ASYNC OFF, es solo cuestión de tiempo que aparezca.
La regla práctica:
- En cualquier data warehouse:
AUTO_UPDATE_STATISTICS_ASYNC ON. No es opcional. - En cualquier OLTP con tablas mayores de 50M filas: lo mismo.
- En cualquier BD con SPs críticos cuya compilación no puede tardar más de unos pocos segundos: lo mismo.
Es una línea de configuración. Cinco palabras. Y evita el incidente al amanecer del lunes.