Las consultas lentas son las asesinas silenciosas del rendimiento de bases de datos. Una sola consulta sin optimizar ejecutándose miles de veces al día puede colapsar un servidor MySQL que de otro modo funcionaría bien. El slow query log de MySQL es tu primera línea de defensa — captura cada consulta que excede un umbral de tiempo, proporcionándote los datos necesarios para identificar y eliminar cuellos de botella. Combinado con herramientas de análisis como mysqldumpslow y pt-query-digest, puedes encontrar, priorizar y corregir sistemáticamente las consultas más problemáticas de tu carga de trabajo.

Requisitos Previos

  • MySQL 5.7+ o MySQL 8.0 (los comandos funcionan en ambos; se indican diferencias menores)
  • Acceso root o administrativo al servidor MySQL
  • Acceso por shell al servidor que ejecuta MySQL
  • Percona Toolkit instalado (para pt-query-digest)
  • Conocimiento básico de SQL y archivos de configuración de MySQL

Activar el Slow Query Log

El slow query log está desactivado por defecto en la mayoría de instalaciones de MySQL. Puedes activarlo permanentemente mediante configuración o temporalmente en tiempo de ejecución.

Configuración Permanente (my.cnf)

Agrega estas líneas a tu archivo de configuración de MySQL, normalmente en /etc/mysql/my.cnf o /etc/my.cnf:

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

Reinicia MySQL para aplicar:

sudo systemctl restart mysql

Configuración en Tiempo de Ejecución (Sin Reinicio)

Activa el slow query log sin reiniciar el servidor:

SET GLOBAL slow_query_log = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_output = 'FILE';

Verifica la configuración:

SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

Salida esperada:

+---------------------+-------------------------------+
| Variable_name       | Value                         |
+---------------------+-------------------------------+
| slow_query_log      | ON                            |
| slow_query_log_file | /var/log/mysql/slow.log       |
+---------------------+-------------------------------+

Opciones de log_output

MySQL soporta tres destinos de salida:

  • FILE — escribe en el archivo especificado por slow_query_log_file (predeterminado, recomendado)
  • TABLE — escribe en la tabla mysql.slow_log (consultable pero agrega sobrecarga)
  • NONE — desactiva el registro a pesar de que slow_query_log=1

Usa FILE para producción. La opción TABLE agrega sobrecarga de escritura medible y hace que el esquema mysql crezca de forma impredecible.

Configurar Parámetros del Log

long_query_time

Este umbral determina qué consultas se registran. El valor predeterminado de 10 segundos es demasiado alto para la mayoría de las cargas de trabajo.

-- Registrar consultas que tarden más de 0.5 segundos
SET GLOBAL long_query_time = 0.5;

-- Registrar TODAS las consultas (útil para auditorías cortas, no para producción)
SET GLOBAL long_query_time = 0;

Valores iniciales recomendados:

Entornolong_query_timeJustificación
Desarrollo0Captura todo para análisis
Staging0.1Detecta consultas >100ms
Producción1.0Enfoque en consultas claramente lentas
Agresivo0.5Equilibrio entre ruido y cobertura

log_queries_not_using_indexes

Este parámetro registra consultas que realizan escaneos completos de tabla, sin importar el tiempo de ejecución:

SET GLOBAL log_queries_not_using_indexes = 1;

Es extremadamente valioso para detectar consultas que son rápidas ahora pero que se degradarán conforme las tablas crezcan. Sin embargo, puede generar muchas entradas de log en bases de datos con muchas tablas pequeñas.

min_examined_row_limit

Filtra consultas triviales requiriendo un número mínimo de filas examinadas:

SET GLOBAL min_examined_row_limit = 1000;

Esto evita que el log se llene con búsquedas rápidas de una sola fila que resultan no usar un índice.

Análisis con mysqldumpslow

mysqldumpslow viene incluido con MySQL y no requiere instalación adicional. Parsea el slow query log, abstrae valores literales y agrupa consultas similares.

Uso Básico

# Top 10 consultas por tiempo total
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# Top 10 consultas por frecuencia
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log

# Top 10 consultas por tiempo promedio
mysqldumpslow -s at -t 10 /var/log/mysql/slow.log

# Filtrar consultas que coincidan con un patrón
mysqldumpslow -s t -t 10 -g "orders" /var/log/mysql/slow.log

Opciones de Ordenamiento

FlagOrdena por
-s tTiempo total
-s cFrecuencia (conteo)
-s atTiempo promedio
-s lTiempo de bloqueo
-s alTiempo promedio de bloqueo
-s rFilas examinadas
-s arPromedio de filas examinadas

Ejemplo de Salida

Count: 1523  Time=2.45s (3731s)  Lock=0.00s (1s)  Rows=245.3 (373534), root[root]@localhost
  SELECT * FROM orders WHERE customer_id = N AND status = 'S' ORDER BY created_at DESC LIMIT N;

Esto indica que la consulta se ejecutó 1,523 veces, con un promedio de 2.45 segundos cada una, examinando 245 filas por ejecución y consumiendo 3,731 segundos en total.

Limitaciones

mysqldumpslow es útil para revisiones rápidas pero tiene limitaciones significativas:

  • Sin análisis de percentiles (P95, P99)
  • Sin distribución de tiempos de respuesta
  • No puede leer logs en formato binario o TABLE
  • Opciones limitadas de filtrado y reportes

Análisis Profundo con pt-query-digest

pt-query-digest del Percona Toolkit es el estándar de la industria para análisis de slow query logs. Proporciona información mucho más detallada que mysqldumpslow.

Instalación

# Debian/Ubuntu
sudo apt-get install percona-toolkit

# RHEL/CentOS
sudo yum install percona-toolkit

# Desde código fuente
wget https://www.percona.com/downloads/percona-toolkit/LATEST/tarball/percona-toolkit-LATEST.tar.gz
tar xzf percona-toolkit-LATEST.tar.gz
cd percona-toolkit-* && perl Makefile.PL && make && sudo make install

Uso Básico

# Reporte completo
pt-query-digest /var/log/mysql/slow.log

# Reporte de las últimas 24 horas
pt-query-digest --since '24h' /var/log/mysql/slow.log

# Filtrar por base de datos
pt-query-digest --filter '$event->{db} eq "myapp"' /var/log/mysql/slow.log

# Salida a archivo
pt-query-digest /var/log/mysql/slow.log > /tmp/slow-report.txt

Estructura del Reporte

La salida tiene tres secciones:

1. Resumen General

# 2.1M QPS, 1.2x concurrency, 45% of time in query
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time         3731s    100ms     45s   2.45s   4.12s   1.33s   1.85s
# Lock time            1s       0     15ms    10us    22us    38us     8us
# Rows sent        373534       0    1000     245   480.0   180.3   198.0
# Rows examine     2.1M         0   50000    1382    3200    2100     850

2. Perfil (Ranking de Consultas)

# Rank Query ID                         Response time  Calls  R/Call  Item
# ==== ================================ ============== ====== ======= ====
#    1 0xE77769C62EF669AA1A6...          1800.0 48.2%   1523  1.1819  SELECT orders
#    2 0xA4B9D8C1F3E5A7B2C9...           950.3 25.5%    892  1.0654  SELECT products
#    3 0xF1C2D3E4A5B6C7D8E9...           480.1 12.9%   3201  0.1500  SELECT users

3. Detalle por Consulta — para cada consulta clasificada, obtienes la huella completa, distribución de tiempos, recomendaciones EXPLAIN y una consulta de ejemplo con valores literales.

mysqldumpslow vs pt-query-digest vs MySQL Enterprise Monitor vs PMM

Característicamysqldumpslowpt-query-digestMySQL Enterprise MonitorPercona PMM
CostoGratis (incluido)Gratis (código abierto)Licencia comercialGratis (código abierto)
InstalaciónNingunaPercona ToolkitServidor dedicadoDocker/VM
Monitoreo en tiempo realNoNo
Análisis históricoBásicoExcelenteExcelenteExcelente
FingerprintingBásicoAvanzadoAvanzadoAvanzado
Análisis de percentilesNoSí (P95, P99)
Dashboard visualNoNoSí (Grafana)
Integración EXPLAINNoParcial
AlertasNoNo
Ideal paraRevisiones rápidasAnálisis profundoEquipos enterpriseEquipos que quieren UI gratis

Optimización de Consultas Lentas

Una vez que has identificado las consultas más lentas, sigue este enfoque sistemático.

Paso 1: EXPLAIN de la Consulta

EXPLAIN SELECT * FROM orders
WHERE customer_id = 42 AND status = 'pending'
ORDER BY created_at DESC LIMIT 10;

Columnas clave a examinar:

  • type: Debe ser ref, range o const. Evita ALL (escaneo completo de tabla)
  • key: Qué índice eligió MySQL. NULL significa que no se usó índice
  • rows: Filas estimadas examinadas. Números altos indican índices faltantes
  • Extra: Vigila Using filesort y Using temporary

En MySQL 8.0, usa EXPLAIN ANALYZE para estadísticas reales de ejecución:

EXPLAIN ANALYZE SELECT * FROM orders
WHERE customer_id = 42 AND status = 'pending'
ORDER BY created_at DESC LIMIT 10;

Paso 2: Agregar Índices Faltantes

Basándote en la salida de EXPLAIN, crea índices compuestos que cubran las cláusulas WHERE y ORDER BY:

-- Cubre WHERE customer_id AND status, ORDER BY created_at
CREATE INDEX idx_orders_customer_status_created
ON orders (customer_id, status, created_at DESC);

Reglas de diseño de índices:

  • Columnas de igualdad primero (customer_id, status)
  • Columnas de rango/ordenamiento al final (created_at)
  • Incluye columnas del SELECT si creas un índice de cobertura
  • Evita sobre-indexar — cada índice ralentiza las escrituras

Paso 3: Reescribir Patrones Problemáticos

Reescrituras comunes que mejoran el rendimiento:

-- MAL: Subconsulta ejecutada por cada fila
SELECT * FROM orders WHERE customer_id IN (
  SELECT id FROM customers WHERE region = 'US'
);

-- BIEN: JOIN se ejecuta una vez
SELECT o.* FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
WHERE c.region = 'US';

-- MAL: SELECT * trae columnas no utilizadas
SELECT * FROM orders WHERE id = 42;

-- BIEN: Selecciona solo columnas necesarias
SELECT id, status, total, created_at FROM orders WHERE id = 42;

-- MAL: Función en columna indexada impide uso del índice
SELECT * FROM orders WHERE YEAR(created_at) = 2025;

-- BIEN: Escaneo por rango usa el índice
SELECT * FROM orders
WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01';

Escenario del Mundo Real

Administras la base de datos de una plataforma de comercio electrónico. Los usuarios reportan que las páginas de productos cargan lentamente durante horas pico (2-6 PM), pero el resto del sitio funciona bien. Las métricas del servidor muestran picos de CPU que coinciden con las quejas, pero la memoria y el I/O de disco son normales.

Paso 1: Activa el slow query log con long_query_time=0.5 durante las horas pico.

Paso 2: Después de 2 horas de registro, ejecuta pt-query-digest:

pt-query-digest --since '2h' /var/log/mysql/slow.log

Paso 3: El reporte revela que la consulta principal consume el 62% del tiempo total:

SELECT p.*, c.name as category_name,
       (SELECT AVG(rating) FROM reviews WHERE product_id = p.id) AS avg_rating,
       (SELECT COUNT(*) FROM reviews WHERE product_id = p.id) AS review_count
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.category_id = 15 AND p.active = 1
ORDER BY created_at DESC LIMIT 20;

Paso 4: EXPLAIN muestra que las subconsultas correlacionadas se ejecutan una vez por fila de producto (2,400 veces por carga de página). Corrige reescribiendo con un JOIN:

SELECT p.*, c.name as category_name,
       COALESCE(r.avg_rating, 0) AS avg_rating,
       COALESCE(r.review_count, 0) AS review_count
FROM products p
JOIN categories c ON p.category_id = c.id
LEFT JOIN (
  SELECT product_id, AVG(rating) as avg_rating, COUNT(*) as review_count
  FROM reviews GROUP BY product_id
) r ON r.product_id = p.id
WHERE p.category_id = 15 AND p.active = 1
ORDER BY p.created_at DESC LIMIT 20;

Paso 5: Agrega un índice compuesto:

CREATE INDEX idx_products_category_active_created
ON products (category_id, active, created_at DESC);

Resultado: El tiempo de consulta baja de 2.4 segundos a 12 milisegundos. La carga de páginas vuelve a la normalidad.

Precauciones y Casos Especiales

  • La rotación de logs es esencial. Sin rotación, el slow log puede consumir todo el espacio disponible en disco. Usa logrotate o el comando FLUSH SLOW LOGS de MySQL con un cron job.
  • Monitoreo de espacio en disco. Configurar long_query_time=0 en un servidor ocupado puede generar gigabytes de datos de log por hora. Siempre monitorea el uso de disco al reducir el umbral.
  • Retraso de replicación por logging. En réplicas con log_slow_replica_statements=1 (MySQL 8.0.26+), registrar consultas replicadas puede aumentar el retraso. Monitorea Seconds_Behind_Source.
  • Inundación por log_queries_not_using_indexes. Tablas pequeñas de búsqueda (países, estados) activarán este flag aunque los escaneos completos en tablas de 50 filas sean óptimos. Usa min_examined_row_limit para filtrarlas.
  • Sentencias preparadas. Por defecto, las sentencias preparadas no se registran. Configura log_slow_extra=1 (MySQL 8.0.14+) para capturar estadísticas adicionales.
  • Los cambios en tiempo de ejecución no persisten. Los cambios con SET GLOBAL se pierden al reiniciar. Siempre actualiza my.cnf además de los comandos en tiempo de ejecución.

Resolución de Problemas

El archivo del slow query log no se crea: Verifica que MySQL tenga permisos de escritura en el directorio de logs:

sudo mkdir -p /var/log/mysql
sudo chown mysql:mysql /var/log/mysql
sudo chmod 750 /var/log/mysql

El log muestra cero consultas a pesar de tener slow_query_log=ON: Verifica que el long_query_time no esté configurado demasiado alto:

SELECT @@global.long_query_time;

Si muestra 10.000000, las consultas deben tardar más de 10 segundos para registrarse. Redúcelo a 1.

pt-query-digest reporta “No events processed”: El archivo de log puede estar vacío o el formato no es reconocido. Asegúrate de que log_output=FILE (no TABLE). Verifica los permisos:

ls -la /var/log/mysql/slow.log
file /var/log/mysql/slow.log

mysqldumpslow muestra “Permission denied”: La herramienta necesita acceso de lectura al archivo de log. Ejecuta con sudo o agrega tu usuario al grupo mysql:

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

Las consultas aparecen en el slow log pero EXPLAIN muestra ejecución rápida: Esto generalmente significa contención de bloqueos. Revisa el campo Lock_time en la entrada del log. Tiempos de bloqueo altos indican problemas de bloqueo a nivel de tabla o fila, no problemas de optimización de consultas.

Resumen

  • Activa el slow query log con slow_query_log=1 y configura long_query_time a 1 segundo o menos
  • Usa log_queries_not_using_indexes para detectar consultas que se degradarán conforme los datos crezcan
  • Comienza con mysqldumpslow para análisis rápido; cambia a pt-query-digest para análisis de nivel producción
  • Enfócate en el tiempo total (frecuencia × tiempo promedio) en lugar de solo la ejecución más lenta
  • Usa EXPLAIN y EXPLAIN ANALYZE (MySQL 8.0) para entender los planes de ejecución antes de crear índices
  • Diseña índices compuestos con columnas de igualdad primero y columnas de rango/ordenamiento al final
  • Reescribe subconsultas correlacionadas como JOINs para mejoras de un orden de magnitud
  • Siempre implementa rotación de logs y monitorea el espacio en disco al usar el slow query log en producción

Artículos Relacionados