SQL Server lento: los 8 lugares donde siempre está el problema
Antes de comprar más RAM, revisa estos ocho puntos. En la mayoría de los casos, uno de ellos explica todo.
La conversación siempre empieza igual: «el sistema está lentísimo, ¿le subimos RAM al servidor?». Y a veces sí es la RAM. Pero en la enorme mayoría de los casos que llegan a nuestras manos, el cuello de botella está en uno de ocho lugares muy concretos, y ninguno se arregla comprando hardware.
Escalar sin diagnosticar tiene un costo doble: pagas el hardware y además pierdes la oportunidad de encontrar el problema real, que suele seguir ahí, esperando a que el sistema crezca lo suficiente como para volver a manifestarse. Solo que ahora con un servidor más caro.
Esta guía va ordenada por probabilidad, no por elegancia. Cada punto trae la consulta DMV para verificarlo, qué valor deberías ver y qué hacer si no lo ves. Empieza por arriba y baja: es raro llegar al punto cinco sin haber encontrado la causa.
Sumario
- 01Antes de nada: ¿qué está esperando?
- 02Las consultas más costosas
- 03Índices: faltantes, inútiles y duplicados
- 04Estadísticas y parameter sniffing
- 05Bloqueos entre sesiones
- 06Memoria mal configurada
- 07tempdb
- 08Almacenamiento y crecimiento automático
- 09Configuración de instancia
Antes de nada: ¿qué está esperando?
SQL Server registra en qué gasta el tiempo cuando no está ejecutando. Esa tabla es el triaje: te dice hacia dónde mirar antes de que empieces a adivinar. Ejecútala primero, siempre.
SELECT TOP 10
wait_type,
wait_time_ms / 1000.0 AS espera_total_s,
signal_wait_time_ms / 1000.0 AS espera_por_cpu_s,
waiting_tasks_count
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
'CLR_SEMAPHORE','LAZYWRITER_SLEEP','RESOURCE_QUEUE','SLEEP_TASK',
'SLEEP_SYSTEMTASK','SQLTRACE_BUFFER_FLUSH','WAITFOR','XE_TIMER_EVENT',
'BROKER_TASK_STOP','CHECKPOINT_QUEUE','DIRTY_PAGE_POLL',
'REQUEST_FOR_DEADLOCK_SEARCH','SP_SERVER_DIAGNOSTICS_SLEEP')
ORDER BY wait_time_ms DESC;
-- Para medir una ventana concreta, reinicia el contador y espera 30 min:
-- DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR);
| Si domina | Mira el punto |
|---|---|
| CXPACKET / CXCONSUMER | Paralelismo mal configurado y consultas costosas · puntos 2 y 8 |
| PAGEIOLATCH_* | Lecturas de disco: faltan índices o el almacenamiento es lento · puntos 3 y 7 |
| LCK_M_* | Bloqueos entre sesiones · punto 5 |
| PAGELATCH_UP en recurso 2:x:x | Contención de tempdb · punto 6 |
| RESOURCE_SEMAPHORE | Memoria insuficiente para las concesiones de consulta · punto 6 |
| WRITELOG | Latencia del log de transacciones · punto 7 |
| SOS_SCHEDULER_YIELD | Presión real de CPU. Aquí sí puede ser hardware · punto 2 primero |
Un tipo de espera dominante no es un veredicto, es una dirección. Las esperas te dicen dónde buscar; los puntos siguientes te dicen qué encontrar ahí.
Las consultas más costosas
Es el punto uno por una razón simple: en la práctica, un puñado de consultas consume la mayor parte de los recursos de casi cualquier instancia. Arreglar tres suele mover más la aguja que duplicar el hardware.
SELECT TOP 15
qs.execution_count AS ejecuciones,
qs.total_worker_time / 1000 AS cpu_total_ms,
qs.total_worker_time / qs.execution_count / 1000 AS cpu_promedio_ms,
qs.total_logical_reads / qs.execution_count AS lecturas_promedio,
qs.total_elapsed_time / qs.execution_count / 1000 AS duracion_promedio_ms,
SUBSTRING(st.text, (qs.statement_start_offset/2) + 1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset END
- qs.statement_start_offset) / 2) + 1) AS consulta
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY qs.total_worker_time DESC;
Ordena por CPU total y luego por lecturas lógicas promedio. Una consulta con 200,000 lecturas lógicas por ejecución está recorriendo tablas completas: casi siempre le falta un índice o tiene una función aplicada sobre la columna del WHERE, lo que impide usar el que ya existe.
La consulta más lenta no siempre es la más dañina. Una que tarda 3 segundos y corre 40,000 veces al día hace más daño que una de 40 segundos que corre dos veces. Prioriza por costo total, no por duración individual.
Índices: faltantes, inútiles y duplicados
El motor lleva registro de los índices que le habrían servido. No hay que creerle ciegamente —sugiere de más y no considera el costo de escritura— pero como punto de partida es excelente.
SELECT TOP 10
ROUND(s.avg_total_user_cost * s.avg_user_impact
* (s.user_seeks + s.user_scans), 0) AS beneficio_estimado,
d.statement AS tabla,
d.equality_columns, d.inequality_columns, d.included_columns,
s.user_seeks, s.user_scans, s.last_user_seek
FROM sys.dm_db_missing_index_group_stats s
JOIN sys.dm_db_missing_index_groups g ON s.group_handle = g.index_group_handle
JOIN sys.dm_db_missing_index_details d ON g.index_handle = d.index_handle
ORDER BY beneficio_estimado DESC;
Y el lado que casi nadie revisa: los índices que sobran. Cada índice que nadie lee sigue pagándose en cada INSERT, UPDATE y DELETE.
SELECT OBJECT_NAME(i.object_id) AS tabla,
i.name AS indice,
us.user_updates AS escrituras_pagadas,
ISNULL(us.user_seeks,0) + ISNULL(us.user_scans,0)
+ ISNULL(us.user_lookups,0) AS lecturas
FROM sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats us
ON i.object_id = us.object_id
AND i.index_id = us.index_id
AND us.database_id = DB_ID()
WHERE i.type_desc = 'NONCLUSTERED'
AND OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
AND ISNULL(us.user_seeks,0) + ISNULL(us.user_scans,0)
+ ISNULL(us.user_lookups,0) = 0
ORDER BY us.user_updates DESC;
No apliques la sugerencia tal cual: consolida. Si el motor pide tres índices parecidos sobre la misma tabla, casi siempre uno bien diseñado cubre los tres. Y antes de borrar un índice sin uso, confirma que las estadísticas de uso llevan acumulándose desde el último reinicio del servicio —no desde ayer.
Estadísticas desactualizadas y parameter sniffing
El optimizador decide con base en estadísticas. Si están viejas, va a estimar que una tabla tiene mil filas cuando tiene ocho millones, y elegirá un plan pensado para mil. Es el clásico «ayer corría en 2 segundos y hoy tarda 4 minutos» sin que nadie tocara nada.
SELECT OBJECT_NAME(s.object_id) AS tabla,
s.name AS estadistica,
sp.last_updated,
sp.rows,
sp.modification_counter AS filas_cambiadas_desde_entonces
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE sp.modification_counter > 0
ORDER BY sp.modification_counter DESC;
Actualiza con UPDATE STATISTICS y muestreo suficiente en las tablas grandes; el muestreo por defecto se queda corto en tablas de decenas de millones de filas. Si el problema reaparece con ciertos parámetros y no con otros, ya no es estadísticas: es parameter sniffing, y se ataca con OPTIMIZE FOR UNKNOWN, recompilación selectiva o reescribiendo el procedimiento.
Bloqueos entre sesiones
El síntoma es inconfundible: todo se pone lento a la misma hora, el CPU está tranquilo y el disco también. No hay saturación de nada; hay sesiones formadas esperando a que otra suelte.
SELECT r.session_id AS bloqueada,
r.blocking_session_id AS bloqueadora,
r.wait_type, r.wait_time / 1000.0 AS esperando_s,
r.wait_resource,
DB_NAME(r.database_id) AS base,
t.text AS consulta
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.blocking_session_id <> 0;
-- ¿Qué está corriendo la sesión bloqueadora?
-- DBCC INPUTBUFFER(<session_id>);
Transacciones abiertas demasiado tiempo —incluida la aplicación que abre transacción, llama a un servicio externo y espera—, orden inconsistente de acceso a las tablas, y reportes pesados corriendo contra producción sin aislamiento adecuado. Para lo último, un snapshot isolation bien evaluado resuelve el 80% de los casos.
Memoria mal configurada
El error más frecuente es dejar max server memory en su valor por defecto: SQL Server toma toda la que puede, el sistema operativo se queda sin margen y empieza a intercambiar. Rendimiento pésimo con un servidor que en el papel tiene RAM de sobra.
SELECT name, value_in_use
FROM sys.configurations
WHERE name IN ('max server memory (MB)', 'min server memory (MB)',
'max degree of parallelism',
'cost threshold for parallelism');
SELECT counter_name, cntr_value
FROM sys.dm_os_performance_counters
WHERE counter_name IN ('Page life expectancy', 'Memory Grants Pending');
Deja entre 4 y 8 GB al sistema operativo, más margen si el servidor comparte con otros servicios. Page life expectancy conviene leerlo como al menos 300 segundos por cada 4 GB asignados —no el mítico 300 fijo, que viene de servidores con 4 GB—. Y Memory Grants Pending debe ser cero de forma sostenida: cualquier otra cosa es presión real de memoria.
tempdb
tempdb es compartida por toda la instancia y se convierte en cuello de botella con una facilidad notable: tablas temporales, variables de tabla, ordenamientos que no caben en memoria, versionado de filas. Cuando hay contención, se manifiesta como lentitud generalizada sin culpable claro.
SELECT COUNT(*) AS archivos_de_datos
FROM tempdb.sys.database_files
WHERE type_desc = 'ROWS';
-- Contención de páginas de asignación: base_id 2 es tempdb
SELECT session_id, wait_type, wait_resource, wait_time
FROM sys.dm_exec_requests
WHERE wait_type LIKE 'PAGELATCH%'
AND wait_resource LIKE '2:%';
Varios archivos de datos del mismo tamaño y con el mismo incremento: uno por núcleo hasta ocho, y si sigue habiendo contención, subir de cuatro en cuatro. Tamaño inicial generoso para que no crezca en horas pico, y tempdb en el disco más rápido que tengas.
Almacenamiento y crecimiento automático
Aquí es donde muchas instancias «lentas» resultan estar perfectamente sanas: el motor espera al disco. Mide la latencia real por archivo antes de culpar a nadie.
SELECT DB_NAME(vfs.database_id) AS base,
mf.physical_name,
vfs.num_of_reads, vfs.num_of_writes,
vfs.io_stall_read_ms / NULLIF(vfs.num_of_reads, 0) AS ms_por_lectura,
vfs.io_stall_write_ms / NULLIF(vfs.num_of_writes, 0) AS ms_por_escritura
FROM sys.dm_io_virtual_file_stats(NULL, NULL) vfs
JOIN sys.master_files mf
ON vfs.database_id = mf.database_id AND vfs.file_id = mf.file_id
ORDER BY ms_por_lectura DESC;
Archivos de datos: por debajo de 20 ms es aceptable, por debajo de 10 es bueno. Log de transacciones: idealmente menos de 5 ms, porque cada confirmación espera ahí. Si el log está por arriba, ese solo dato explica una lentitud generalizada.
Un archivo configurado para crecer en 10% funciona bien con 1 GB y es un desastre con 400 GB: cada crecimiento congela las escrituras mientras reserva 40 GB. Usa incrementos fijos y razonables —512 MB o 1 GB— y dimensiona los archivos por adelantado. Verifica también que esté habilitada la inicialización instantánea de archivos.
Configuración de instancia
Cuatro ajustes que siguen en su valor por defecto en la mayoría de las instalaciones y que rara vez son los correctos.
- —Cost threshold for parallelism. El valor por defecto es 5, un número de los años noventa. Consultas triviales se paralelizan sin necesidad y aparecen esperas CXPACKET por todos lados. Subirlo al rango de 40 a 50 es un punto de partida sensato.
- —MAXDOP. En 0 significa «usa todos los núcleos para todo». Ajústalo al número de núcleos por nodo NUMA, con tope de 8 en la mayoría de los escenarios.
- —Nivel de compatibilidad de la base. Una base migrada que quedó en un nivel viejo no aprovecha las mejoras del optimizador. Súbelo, pero con pruebas: puede cambiar planes.
- —Plan de energía del servidor. En «equilibrado», Windows reduce la frecuencia del procesador. Debe estar en alto rendimiento; es un cambio de treinta segundos con efecto medible.
El error de escalar sin diagnosticar
Agregar RAM o núcleos a una instancia con problemas de índices o de consultas mal escritas produce un alivio temporal y engañoso. La memoria adicional permite cachear más páginas, la consulta que hacía recorridos completos ahora los hace en memoria, y todo parece mejorar. Durante unos meses.
Mientras tanto, el problema real siguió creciendo. Y cuando reaparezca —lo hará— vas a tener menos margen, porque ya usaste el comodín del hardware. En licenciamiento por núcleo, además, escalar CPU tiene un costo que suele superar con creces el de una semana de trabajo de optimización.
Cuando las esperas dominantes son de CPU o de E/S, las consultas ya están optimizadas, los índices son los correctos y la configuración está bien. Entonces el hardware es la respuesta, y además vas a saber exactamente cuánto y de qué necesitas, en lugar de comprar a ciegas.
Orden de trabajo
Diagnóstico en una sesión
Imprimir o guardar como PDF- Registrar el síntoma con precisión: qué está lento, para quién y desde cuándo «Todo está lento» no es un síntoma diagnosticable
- Revisar esperas acumuladas y anotar los tres tipos dominantes sys.dm_os_wait_stats
- Sacar el top 15 de consultas por CPU total y por lecturas lógicas sys.dm_exec_query_stats + sys.dm_exec_sql_text
- Revisar índices faltantes y consolidar sugerencias antes de crear nada sys.dm_db_missing_index_details
- Listar índices sin lecturas y con muchas escrituras sys.dm_db_index_usage_stats
- Verificar antigüedad de estadísticas en las tablas más grandes sys.dm_db_stats_properties
- Confirmar max server memory, MAXDOP y cost threshold sys.configurations
- Medir latencia de E/S por archivo, con atención especial al log sys.dm_io_virtual_file_stats
- Revisar número, tamaño y crecimiento de los archivos de tempdb tempdb.sys.database_files
- Documentar el antes: sin línea base no vas a poder demostrar la mejora Guarda las salidas de todas las consultas anteriores con fecha
Dos a tres horas de trabajo. En la mayoría de los casos, el hallazgo aparece antes de llegar al punto seis.
Un servidor más grande no arregla una consulta mal escrita. Solo le da más espacio para hacer lo mismo, mal, un poco más rápido.
¿Vas a comprarle más RAM al servidor sin saber si es el problema?
Hacemos el diagnóstico completo de tu instancia: esperas, consultas costosas, índices, estadísticas, tempdb, memoria y almacenamiento. Te entregamos los hallazgos priorizados por impacto, con la corrección de cada uno y una línea base para medir la mejora. Casi siempre sale más barato que el hardware.
- —Diagnóstico y optimización de rendimiento en bases de datos
- —Mantenimiento gestionado de SQL Server y PostgreSQL
- —Respaldos, alta disponibilidad y pruebas de restauración
- —Observabilidad para aplicaciones .NET