MySQL en producción rara vez “se cae” con un crash dramático. Simplemente se vuelve más lento. La latencia sube, la profundidad de cola crece, el equipo de la app dice “no cambiamos nada” y tu noche de guardia se disuelve en una niebla de medias verdades y paneles que no coinciden en nada.
El registro de consultas lentas es el único instrumento que corta el ruido. No porque sea sofisticado, sino porque escribe lo que tu base de datos realmente estuvo haciendo. Si estás en Debian 13 y sospechas que una única consulta está silenciosamente convirtiendo tu servidor en un radiador, así es como la cazas, la demuestras y la arreglas sin empeorar las cosas.
Por qué el registro de consultas lentas sigue siendo ganador en 2025
MySQL ha ganado mucha instrumentación: Performance Schema, vistas del esquema sys, trazas del optimizador, digest de sentencias y suficientes perillas para construir una cabina de control. Aun así, el registro de consultas lentas sigue siendo la forma más rápida de conectar “los usuarios están enfadados” con “este es el SQL que lo provoca”. No es perfecto. No es completo. Simplemente es fiable y útil.
Esto es lo que hace mejor que la mayoría de los paneles:
- Captura la realidad bajo carga. No la consulta que desearías ejecutar, sino la que realmente se ejecutó cuando todo estaba en llamas.
- Es barato para arrancar. Actívalo, muestrea, y sigue. Performance Schema puede ser más pesado si no sabes qué estás activando.
- Es evidencia portátil. Un archivo de log lento es algo que puedes pasar a un compañero, archivar con un ticket de incidente y comparar entre versiones.
- Convierte quejas vagas en sospechosos ordenados. Tiempo total, conteo, filas examinadas, tiempo de bloqueo: suficiente para saber dónde cavar.
Y sí, también puede convertirse en un vertedero de consultas “lentas” que en realidad están bien. Por eso ajustas los umbrales e interpretas como operador: te importan el throughput, la latencia tail y la contención de recursos, no solo un número llamado “Query_time”.
Broma 1: El registro de consultas lentas es como un rastreador de tiempo en el trabajo: es incómodo hasta que ves realmente dónde se fue tu día.
Una cita para llevar (idea parafraseada): “La esperanza no es una estrategia”, atribuida a círculos de liderazgo de operaciones; trata el trabajo de rendimiento igual: mide primero, luego actúa.
Hechos interesantes y un poco de historia (para que dejes de repetir errores antiguos)
No necesitas trivia para administrar bases de datos, pero algunos hechos concretos explican por qué existen ciertas perillas y por qué algunas “soluciones obvias” siguen fallando.
- El registro de consultas lentas antecede a la observabilidad moderna. Fue una de las primeras formas “integradas” de encontrar problemas de consultas sin perfilar externo.
- “Lento” es una elección de política, no una verdad universal. Una consulta de 200 ms es lenta para un endpoint de login pero rápida para un informe nocturno.
- MySQL puede registrar consultas que no usan índices. La bandera
log_queries_not_using_indexesexiste porque los escaneos completos de tabla son una forma fiable de provocar incidencias sorpresa. - El log puede incluir sentencias administrativas. Con
log_slow_admin_statementsverás DDL o mantenimientos “inocentes” que detienen la producción. - InnoDB cambió la forma de lo “lento”. Cuando InnoDB se volvió el motor por defecto, muchas ralentizaciones pasaron de CPU/parseo a esperas de I/O y comportamiento de bloqueo.
- La caché de consultas se eliminó en MySQL 8.0. Terminó una era de “lo arreglamos con cache”; buena decisión—la invalidación de query cache era generadora de caos.
- La replicación puede mostrar las consultas lentas dos veces. Una sentencia lenta en el primario suele ser lenta en réplicas también; además, las réplicas pueden retrasarse y agravar el problema.
- La replicación por filas hizo menos relevante el “mismo texto de sentencia”. Problemas de rendimiento a veces se ocultan en lógica de triggers o efectos secundarios, no solo en el SELECT crudo.
- MySQL moderno expone resúmenes basados en digest. Performance Schema puede agregar por patrones normalizados; el slow log muestra ejemplos concretos que puedes reproducir.
Guía de diagnóstico rápido (primero/segundo/tercero)
Este es el orden que encuentra al culpable rápidamente, sin empeorar tu incidente. La meta no es “optimizar MySQL”. La meta es “detener la hemorragia”.
Primero: decide si estás limitado por CPU, I/O o bloqueos
- Si la CPU está a tope: espera planes malos, índices faltantes, grandes ordenamientos o demasiadas consultas concurrentes.
- Si el disco está saturado: espera escaneos, lecturas grandes, misses del buffer pool o presión de checkpoints/flush.
- Si los hilos esperan: espera bloqueos, problemas de metadata lock, transacciones largas o filas calientes.
Segundo: habilita o confirma el slow log, luego captura una ventana limpia
No leas un log de hace una semana y adivines. Rota y luego captura 5–15 minutos durante el dolor. Quieres una ventana estrecha que coincida con los síntomas.
Tercero: ordena por tiempo total, no por “la peor consulta única”
La consulta que corrió 20 segundos una vez es dramática. La consulta que corre 120 ms 4.000 veces por minuto es la que te está comiendo el almuerzo en silencio. Ordena por sum(Query_time) y por count, luego profundiza en ejemplos.
Cuarto: verifica con EXPLAIN y estadísticas reales de handlers
Los slow logs te dicen lo que pasó. EXPLAIN te dice lo que MySQL planeó hacer. Ambos pueden estar equivocados aisladamente. Combínalos.
Habilitar el registro de consultas lentas en Debian 13 (y no hacerte daño)
Debian 13 suele ejecutar MySQL 8.0-ish o MariaDB según tu elección. La mecánica es similar, pero las ubicaciones de configuración y los valores por defecto del paquete difieren. Asumiré Oracle MySQL (paquete mysql-server) con systemd; los mismos pasos aplican mayormente a MariaDB con nombres de servicio y variables ligeramente distintos.
Dónde suele vivir la configuración
/etc/mysql/my.cnfincluye otros archivos./etc/mysql/mysql.conf.d/mysqld.cnfes el lugar común para ajustes del servidor./var/log/mysql/es un directorio común de logs, pero verifica permisos y perfiles AppArmor.
Configuración mínima sensata para slow-log
No pienses demasiado. Empieza con logging a archivo, un umbral modesto y evita registrar cada consulta en un sistema caliente.
cr0x@server:~$ sudo editor /etc/mysql/mysql.conf.d/mysqld.cnf
...add or adjust...
slow_query_log = ON
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 0.2
log_queries_not_using_indexes = OFF
log_slow_admin_statements = ON
log_slow_replica_statements = OFF
Por qué estas elecciones:
long_query_time=0.2es lo bastante agresivo para atrapar “muerte por mil cortes” sin registrar todo (usualmente). Ajusta después.log_queries_not_using_indexes=OFFal principio porque puede llenar los logs con escaneos pequeños de tablas inocuas.log_slow_admin_statements=ONporque DDL en línea y comandos admin pueden paralizar la producción.
Recargar de forma segura
Cambiar ajustes del slow log a menudo puede hacerse dinámicamente, pero no lo asumas. Usa SET PERSIST cuando proceda y valida con SHOW VARIABLES.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estos son los movimientos de operador que funcionan a las 2 a. m. Cada uno incluye qué significa la salida y qué decisión tomar después.
Tarea 1: Confirma qué servidor estás ejecutando (MySQL vs MariaDB) y la versión
cr0x@server:~$ mysql --version
mysql Ver 8.0.36-0+deb13u1 for Linux on x86_64 (MySQL Community Server - GPL)
Qué significa: Sabes qué nombres de variables y funciones aplican. MySQL 8 tiene SET PERSIST; MariaDB difiere.
Decisión: Usa la sintaxis de MySQL 8 y espera que Performance Schema esté disponible por defecto.
Tarea 2: Comprueba si el logging lento está habilitado actualmente
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'slow_query_log';"
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | OFF |
+----------------+-------+
Qué significa: Actualmente no tienes rastro forense.
Decisión: Actívalo inmediatamente (dinámico si es posible), y luego captura una ventana corta durante carga.
Tarea 3: Habilitar slow log dinámicamente (inmediato, no después de reiniciar)
cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log = ON;"
...Query OK, 0 rows affected...
Qué significa: Nuevas consultas lentas se registrarán ahora mismo.
Decisión: Establece la ruta del archivo a continuación, verifica que sea escribible y luego ajusta el umbral.
Tarea 4: Establece la ubicación del archivo slow log y verifica que MySQL pueda escribirlo
cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';"
...Query OK, 0 rows affected...
cr0x@server:~$ sudo install -o mysql -g adm -m 0640 /dev/null /var/log/mysql/mysql-slow.log
cr0x@server:~$ sudo ls -l /var/log/mysql/mysql-slow.log
-rw-r----- 1 mysql adm 0 Dec 30 01:12 /var/log/mysql/mysql-slow.log
Qué significa: El archivo existe con una propiedad sensata. MySQL no fallará silenciosamente porque no puede abrir el log.
Decisión: Si la propiedad es incorrecta, arréglala antes de perseguir problemas fantasma de “no hay consultas lentas”.
Tarea 5: Establece long_query_time para triaje
cr0x@server:~$ mysql -e "SET GLOBAL long_query_time = 0.2; SHOW VARIABLES LIKE 'long_query_time';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| long_query_time | 0.200 |
+-----------------+-------+
Qué significa: Se registran las consultas más lentas que 200 ms.
Decisión: Durante un incidente, bájalo (por ejemplo, 0.1) si necesitas más muestras. Después, súbelo para reducir ruido.
Tarea 6: Rota el slow log para aislar una ventana de tiempo
cr0x@server:~$ mysql -e "FLUSH SLOW LOGS;"
...Query OK, 0 rows affected...
cr0x@server:~$ sudo ls -lh /var/log/mysql/mysql-slow.log*
-rw-r----- 1 mysql adm 12K Dec 30 01:15 /var/log/mysql/mysql-slow.log
-rw-r----- 1 mysql adm 1.8M Dec 30 01:12 /var/log/mysql/mysql-slow.log.1
Qué significa: Has creado un archivo “actual” limpio. El anterior está archivado con sufijo.
Decisión: Captura los siguientes 5–15 minutos de dolor y analiza solo esa ventana.
Tarea 7: Confirma que MySQL realmente está escribiendo entradas
cr0x@server:~$ sudo tail -n 5 /var/log/mysql/mysql-slow.log
# Time: 2025-12-30T01:16:19.123456Z
# User@Host: app[app] @ 10.0.2.41 [] Id: 9123
# Query_time: 0.412 Lock_time: 0.000 Rows_sent: 20 Rows_examined: 250000
SET timestamp=1735521379;
SELECT * FROM orders WHERE user_id=84721 ORDER BY created_at DESC LIMIT 20;
Qué significa: El logging funciona. Esta muestra ya huele a “falta de índice para ORDER BY + filtro”.
Decisión: No optimices a partir de una sola línea. Agrega agregación a continuación.
Tarea 8: Agregación rápida con mysqldumpslow
cr0x@server:~$ sudo mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log
Reading mysql slow query log from /var/log/mysql/mysql-slow.log
Count: 184 Time=0.29s (52s) Lock=0.00s (0s) Rows=20.0 (3680), app@app[10.0.2.41]
SELECT * FROM orders WHERE user_id=N ORDER BY created_at DESC LIMIT N
Count: 21 Time=1.10s (23s) Lock=0.02s (0s) Rows=1.0 (21), app@app[10.0.2.55]
UPDATE inventory SET qty=qty-N WHERE sku='S'
Qué significa: La primera consulta consume 52 segundos en total en esta ventana por su alto conteo. La segunda tiene mayor tiempo por consulta pero menor tiempo total.
Decisión: Arregla primero al culpable con mayor tiempo total a menos que la segunda cause bloqueos o una falla en cascada.
Tarea 9: Instala y ejecuta pt-query-digest para un ranking real
cr0x@server:~$ sudo apt-get update
...output...
cr0x@server:~$ sudo apt-get install -y percona-toolkit
...output...
cr0x@server:~$ sudo pt-query-digest /var/log/mysql/mysql-slow.log | sed -n '1,80p'
# 2025-12-30T01:20:44
# Profile
# Rank Query ID Response time Calls R/Call V/M Item
# ==== ================== ============= ===== ====== ===== ====
# 1 0xA1B2C3D4E5F6A7B8 52.1230 65.0% 184 0.2833 0.12 SELECT orders
# 2 0x1122334455667788 23.0041 28.7% 21 1.0954 0.03 UPDATE inventory
# 3 0x9988776655443322 3.9002 4.9% 97 0.0402 0.01 SELECT sessions
Qué significa: Ahora tienes una lista ordenada por tiempo de respuesta total. Los fingerprints de consulta previenen ruido de “misma consulta, distintos IDs”.
Decisión: Extrae la sección completa del informe para el Rank 1 y úsala para reproducir y EXPLAIN.
Tarea 10: Obtén contexto de esquema e índices para la consulta top
cr0x@server:~$ mysql -e "SHOW CREATE TABLE orders\G" | sed -n '1,60p'
*************************** 1. row ***************************
Table: orders
Create Table: CREATE TABLE `orders` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`created_at` datetime(6) NOT NULL,
`status` varchar(32) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB
Qué significa: No hay índice compuesto en (user_id, created_at). La consulta filtra por user_id y ordena por created_at—caso clásico.
Decisión: Planea un índice que coincida con el patrón de acceso: (user_id, created_at DESC) en MySQL 8 (soporta DESC), o (user_id, created_at) que también suele ayudar.
Tarea 11: Valida el plan de ejecución (EXPLAIN) y lo que está escaneando
cr0x@server:~$ mysql -e "EXPLAIN SELECT * FROM orders WHERE user_id=84721 ORDER BY created_at DESC LIMIT 20\G"
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 248901
filtered: 10.00
Extra: Using where; Using filesort
Qué significa: Escaneo completo de tabla (type: ALL) más filesort. Ahí está tu I/O y CPU.
Decisión: Añade el índice correcto, luego vuelve a ejecutar EXPLAIN para confirmar que lo usa y mide antes/después.
Tarea 12: Añade el índice con cuidado (online cuando sea posible) y observa el impacto
cr0x@server:~$ mysql -e "ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at DESC);"
...Query OK, 0 rows affected...
Qué significa: El índice se creó. Dependiendo del tamaño de la tabla y de los ajustes de MySQL, esto puede tardar y consumir I/O. En tablas grandes, hazlo en ventana de mantenimiento o usa herramientas de cambio de esquema online.
Decisión: Valida inmediatamente el nuevo plan y comprueba si el volumen de slow log baja. Si hay replicación, verifica el lag en réplicas.
Tarea 13: Vuelve a ejecutar EXPLAIN para confirmar la mejora del plan
cr0x@server:~$ mysql -e "EXPLAIN SELECT * FROM orders WHERE user_id=84721 ORDER BY created_at DESC LIMIT 20\G"
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
type: ref
possible_keys: idx_user_created
key: idx_user_created
key_len: 8
ref: const
rows: 20
Extra: Using index condition
Qué significa: MySQL ahora usa el índice y espera ~20 filas, no ~250k. Filesort desaparece.
Decisión: Monitoriza latencia e I/O. Si mejoró, probablemente encontraste “la consulta”. Sigue con el siguiente culpable.
Tarea 14: Comprueba si el tiempo “lento” fue en realidad tiempo de bloqueo
cr0x@server:~$ grep -E "Query_time|Lock_time" -n /var/log/mysql/mysql-slow.log | head
2:# Query_time: 0.412 Lock_time: 0.000 Rows_sent: 20 Rows_examined: 250000
8:# Query_time: 1.905 Lock_time: 1.723 Rows_sent: 1 Rows_examined: 1
Qué significa: La segunda muestra pasó la mayor parte del tiempo esperando bloqueos, no ejecutándose.
Decisión: Deja de “optimizar” texto SQL y busca quién mantiene los bloqueos: transacciones largas, filas calientes o metadata locks.
Tarea 15: Identifica bloqueadores actuales y esperas de bloqueo
cr0x@server:~$ mysql -e "SHOW PROCESSLIST;" | sed -n '1,20p'
Id User Host db Command Time State Info
9123 app 10.0.2.41:51022 prod Query 2 Sending data SELECT * FROM orders WHERE user_id=84721 ORDER BY created_at DESC LIMIT 20
9150@app 10.0.2.55:53488 prod Query 35 Waiting for row lock UPDATE inventory SET qty=qty-1 WHERE sku='ABC-123'
9201 admin localhost prod Query 120 Starting alter table ALTER TABLE inventory ADD COLUMN last_checked datetime
Qué significa: Tienes esperas de bloqueo de fila y un DDL en curso. Ese DDL puede afectar la concurrencia según el algoritmo y los metadata locks.
Decisión: Si estás en modo incidente, considera matar el DDL o moverlo fuera de pico, y arregla el comportamiento transaccional en las actualizaciones de inventory.
Tarea 16: Comprueba la presión del buffer pool de InnoDB (I/O vs memoria)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name | Value |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests | 987654321 |
| Innodb_buffer_pool_reads | 12345678 |
+---------------------------------------+------------+
Qué significa: Las lecturas desde disco son significativas. Una tasa alta de Innodb_buffer_pool_reads indica misses de caché y coste de I/O.
Decisión: Si el buffer pool es demasiado pequeño, aumenta su tamaño (con cuidado). Pero primero elimina consultas que generan escaneos; si no, solo cachearás tus errores.
Leer logs lentos como un SRE, no como un adivino
Una entrada de slow log es una pequeña historia: quién la ejecutó, cuánto tardó, qué devolvió y cuánto examinó. El truco es entender qué parte de esa historia es accionable.
Campos que importan (y qué intentan decirte)
- Query_time: tiempo de reloj de pared. Incluye esperas de CPU, I/O, bloqueos y a veces retrasos de scheduling. Es tu síntoma, no tu diagnóstico.
- Lock_time: tiempo esperando bloqueos de tabla/fila (según el motor). Si es grande, la “consulta lenta” puede ser una SQL rápida atrapada detrás de una transacción más lenta.
- Rows_examined: cuánto trabajo se hizo. Muchas filas examinadas con pocas filas enviadas es una firma común de índice faltante.
- Rows_sent: cuánto dato devolviste. Si es enorme, la consulta puede “hacer lo que se diseñó” pero el diseño está mal (endpoints chatos, exportaciones gigantes).
- User@Host: identifica la fuente de la carga. A menudo te dice qué servicio debe arreglarlo.
- Timestamp: te permite correlacionar con despliegues, cron jobs o picos de tráfico.
Los tres arquetipos de “que te está matando en silencio”
Los verás una y otra vez:
- Alta frecuencia, tiempo moderado: una consulta que no es terrible pero se ejecuta constantemente. Arreglarla mejora CPU y latencia tail. Mejor ROI.
- Baja frecuencia, gran tiempo: informes, jobs por lotes y “una gran consulta”. A menudo aceptable si está planificada; inaceptable si la disparan usuarios.
- Dominado por bloqueos: consultas que parecen lentas porque fueron bloqueadas. Aquí arreglas el alcance de transacciones, el comportamiento de aislamiento o hotspots de datos.
Dónde se engañan los operadores
El slow log es literal. Registra que una consulta tardó 1.2 segundos. No registra por qué. El porqué puede ser:
- un pico de latencia del sistema de ficheros,
- un flush de checkpoint,
- contención de mutex,
- saturación de I/O en la réplica,
- o una sesión que mantiene una transacción larga y luego se fue a tomar un café.
Ese último caso pasa más a menudo de lo que cualquiera quiere admitir.
Broma 2: Una transacción larga es como dejar el microondas de la oficina encendido—todos lo notan, nadie sabe quién lo hizo.
Herramientas: mysqldumpslow, pt-query-digest y aliados
Puedes hacer mucho con solo grep y paciencia, pero tienes cosas mejores que hacer que normalizar SQL manualmente. Usa las herramientas. Sabe para qué sirven.
mysqldumpslow: rápido y sucio
Viene con MySQL y sirve para un primer corte. Agrupa consultas por una normalización cruda (reemplazando números y cadenas). Está bien para “¿quién es el principal culpable ahora?”. No es ideal para análisis profundos a través de muchas fuentes o variantes complejas de sentencias.
pt-query-digest: triaje de nivel producción
El digest de Percona Toolkit es el caballo de batalla. Fingerprinta consultas, agrega por tiempo total y varianza, e imprime muestras representativas. Tiene opinión, y es buena: te empuja a enfocarte en lo que importa.
Si tienes un entorno de producción estable, considera un job programado que digiera slow logs y guarde resúmenes. No porque ames los informes, sino porque los incidentes son más cortos cuando ya conoces a los sospechosos habituales.
Cuando Performance Schema ayuda
El slow log es reactivo: te dice sobre consultas que cruzaron un umbral. Performance Schema puede ser proactivo: puede mostrar las sentencias top por tiempo total incluso si individualmente “no son lentas”.
Pero si ya estás en dolor y no conoces tu sobrecarga actual, no empieces a activar una docena de switches de instrumentación a mitad de incidente. Tu trabajo es restaurar el servicio primero, no convertirte en una plataforma de telemetría de una sola persona.
Modos de fallo: cuando los logs lentos mienten (o al menos confunden)
El registro de consultas lentas no está equivocado. Simplemente es incompleto. Aquí las formas comunes en que te lleva a perseguir la liebre equivocada.
1) Umbral demasiado alto, y el verdadero asesino nunca aparece
Si long_query_time es 2 segundos, te perderás la consulta de 150 ms que se ejecuta 1.000 veces por segundo. Esa consulta puede saturar tu CPU sin ser “lenta”.
Solución: baja temporalmente long_query_time durante la investigación o usa Performance Schema para ordenar por tiempo total.
2) Umbral demasiado bajo, y te ahogas en ruido
Si pones long_query_time en 0.01 en un sistema ocupado, podrías generar logs más rápido de lo que puedes escribirlos. Felicidades, acabas de inventar un DoS basado en logs.
Solución: muestrea en ventanas cortas. Rota logs. Analiza porciones pequeñas. Sube el umbral después.
3) Las esperas por bloqueos hacen que consultas inocentes parezcan culpables
Un simple UPDATE puede mostrar Query_time: 2.0 con Lock_time: 1.9. El SQL no es lento; el modelo de concurrencia está roto.
Solución: encuentra al bloqueador (processlist, InnoDB lock waits), acorta transacciones, reduce filas calientes o cambia patrones de escritura.
4) La consulta lenta es síntoma de un colapso de I/O, no la causa
Cuando la latencia de almacenamiento sube, todo se ralentiza. El log mostrará un desfile de consultas “lentas” y culparás a la equivocada.
Solución: confirma la salud y latencia del almacenamiento. Busca misses de buffer pool y escaneos; luego revisa la plataforma (disco, RAID, virtualización, vecinos ruidosos).
5) La ubicación del log no es escribible, y obtienes silencio
MySQL no siempre puede escribir donde le dijiste, especialmente con perfiles AppArmor. Pensarás que no tienes consultas lentas. Sí las tienes. Sólo que no hay log.
Solución: asegura que el archivo exista, la propiedad sea correcta y revisa el log de errores de MySQL por errores de apertura de archivo.
Errores comunes: síntoma → causa raíz → solución
1) “La CPU está al máximo, pero el slow log está vacío”
Síntoma: Alta CPU, alto QPS, usuarios se quejan, el slow log tiene poco o nada.
Causa raíz: long_query_time demasiado alto; tu asesino es “rápido” pero frecuente.
Solución: baja temporalmente long_query_time (p. ej., 0.1–0.2), rota logs y/o usa digests de Performance Schema para ordenar por tiempo total.
2) “Todo está lento después de habilitar log_queries_not_using_indexes”
Síntoma: Disco ocupado, volumen de logs explota, MySQL se siente peor.
Causa raíz: Registraste cada pequeño escaneo en tablas pequeñas y creaste amplificación de escritura.
Solución: apágalo, luego usa digests para identificar los pocos escaneos con alto Rows_examined o alto tiempo total.
3) “Agregamos un índice y el rendimiento empeoró”
Síntoma: Las escrituras se ralentizan, la replicación se atrasa, el buffer pool churn aumenta.
Causa raíz: El nuevo índice aumentó el coste de escritura y la huella de memoria; optimizaste una lectura sin considerar el volumen de escritura.
Solución: valida el beneficio de lectura frente al coste de escritura. Considera un índice covering más estrecho, reduce el ancho del índice o rediseña la consulta para evitar necesitarlo.
4) “Las consultas lentas son todas el mismo SELECT, pero solo a veces”
Síntoma: La misma sentencia aparece con Query_time variable.
Causa raíz: Los valores de parámetros producen distinta selectividad; el plan es inestable; las estadísticas están obsoletas; o estás viendo picos de contención por bloqueos.
Solución: compara Rows_examined entre muestras, ejecuta EXPLAIN con parámetros representativos, actualiza estadísticas e investiga bloqueos si Lock_time sube.
5) “La consulta más lenta es un informe; el sitio se está muriendo”
Síntoma: Una gran consulta de reporting domina el slow log, la latencia de la app se dispara.
Causa raíz: Recursos compartidos. El informe está saturando I/O o buffer pool, dañando la carga OLTP.
Solución: mueve el informe a una réplica, prográmalo fuera de pico, añade aislamiento de recursos o pre-agrega. No “optimices” añadiendo índices al azar a tablas OLTP en producción primero.
6) “Las consultas se pusieron más lentas justo después de una migración de esquema”
Síntoma: Aumenta Lock_time y estados de espera; el slow log muestra sentencias admin.
Causa raíz: El DDL provocó metadata locks o reconstrucciones grandes de tabla; la migración chocó con tráfico pico.
Solución: usa estrategias de cambio de esquema online, ejecuta migraciones en ventanas controladas y registra slow admin statements para poder probar causalidad la próxima vez.
Tres mini-historias corporativas desde la trinchera
Incidente: la suposición equivocada de que “las réplicas no importan”
Un equipo ejecutaba una base de datos transaccional ocupada con un primario y dos réplicas. Las réplicas eran “para lecturas y backups”, y esa frase se volvió una excusa para ignorar por completo el rendimiento de las réplicas. El primario se monitorizaba obsesivamente; las réplicas eran tratadas como repuestos fríos.
Luego se lanzó una nueva característica: la aplicación empezó a enrutar un subconjunto de lecturas de perfiles de usuario a las réplicas. No todas las lecturas. Solo algunas. El despliegue fue gradual, lo que lo hizo más difícil: nadie vio un cliff obvio. Lo que la gente vio fue un aumento lento en la latencia p95 y un puñado de “timeouts aleatorios” que no correlacionaban con las métricas del primario.
El slow log en el primario parecía aburrido. En las réplicas, estaba gritando. Un SELECT particular con un ORDER BY inocente corría justo por debajo del umbral lento en el primario gracias a cache warm, pero cruzaba el umbral constantemente en las réplicas porque sus buffer pools estaban fríos y sus discos eran más lentos.
La suposición equivocada fue que una consulta que “funciona en el primario” debe estar bien en todos lados. No lo estaba. Las réplicas tenían características de I/O distintas, forma de carga distinta y un job de backup que picos de lectura cada hora. Los slow logs lo demostraron: mismo fingerprint de consulta, distribución de Query_time distinta.
La solución no fue heroica: añadir el índice compuesto faltante, aumentar el buffer pool de las réplicas y programar backups alejados del nuevo patrón de tráfico. La lección quedó: las réplicas también son producción, solo con menos excusas.
Optimización que salió mal: el índice que devoró la ruta de escritura
Un servicio cercano a finanzas tenía una tabla que registraba entradas de ledger. Las lecturas eran frecuentes, pero las escrituras eran constantes. Un ingeniero vio un registro lento para una consulta que filtraba por (account_id, created_at) y hizo lo “obvio”: añadió un índice compuesto grande que además incluía una columna varchar amplia para “cubrir” completamente la consulta.
La lectura mejoró. Fue hermoso en staging. En producción, la ruta de escrituras empezó a tambalear. La latencia de inserts aumentó, la replicación se retrasó y el buffer pool se volvió menos efectivo. El slow log empezó a mostrar consultas distintas: no el SELECT original, sino inserts y updates que ahora tardaban más porque cada escritura tenía que mantener un índice más pesado.
El equipo pasó un día discutiendo si el índice “debería” haber ayudado. Ayudó a la lectura. También cambió la economía de toda la tabla. InnoDB no te da índices gratis; te los cobra en amplificación de escritura y memoria, cada vez.
La solución fue quirúrgica. Reemplazaron el índice amplio covering por un índice compuesto más estrecho, luego reescribieron la consulta para traer solo las columnas necesarias. Eso redujo el bloat de índices y restauró el throughput de escritura. También añadieron una regla: las propuestas de índice requieren discusión explícita del coste de escritura y un plan de rollback.
Aburrido pero correcto: registro por ventana de tiempo y rotación que salvó el día
Una plataforma SaaS tenía un problema recurrente: una vez cada pocos días, el sistema “se sentía lento” durante unos diez minutos. Nada dramático, solo suficiente para disparar tickets de soporte. Era el tipo de problema que hace perder semanas porque es intermitente y todos tienen una teoría.
El ingeniero de guardia hizo algo poco glamuroso: añadió un paso al playbook para flush y rotar el slow log en cuanto empezara el incidente, y luego capturar exactamente 10 minutos de datos. No “el archivo de anoche”, no “tal vez fue alrededor de las 3”. Una ventana nítida.
Tras dos incidentes, emergió el patrón. No era una sola consulta; era un job por lotes que corría cada pocas horas y generaba una oleada de consultas moderadas. Individualmente estaban bien. Juntas saturaban I/O y empujaban el churn del buffer pool al límite. El slow log, cuando se cortaba a la ventana adecuada, mostró un conjunto consistente de fingerprints apareciendo solo durante el evento.
La solución fue igualmente aburrida: mover la carga por lotes a una réplica y añadir limitación de tasa al job. Los tickets de soporte bajaron. Nadie recibió un trofeo. La producción se tranquilizó, que es lo más cercano al romance que tienen los SREs.
Listas de verificación / plan paso a paso
Cuando estás en un incidente (30–60 minutos)
- Clasifica el cuello de botella: CPU, I/O o bloqueos. No adivines; comprueba.
- Confirma que el slow log está habilitado y escribible: si no escribe, arregla eso primero.
- Establece un umbral de triaje:
long_query_timealrededor de 0.1–0.5 según la carga. - Flush/rota logs: aísla una ventana limpia durante el dolor.
- Digestiona y ordena: usa
pt-query-digestsi está disponible; si no,mysqldumpslow. - Elige al principal culpable por tiempo total: salvo que el tiempo de bloqueo indique un problema de bloqueo.
- EXPLAIN con parámetros reales: confirma escaneo vs uso de índice, filesort, tablas temporales.
- Arregla mínimamente: añade o ajusta un índice, o reescribe una ruta de consulta. No refactors universales.
- Vuelve a medir: rota el log otra vez, compara los principales culpables antes/después.
- Documenta el fingerprint: el patrón de consulta normalizado es lo que importa para recurrencias.
Después del incidente (al día siguiente)
- Establece configuración persistente: usa archivo de configuración o
SET PERSISTpara no perder ajustes en reinicios. - Decide la retención de logs: los slow logs crecen; rota con logrotate, envía a almacenamiento central si hace falta.
- Establece umbrales alineados con SLO: elige
long_query_timeque capture latencia visible al usuario, no ruido. - Baslinea fingerprints de consultas top: tus top 20 “normales” es un poderoso sistema de alerta temprana.
- Construye un hábito de revisión de índices: cada nuevo índice tiene coste de escritura y memoria; trátalo como planificación de capacidad.
Guardarraíles operativos (para evitar autolesiones futuras)
- Nunca habilites “registrar todo” permanentemente en un primario ocupado.
- Mantén slow logs en disco local con latencia predecible, no en un sistema de archivos de red.
- Haz las migraciones de esquema observables: registra slow admin statements y rastrea esperas de metadata lock.
- Enseña a los equipos a leer
Rows_examinedcomo una factura: si es grande, alguien paga.
Preguntas frecuentes
1) ¿Debo registrar en archivo o en tabla?
Archivo, para la mayoría de sistemas de producción. Registrar en tabla puede crear contención, inflar la carga del diccionario de datos y complicar la retención. Usa logging en tabla solo si tienes un pipeline controlado y has probado la sobrecarga.
2) ¿Cuál es un buen valor para long_query_time?
Empieza alrededor de 0.2–0.5 segundos para servicios OLTP, luego ajusta según volumen y SLOs. Durante la investigación, bájalo brevemente para capturar más muestras. No lo pongas a 0 a menos que disfrutes llenar discos.
3) ¿Por qué veo alto Rows_examined pero bajo Rows_sent?
Usualmente es un índice faltante o mal emparejado, o una consulta que no puede usar el índice por funciones, casts implícitos o mal orden de predicados. Confirma con EXPLAIN; busca type: ALL o Using filesort.
4) El slow log muestra un UPDATE con tiempo enorme, pero EXPLAIN parece bien. ¿Y ahora?
Revisa Lock_time. Si es grande, tu UPDATE fue bloqueado. Encuentra la transacción bloqueadora, acorta el alcance de transacciones y evita diseños con filas calientes donde muchas sesiones actualizan la misma fila(s).
5) ¿Habilitar el slow query log perjudicará el rendimiento?
Algo de sobrecarga, sí—principalmente por escribir logs. Con umbrales razonables y logging a archivo local, suele ser aceptable. El verdadero riesgo viene de ajustes demasiado agresivos y volumen masivo de logs.
6) ¿Cómo atrapo consultas “no lentas pero demasiado frecuentes”?
Baja temporalmente long_query_time y digiere por tiempo total, o usa resúmenes de sentencias de Performance Schema para ordenar por latencia total. El slow log por sí solo está diseñado para basarse en umbrales.
7) ¿Por qué mis slow logs están vacíos aunque slow_query_log=ON?
Causas comunes: ruta de archivo no escribible, AppArmor denegando escrituras, umbral demasiado alto, o la carga está dominada por consultas rápidas y esperas de bloqueo que no cruzan tu umbral. Revisa logs de errores por problemas de archivo y valida la ruta activa con SHOW VARIABLES.
8) ¿Puedo usar slow logs para encontrar índices faltantes automáticamente?
Puedes obtener pistas fuertes, no certezas. Alto Rows_examined y Using filesort apuntan a oportunidades de índice. Pero el diseño de índices requiere entender mezcla lectura/escritura, cardinalidad y patrones de consulta. El slow log te dice dónde mirar; aún tienes que pensar.
9) ¿Y las réplicas—deberían tener ajustes de slow log distintos?
A menudo sí. Las réplicas pueden usarse para logging más intensivo e instrumentación más profunda porque no son el cuello de botella de escritura primario. Pero si tu aplicación lee desde réplicas, trátalas como producción de primera clase y móntoralas igual.
10) ¿Cuánto tiempo debo guardar los slow logs?
Conserva lo suficiente para cubrir investigación de incidentes y detección de regresiones—comúnmente días hasta un par de semanas, según volumen. Rota agresivamente y digiere resúmenes si necesitas tendencias más largas sin almacenamiento masivo.
Conclusión: qué hacer mañana por la mañana
Si tu servidor MySQL en Debian 13 parece morirse en silencio, deja de adivinar. Habilita el slow query log con un umbral sensato, rota para capturar una ventana limpia, digiere por tiempo total y arregla al principal culpable con pruebas: EXPLAIN, cambios de índice que coincidan con los patrones de acceso y validación posterior al cambio.
Luego hazlo rutinario. Mantén el logging lento disponible, ejecuta digests durante incidentes y trata los cambios de índice como cambios de producción—con pensamiento de rollback y conciencia del coste de escritura. La “consulta asesina silenciosa” rara vez es ingeniosa. Por lo general es aburrida, repetida y cara. Exactamente por eso se sale con la suya.