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.

Bases de datos · SQL Server

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

  1. 01Antes de nada: ¿qué está esperando?
  2. 02Las consultas más costosas
  3. 03Índices: faltantes, inútiles y duplicados
  4. 04Estadísticas y parameter sniffing
  5. 05Bloqueos entre sesiones
  6. 06Memoria mal configurada
  7. 07tempdb
  8. 08Almacenamiento y crecimiento automático
  9. 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.

Triaje · esperas acumuladas
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 dominaMira el punto
CXPACKET / CXCONSUMERParalelismo 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:xContención de tempdb · punto 6
RESOURCE_SEMAPHOREMemoria insuficiente para las concesiones de consulta · punto 6
WRITELOGLatencia del log de transacciones · punto 7
SOS_SCHEDULER_YIELDPresión real de CPU. Aquí sí puede ser hardware · punto 2 primero
Cómo leer esto

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í.

1

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.

Top consultas por CPU acumulado
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;
Qué buscar

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.

Ojo con el peor sospechoso

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.

2

Í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.

Índices que el motor echa de menos
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.

Índices que solo cuestan
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;
Cómo actuar

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.

3

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.

Estadísticas con más cambios pendientes
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;
Acción

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.

4

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.

Quién bloquea a quién, ahora mismo
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>);
Causas habituales

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.

5

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.

Configuración y salud de memoria
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');
Valores de referencia

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.

6

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.

Contención y configuración de tempdb
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:%';
Regla práctica

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.

7

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.

Latencia de E/S por archivo
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;
Referencias

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.

El crecimiento automático en porcentaje

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.

8

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.

Cuándo sí es hardware

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.

CS
Consultoría SysAdmin

¿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
Diagnóstico inicial sin costo. Si tu instancia está bien configurada, te lo decimos con los datos en la mano.