Solución de problemas de consultas lentas en MySQL: una guía paso a paso

Un flujo de trabajo práctico para encontrar consultas lentas de MySQL, leer planes de ejecución, corregir índices y demostrar que el cambio funcionó.

Solución de problemas de consultas lentas en MySQL: una guía paso a paso

La solución de problemas de consultas lentas en MySQL comienza con una regla incómoda: no adivinar solo por el texto de la consulta. Una consulta que se ve fea puede ser inofensiva porque se ejecuta una vez al día. Una consulta que parece simple puede estar arruinando la base de datos porque se ejecuta miles de veces por minuto, escanea demasiadas filas o espera detrás de bloqueos.

El flujo de trabajo útil es aburrido en el mejor sentido. Captura consultas lentas reales, agrupa por costo, inspecciona el plan de ejecución, cambia una cosa y mide de nuevo. Eso evita que agregues índices aleatorios, cambies configuraciones globales a ciegas o culpes a MySQL cuando la aplicación está enviando un patrón de consultas evitable.

Normalmente empiezo con tres preguntas:

  • ¿Qué consulta está afectando a los usuarios, no solo pareciendo sospechosa?
  • ¿El tiempo se gasta leyendo filas, ordenando, esperando bloqueos o esperando a la aplicación?
  • ¿Puedo probar la corrección con EXPLAIN, tiempos y datos nuevos del registro de consultas lentas?

Comienza con el registro de consultas lentas

El registro de consultas lentas de MySQL registra las sentencias que superan el umbral configurado. Según el manual de MySQL, el registro está deshabilitado por defecto, long_query_time tiene un valor predeterminado de 10 segundos, y una sentencia normalmente necesita ejecutarse al menos ese tiempo y examinar al menos min_examined_row_limit filas antes de ser registrada. Si log_queries_not_using_indexes está habilitado, MySQL también puede registrar sentencias que no usan índices para búsquedas de filas. Esa opción es útil durante el diagnóstico, pero puede generar mucho ruido en sistemas ocupados.

Una configuración inicial práctica se ve así:

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_output = FILE

En muchos sistemas de producción, 1 segundo es un primer paso razonable. Para una API sensible a la latencia, puedes reducirlo temporalmente a 0.5 o 0.2. Hazlo con un plan y vigilando el espacio en disco. Una base de datos de alto tráfico puede generar una cantidad sorprendente de datos de registro lento una vez que el umbral baja.

Puedes verificar la configuración activa desde una sesión de MySQL:

SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'slow_query_log_file';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'log_output';
SHOW VARIABLES LIKE 'min_examined_row_limit';
SHOW VARIABLES LIKE 'log_queries_not_using_indexes';

Para una investigación temporal, puedes habilitar el registro sin editar el archivo de configuración:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

Recuerda que los cambios con SET GLOBAL pueden no sobrevivir a un reinicio a menos que también los persistas a través de tu proceso de configuración normal. En MySQL 8, algunos equipos usan SET PERSIST, pero yo prefiero comprometer la configuración deseada en la gestión de configuración para que el próximo operador pueda verla.

Si activas log_queries_not_using_indexes, considera también establecer log_throttle_queries_not_using_indexes para que un endpoint ruidoso no inunde el registro. MySQL admite ese límite precisamente porque el registro de falta de índice puede crecer rápidamente.

Agrupa el registro antes de leer consultas individuales

Los registros de consultas lentas en bruto son repetitivos. Puedes ver la misma consulta cientos de veces con diferentes IDs. Leer el archivo de principio a fin pierde tiempo y hace que las consultas raras y alarmantes parezcan más importantes que las comunes y costosas.

Comienza con mysqldumpslow, que viene con las instalaciones de MySQL en muchos entornos:

sudo mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log

Eso pide los diez patrones principales ordenados por tiempo de consulta. Las banderas exactas varían según la versión y la plataforma, así que revisa mysqldumpslow --help si tu comando se comporta de manera diferente. Las ordenaciones útiles incluyen tiempo total, tiempo promedio, tiempo de bloqueo y filas examinadas.

Para investigaciones en producción, pt-query-digest de Percona Toolkit suele ser mejor porque proporciona una agrupación más rica y detalles de percentiles. La herramienta no es mágica; simplemente te ahorra hacer cálculos a mano. Lo importante es clasificar por impacto. Una consulta que tarda ocho segundos una vez por noche puede ser menos urgente que una que tarda 120 milisegundos pero se ejecuta 600 veces por segundo.

Al leer la salida agrupada, busca patrones:

  • Tiempo total alto: probablemente visible para el usuario o con uso intensivo de recursos.
  • Recuento alto: a menudo un bucle de aplicación o falta de caché.
  • Filas examinadas altas con filas enviadas bajas: generalmente un problema de indexación o filtrado.
  • Tiempo de bloqueo alto: posiblemente una transacción, contención de escritura, bloqueo de metadatos o problema de DDL.

No asumas que Rows_examined alto siempre es malo. Las consultas de informes y los trabajos por lotes a veces escanean intencionalmente. La pregunta es si el escaneo coincide con el trabajo y si ocurre en el momento adecuado.

Reproduce una consulta de forma segura

Elige un patrón de consulta y obtén una muestra real con parámetros. Si el registro lento normalizó los literales, encuentra la consulta original en los registros de la aplicación, trazas de APM o la entrada del registro lento sin procesar.

Antes de ejecutarla manualmente, verifica el radio de explosión. Un SELECT lento en una réplica suele ser seguro. Un UPDATE lento en producción no es algo que se deba reejecutar casualmente. Para consultas de escritura, primero inspecciona el plan y el patrón de transacciones, o prueba contra una copia de staging con datos realistas.

Una nota útil para cada consulta se ve así:

Endpoint: GET /customers/123/orders
Patrón de consulta: pedidos por cliente y estado, más recientes primero
Observado: 1.8s promedio, 420k filas examinadas, 20 filas enviadas
Tamaño de tabla: 12M filas
Tamaño de resultado esperado: una página de pedidos
Sospecha: falta un índice compuesto para customer_id, status, created_at

Esa nota mantiene el trabajo vinculado a una ruta de usuario real en lugar de un fragmento SQL aleatorio.

Usa EXPLAIN, luego léelo como un operador

Ejecuta EXPLAIN en la consulta lenta:

EXPLAIN
SELECT id, customer_id, status, created_at, total
FROM orders
WHERE customer_id = 123
  AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

Para MySQL 8, EXPLAIN ANALYZE puede ejecutar la consulta y mostrar información de tiempo real. Úsalo con cuidado en consultas costosas porque realmente ejecuta la sentencia. Para consultas SELECT simples en un entorno controlado, puede ser muy útil.

Las columnas que reviso primero son type, possible_keys, key, rows, filtered y Extra.

type te dice el patrón de acceso. const, eq_ref, ref y range suelen ser buenas señales. index significa que MySQL está escaneando un índice, lo que aún puede ser demasiado trabajo. ALL significa un escaneo completo de tabla. Un escaneo completo no es automáticamente incorrecto en una tabla pequeña, pero es sospechoso en una tabla activa con millones de filas.

key muestra el índice que MySQL eligió. Si possible_keys enumera un índice prometedor pero key es diferente, el optimizador puede pensar que el otro índice es más barato. Eso puede ocurrir debido a una selectividad deficiente, estadísticas obsoletas o un índice que no coincide con el filtro y la ordenación juntos.

rows es una estimación, no una promesa. Si la estimación es muy incorrecta, ejecuta ANALYZE TABLE durante una ventana de mantenimiento adecuada o revisa si la distribución de datos está sesgada.

Extra a menudo cuenta la historia. Using filesort significa que MySQL necesita un paso de ordenación separado; no necesariamente implica ordenación en disco, pero vale la pena verificarlo cuando el conjunto de resultados es grande. Using temporary aparece a menudo con agrupaciones, consultas distintas u ordenaciones complejas. Using index puede ser bueno porque la consulta se satisface desde el índice sin leer filas de la tabla.

Corrige índices teniendo en cuenta la forma completa de la consulta

La corrección más común para consultas lentas no es "agregar un índice a la columna en la cláusula WHERE". La mejor regla es: construir un índice que coincida con la forma en que la consulta filtra, une, ordena y limita filas.

Para la consulta de pedidos anterior, un índice de una sola columna en customer_id podría ayudar, pero aún podría dejar a MySQL ordenando muchas filas para ese cliente. Un índice compuesto suele ser más útil:

CREATE INDEX idx_orders_customer_status_created
ON orders (customer_id, status, created_at);

Si la consulta ordena por los más recientes primero, MySQL a menudo puede escanear el índice en orden inverso. En MySQL 8 también puedes definir índices descendentes cuando eso se ajuste a un patrón más grande:

CREATE INDEX idx_orders_customer_status_created_desc
ON orders (customer_id, status, created_at DESC);

El orden de las columnas importa. Pon primero los filtros de igualdad, luego las columnas de rango u ordenación cuando eso coincida con la consulta. Por ejemplo, con WHERE customer_id = ? AND status = ? ORDER BY created_at DESC LIMIT 20, customer_id, status, created_at suele ser más útil que created_at, customer_id, status.

No agregues todos los índices que parezcan útiles. Los índices aceleran las lecturas pero ralentizan las escrituras y consumen almacenamiento. Si la tabla recibe muchas inserciones o actualizaciones, un nuevo índice compuesto tiene un costo real. Primero verifica los índices existentes:

SHOW INDEX FROM orders;

A veces la respuesta correcta es reemplazar dos índices débiles con un mejor índice compuesto, no mantener los tres.

Reescribe consultas que bloquean el uso de índices

Algunas consultas lentas lo son porque ocultan valores indexados detrás de funciones o patrones que MySQL no puede usar de manera eficiente.

Esta versión es común y dolorosa:

SELECT *
FROM orders
WHERE YEAR(created_at) = 2026;

Si created_at está indexado, envolverlo en YEAR() puede impedir una búsqueda de rango normal. Escribe el predicado como un rango en su lugar:

SELECT id, customer_id, status, created_at, total
FROM orders
WHERE created_at >= '2026-01-01'
  AND created_at <  '2027-01-01';

La misma idea se aplica a las búsquedas con comodín al inicio:

WHERE email LIKE '%@example.com'

Un índice B-tree normal no puede saltar al medio de una cadena. Si las búsquedas de sufijo importan, es posible que necesites una columna generada, un campo normalizado separado o un sistema de búsqueda diseñado para ese caso de uso.

También ten cuidado con SELECT *. Parece inofensivo durante el desarrollo, pero puede obligar a MySQL a leer filas de la tabla cuando una proyección más pequeña podría usar un índice cobertor. También envía datos innecesarios a través de la red.

Verifica bloqueos cuando el plan de consulta parece correcto

Una consulta puede tener un plan decente y aún así ser lenta porque está esperando. El Lock_time del registro lento puede señalarte en esa dirección, pero no explica todos los tipos de espera. Si los usuarios informan pausas aleatorias, verifica las sesiones activas:

SHOW PROCESSLIST;

En MySQL 8, las vistas de Performance Schema y sys schema pueden dar mejores detalles, dependiendo de cómo esté configurado el servidor. Para una revisión rápida, a menudo verifico las transacciones de larga duración y las sentencias bloqueadas antes de cambiar índices.

Un ejemplo real: una consulta UPDATE orders SET status = ? WHERE id = ? debería ser rápida. Si aparece en el registro lento con una búsqueda por clave primaria, el problema puede ser una transacción que dejó la fila bloqueada mientras hacía trabajo no relacionado. La solución no es otro índice. La solución es acortar la transacción y mover las llamadas externas lentas fuera de ella.

Los bloqueos de metadatos pueden crear una trampa similar. Una migración que ejecuta ALTER TABLE puede esperar a una transacción antigua, mientras que las nuevas consultas se acumulan detrás del DDL pendiente. El registro de consultas lentas mostrará síntomas, pero la causa raíz es el comportamiento de despliegue.

Ajusta la configuración del servidor solo después del trabajo en consultas

La configuración importa, pero es fácil abusar de ella como primera respuesta. Si una consulta escanea cinco millones de filas para devolver diez, aumentar la memoria solo puede hacer que el mal plan duela menos.

Para sistemas con uso intensivo de InnoDB, innodb_buffer_pool_size es el primer ajuste a revisar. En un servidor MySQL dedicado, a menudo se establece en una gran parte de la memoria, pero el valor correcto depende de qué más se ejecuta en el host, el tamaño del conjunto de datos y la carga de trabajo. No copies un porcentaje ciegamente de una publicación de blog.

También verifica si la base de datos está esperando en el disco. Si el conjunto de trabajo no cabe en la memoria, o el almacenamiento está saturado, incluso las consultas bien indexadas pueden detenerse. Combina la revisión de consultas con métricas del host: CPU, latencia de disco, IOPS, presión de memoria y número de conexiones.

Los pools de conexiones pueden hacer que las consultas lentas se vean peor. Si un endpoint dispara demasiadas sentencias lentas, el pool se llena, las solicitudes no relacionadas esperan por conexiones y toda la aplicación se siente rota. En ese caso, arreglar la consulta sigue siendo el trabajo principal, pero los límites del pool y los tiempos de espera determinan con qué gracia falla el sistema.

Prueba la corrección

Después de agregar un índice o reescribir una consulta, ejecuta EXPLAIN de nuevo. Quieres ver menos filas estimadas, una clave mejor elegida y menos pasos adicionales costosos. Luego prueba la consulta real con parámetros realistas.

No te detengas en una ejecución rápida. La caché cálida puede ocultar problemas. Prueba casos comunes, grandes y difíciles:

  • Un cliente con muchos pedidos.
  • Un cliente sin pedidos coincidentes.
  • Un rango de fechas que abarca un período ocupado.
  • Un valor de estado que coincide con la mayoría de las filas.

Luego observa el registro lento después del despliegue. El mejor resultado no es "la consulta se veía mejor en staging". El mejor resultado es que el patrón de consulta desaparezca de los principales infractores, la presión de CPU o E/S disminuya y la ruta del usuario sea más rápida.

La solución de problemas de consultas lentas en MySQL es principalmente una recopilación de evidencia disciplinada. Habilita el registro con umbrales sensatos, agrupa los patrones costosos, inspecciona el plan, corrige la forma de la consulta y valida con datos nuevos. Ese hábito previene tanto la corrección insuficiente como la corrección excesiva, que es exactamente lo que quieres cuando la base de datos ya está bajo presión.