MySQL vs MariaDB: registro de consultas lentas — convierte una hora de registros en un acelerón de 2×

¿Te fue útil?

Hay dos tipos de “problemas de rendimiento” en bases de datos: los que puedes reproducir y los que desaparecen en cuanto abres un ticket. El registro de consultas lentas convierte el segundo tipo en el primero, porque registra lo que realmente ocurrió, no lo que alguien jura que ocurrió.

Si puedes dedicar una hora enfocada al slow log y salir con un acelerón de 2×, normalmente no es porque seas un mago. Es porque producción ha estado haciendo la misma cosa cara y tonta todo el día, y nadie la hizo visible.

MySQL vs MariaDB: qué difiere realmente en los registros lentos

En teoría, MySQL y MariaDB ofrecen ambos un registro de consultas lentas, y lo habilitas de la misma manera: fijas un umbral, registras a los infractores y luego analizas. En la práctica, los detalles importan porque la “misma” consulta puede aparecer con tiempos distintos, ruido distinto y visibilidad distinta según los valores por defecto del motor, la instrumentación y el comportamiento del optimizador.

Qué es igual

  • Semántica del slow query log: un registro de sentencias que exceden un umbral de tiempo (o que cumplen otros criterios como “no usan índices” si eliges ese camino).
  • Controles principales: slow_query_log, slow_query_log_file, long_query_time, log_queries_not_using_indexes (con matices), log_slow_admin_statements.
  • Enfoque de análisis: agrupar por fingerprint, ordenar por tiempo total y luego perseguir a los peores infractores con EXPLAIN/EXPLAIN ANALYZE, cambios de esquema/índices y reescrituras de consultas.

Qué difiere lo suficiente como para tropezarte

1) Las diferencias de optimizador y ejecución aparecen como “top queries” distintas en el slow-log. MariaDB y MySQL divergieron con el tiempo: estrategias del optimizador, manejo de tablas derivadas y valores por defecto de características pueden llevar a planes distintos. Eso significa que no puedes aplicar a ciegas “la solución que usamos en MySQL” a MariaDB (o viceversa) sin validar planes y estimaciones de filas.

2) Las rutas de instrumentación difieren. MySQL 8 se apoya mucho en Performance Schema y las vistas sys para análisis. MariaDB también tiene Performance Schema, pero en algunas implementaciones está deshabilitado o se usa menos; MariaDB además tiene sus propios auxiliares de diagnóstico. En cualquier caso, el slow log es la verdad base, pero la telemetría de apoyo puede diferir.

3) Los valores por defecto de versión importan más que la marca. Un servidor MySQL 5.7 y uno MySQL 8.0 pueden comportarse más diferente entre sí que MySQL vs MariaDB en tu cabeza. Lo mismo para MariaDB 10.3 vs 10.11. Para los slow logs, lo importante es: ¿tienes marcas de tiempo precisas, capturas suficiente contexto y puedes conectar una sentencia lenta con cuellos de botella de recursos?

Aquí está la incómoda verdad de producción: la base de datos que tienes es la que debes tunear. Parte desde los registros, no desde la ideología.

Guion de diagnóstico rápido: encuentra el cuello de botella antes de que te encuentre a ti

Este es el flujo “tengo 20 minutos antes de la próxima llamada por el incidente”. No es exhaustivo. Está diseñado para encontrar rápidamente el cuello de botella dominante y evitar que persigas fantasmas.

Primero: confirma que el problema es tiempo de consulta, no tiempo de conexión

  • Verifica si las peticiones son lentas porque las consultas son lentas, o porque los hilos están saturados, o porque esperas por bloqueos.
  • Busca síntomas: aumento de “Threads_running”, “Questions” estabilizándose mientras la latencia sube, picos en “Innodb_row_lock_time” o muchas entradas “Sending data” en processlist.

Segundo: identifica al máximo infractor por tiempo total, no por la peor consulta única

  • Una consulta que toma 1.2s una vez por hora no es tu incidente. Una consulta que toma 80ms y se ejecuta 50k veces sí lo es.
  • Ordena por suma de Query_time en el informe del slow log, luego por contador.

Tercero: clasifica el cuello de botella

Usa el patrón del infractor para colocarlo en una categoría:

  • CPU/optimizador: muchas filas examinadas, pobre uso de índices, mal orden de joins, filesort, tablas temporales.
  • I/O: fallos de buffer pool, amplificación de lectura, tablas temporales en disco, latencia de almacenamiento lenta.
  • Bloqueos: alto tiempo de lock, “Waiting for … lock”, transacciones largas, filas calientes.
  • Comportamiento de la aplicación: consultas N+1, patrones chatty de ORM, sin paginación, “SELECT *” en bucles furiosos.

Cuarto: elige la solución de menor riesgo y mayor impacto

  • Crea o ajusta un índice que coincida con el patrón WHERE + JOIN + ORDER BY.
  • Reescribe una consulta para evitar escaneos, reducir el ancho de fila o preagregar.
  • Reduce el alcance de los locks (transacciones más cortas, orden de acceso consistente, mejores opciones de aislamiento).
  • Sólo entonces considera cambios de configuración. Las correcciones de configuración son reales, pero también son las más fáciles de copiar sin entender.

Idea parafraseada (atribución): Gene Kim enfatiza a menudo que la mejora viene de hacer el trabajo visible y reducir el coste de aprender desde producción.

Flujo de trabajo de una hora con slow log que consigue mejoras reales

Una hora es suficiente para conseguir un acelerón de 2× si haces menos “análisis” y más “triaje”. El objetivo no es comprensión perfecta. El objetivo es una clase de consultas dominante arreglada de forma segura.

Minuto 0–10: asegúrate de que estás registrando lo correcto

Empieza confirmando que el slow log está habilitado, el umbral es sensato y el formato de registro es analizables. Si el umbral es demasiado alto, no verás las muertes por miles de cortes. Si es demasiado bajo, te ahogarás en ruido y tu disco protestará.

Umbrales recomendados iniciales en producción:

  • long_query_time: 0.1–0.5s para OLTP, 1–2s para cargas mixtas. Si ya estás en llamas, empieza en 0.2s y ajusta.
  • log_queries_not_using_indexes: normalmente apagado al principio. Produce muchos falsos positivos (tablas pequeñas, escaneos legítimos). Enciéndelo brevemente si sabes lo que haces.
  • log_slow_admin_statements: activado, porque “ALTER TABLE” puede ser el asesino silencioso.

Minuto 10–25: genera un informe top-N y elige una víctima

Genera un informe digest (pt-query-digest es la herramienta fundamental). Ordena por tiempo total de consulta. Elige la clase de consulta superior que sea (a) frecuente, (b) cara, (c) arreglable sin reescribir media aplicación.

Aquí es donde la gente se sabotea: eligen la consulta más compleja porque parece “importante”. Quieres la consulta aburrida que se ejecuta constantemente.

Minuto 25–45: valida con EXPLAIN ANALYZE y aplica un índice o reescritura dirigida

Toma una muestra representativa de la consulta (no el peor outlier) y ejecuta EXPLAIN y, si está disponible, EXPLAIN ANALYZE. Buscas la discrepancia entre lo que crees que pasa y lo que realmente pasa: escaneos de tabla, orden de joins equivocado, filas examinadas que explotan, tablas temporales, filesorts o uso del índice equivocado.

Luego haz exactamente un cambio:

  • Crea un índice (o ajusta el orden de un índice compuesto).
  • Reescribe un predicado para que sea sargable, p. ej., evita envolver columnas indexadas en funciones.
  • Añade LIMIT/estrategia de paginación, o divide una consulta en un prefiltro barato más un join.

Minuto 45–60: demuestra que mejoró y que no rompiste nada

Vuelve a ejecutar EXPLAIN ANALYZE. Compara el slow log antes/después (incluso una ventana breve ayuda). Observa la latencia p95 y los contadores de CPU/IO. Si la consulta se hizo más rápida pero el sistema empeoró, probablemente empujaste la carga a otro sitio (bloqueos, tablas temporales, replicación, disco).

broma #1: El slow query log es el único compañero de trabajo que recuerda lo que pasó anoche, y nunca “se olvida” de escribirlo.

Tareas prácticas (comandos, salidas, decisiones)

A continuación hay tareas prácticas y ejecutables. Cada una incluye: un comando, un ejemplo de salida, qué significa y qué decisión tomar. Úsalas como checklist, no como ritual. El objetivo es reducir el tiempo hasta la verdad.

Tarea 1: Confirma que el slow log está habilitado y dónde escribe

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'slow_query_log_file'; SHOW VARIABLES LIKE 'long_query_time';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| slow_query_log| ON    |
+---------------+-------+
+---------------------+---------------------------+
| Variable_name       | Value                     |
+---------------------+---------------------------+
| slow_query_log_file | /var/log/mysql/slow.log   |
+---------------------+---------------------------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| long_query_time | 0.200 |
+-----------------+-------+

Qué significa: El registro está activo, el umbral es 200ms, la ruta del archivo está conocida.

Decisión: Si slow_query_log está OFF, actívalo (temporalmente si es necesario). Si long_query_time es 10s, no estás observando tu carga real.

Tarea 2: Habilitar slow log dinámicamente (seguro, reversible)

cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.2; SET GLOBAL log_slow_admin_statements = 'ON';"

Qué significa: Has habilitado la captura sin reiniciar.

Decisión: Haz esto durante un incidente si te faltan datos. Pero también programa un cambio en la configuración para que persista tras reinicios.

Tarea 3: Confirma que no estás registrando todo por accidente (chequeo del crecimiento del archivo)

cr0x@server:~$ sudo ls -lh /var/log/mysql/slow.log
-rw-r----- 1 mysql adm 1.8G Dec 29 10:12 /var/log/mysql/slow.log

Qué significa: El archivo es grande. Puede estar bien. Puede estar consumiendo disco.

Decisión: Si crece demasiado rápido, sube long_query_time ligeramente o muestrea durante el pico, luego rota.

Tarea 4: Rotar el slow log sin reiniciar mysqld

cr0x@server:~$ mysql -e "FLUSH SLOW LOGS;"

Qué significa: MySQL/MariaDB cierra y reabre el archivo del slow log, permitiendo que logrotate haga su trabajo.

Decisión: Úsalo antes de empezar una ventana de captura de una hora para que tu dataset esté “limpio”.

Tarea 5: Rápida revisión: principales infractores con mysqldumpslow

cr0x@server:~$ mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
Reading mysql slow query log from /var/log/mysql/slow.log
Count: 842  Time=0.38s (319s)  Lock=0.00s (0s)  Rows=1.0 (842), root[root]@10.0.2.15
  SELECT * FROM orders WHERE user_id=N AND status='open' ORDER BY created_at DESC LIMIT N
Count: 55  Time=2.12s (116s)  Lock=0.01s (1s)  Rows=30000.0 (1650000), app[app]@10.0.3.9
  SELECT customer_id, SUM(amount) FROM invoices WHERE created_at >= 'S' GROUP BY customer_id

Qué significa: La primera consulta es frecuente y moderadamente lenta; la segunda es pesada pero menos frecuente. El tiempo total importa: 319s vs 116s en esta muestra.

Decisión: Investiga primero la que está arriba por tiempo total. La consulta con 842 llamadas es la probable ganadora 2×.

Tarea 6: Agrupado y ranking correcto con pt-query-digest

cr0x@server:~$ pt-query-digest /var/log/mysql/slow.log | sed -n '1,120p'
# 320s total, 842 queries, 0.38s avg, 0.05s 95%  0.89s max
# Rank Query ID           Response time Calls R/Call V/M   Item
# ==== ================== ============= ===== ====== ===== ============
#    1 0x8F3A1C9E6B0A2C11  319.1240  99.7%   842 0.3790  1.02 SELECT orders
#    2 0xB11D0E2A6F9C0D22  116.6042  36.4%    55 2.1201  1.40 SELECT invoices
# ...

Qué significa: La consulta #1 domina el tiempo de reloj. Arreglarla te compra la mayor ganancia.

Decisión: Extrae la muestra exacta de consulta de la sección de pt-query-digest y llévala a EXPLAIN.

Tarea 7: Encuentra la situación exacta de tabla/índice (SHOW CREATE TABLE)

cr0x@server:~$ mysql -e "SHOW CREATE TABLE orders\G"
*************************** 1. row ***************************
       Table: orders
Create Table: CREATE TABLE `orders` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL,
  `status` varchar(16) NOT NULL,
  `created_at` datetime NOT NULL,
  `total` decimal(10,2) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_status` (`status`),
  KEY `idx_user` (`user_id`)
) ENGINE=InnoDB

Qué significa: No hay un índice compuesto que coincida con (user_id, status, created_at). Ordenar por created_at probablemente causa trabajo extra.

Decisión: Añade un índice compuesto alineado con WHERE y ORDER BY.

Tarea 8: EXPLAIN ANALYZE de la consulta lenta (chequeo de la realidad)

cr0x@server:~$ mysql -e "EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id=123 AND status='open' ORDER BY created_at DESC LIMIT 20\G"
*************************** 1. row ***************************
EXPLAIN: -> Limit: 20 row(s)  (actual time=210.112..210.140 rows=20 loops=1)
    -> Sort: orders.created_at DESC  (actual time=210.109..210.114 rows=3200 loops=1)
        -> Index lookup on orders using idx_user (user_id=123)  (actual time=0.210..205.900 rows=3200 loops=1)

Qué significa: Usa idx_user y luego ordena 3200 filas para escoger 20. Ahí es donde va el tiempo.

Decisión: Añade un índice que satisfaga filtrado y orden para que el motor pueda salir temprano.

Tarea 9: Crea el índice de forma segura (y entiende qué significa “online”)

cr0x@server:~$ mysql -e "ALTER TABLE orders ADD INDEX idx_user_status_created (user_id, status, created_at DESC);"
Query OK, 0 rows affected (12.41 sec)
Records: 0  Duplicates: 0  Warnings: 0

Qué significa: Índice creado. El tiempo y el comportamiento de bloqueo dependen de la versión y la configuración; algunas compilaciones lo hacen “in place”, pero aún consume IO y puede pararse en locks de metadatos.

Decisión: Ejecuta durante una ventana de bajo tráfico si la tabla es grande. Monitoriza esperas por metadata lock.

Tarea 10: Vuelve a ejecutar EXPLAIN ANALYZE para verificar salida temprana

cr0x@server:~$ mysql -e "EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id=123 AND status='open' ORDER BY created_at DESC LIMIT 20\G"
*************************** 1. row ***************************
EXPLAIN: -> Limit: 20 row(s)  (actual time=1.432..1.455 rows=20 loops=1)
    -> Index lookup on orders using idx_user_status_created (user_id=123, status='open')  (actual time=0.210..1.410 rows=20 loops=1)

Qué significa: No hay paso de ordenamiento, sólo 20 filas tocadas. Así recuperas latencia.

Decisión: Si el plan no cambió, investiga por qué (orden de índice incorrecto, desajuste de collation/tipo, o preferencia del optimizador).

Tarea 11: Revisa la contención de locks en el slow log y contadores en tiempo de ejecución

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_time%'; SHOW GLOBAL STATUS LIKE 'Threads_running';"
+----------------------------+--------+
| Variable_name              | Value  |
+----------------------------+--------+
| Innodb_row_lock_time       | 184223 |
| Innodb_row_lock_time_max   | 12000  |
| Innodb_row_lock_waits      | 912    |
+----------------------------+--------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Threads_running | 48    |
+-----------------+-------+

Qué significa: Hay esperas significativas por locks. Threads_running está alto, indicando presión de concurrencia.

Decisión: Si el tiempo de lock es una gran parte de Query_time en las entradas del slow log, céntrate en el alcance de las transacciones y filas calientes, no en índices.

Tarea 12: Inspecciona consultas en vivo y detecta rápidamente “Waiting for … lock”

cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;" | sed -n '1,18p'
Id	User	Host	db	Command	Time	State	Info
1209	app	10.0.3.9:53122	prod	Query	12	Waiting for table metadata lock	ALTER TABLE orders ADD COLUMN note varchar(64)
1211	app	10.0.3.7:60111	prod	Query	9	Sending data	SELECT * FROM orders WHERE user_id=123 AND status='open' ORDER BY created_at DESC LIMIT 20
1220	app	10.0.3.6:42290	prod	Query	8	Sending data	SELECT * FROM orders WHERE user_id=456 AND status='open' ORDER BY created_at DESC LIMIT 20

Qué significa: Un ALTER TABLE está bloqueando. Eso no es “tunning de consultas”; es gestión de cambios.

Decisión: Para el DDL (si es seguro) o reprográmalo. Evita repeticiones con prácticas disciplinadas de migración.

Tarea 13: Revisa la salud del buffer pool de InnoDB (¿leemos desde disco?)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+-----------+
| Variable_name                         | Value     |
+---------------------------------------+-----------+
| Innodb_buffer_pool_read_requests      | 982341234 |
| Innodb_buffer_pool_reads              | 19348231  |
+---------------------------------------+-----------+

Qué significa: Las lecturas desde disco son no triviales. La proporción importa más que el número bruto; muchas lecturas de disco pueden estar bien en conjuntos de trabajo grandes, pero también pueden significar que tu buffer pool es pequeño o que tus consultas están escaneando.

Decisión: Si el conjunto de trabajo cabe en memoria pero sigues leyendo mucho, probablemente tengas índices pobres o patrones de acceso inadecuados. Si el conjunto de trabajo supera realmente la memoria, prioriza los escaneos más grandes y considera dimensionar la memoria.

Tarea 14: Revisa tablas temporales y si vuelcan a disco

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables';"
+-------------------------+--------+
| Variable_name           | Value  |
+-------------------------+--------+
| Created_tmp_disk_tables | 48291  |
| Created_tmp_tables      | 90210  |
+-------------------------+--------+

Qué significa: Muchas tablas temporales están yendo a disco. Esto se correlaciona con GROUP BY/ORDER BY en columnas no indexadas, filas anchas o ajustes insuficientes de memoria para tmp tables.

Decisión: Prefiere arreglos de consulta e índices primero. Si la consulta es inherentemente pesada, ajusta tmp_table_size/max_heap_table_size con cuidado y valida el impacto en memoria.

Tarea 15: Valida la latencia de almacenamiento durante la ventana lenta (no adivines)

cr0x@server:~$ iostat -xz 1 5
Linux 6.5.0 (server) 	12/29/2025 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          22.14    0.00    7.12   18.33    0.00   52.41

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s w_await aqu-sz  %util
nvme0n1         322.0  18232.0     0.0   0.00   18.40    56.60    90.0   9120.0   4.10   6.32  78.00

Qué significa: r_await ~18ms sugiere que las lecturas de almacenamiento no son “rápidas”. Si tu carga espera lecturas de pocos milisegundos, esto contribuye directamente al tiempo de consulta cuando hay fallos de buffer pool.

Decisión: Si la latencia es alta, aún puedes conseguir un 2× reduciendo lecturas (indexación, mejor uso de LIMIT). También investiga vecinos ruidosos, degradación de RAID, picos de fsync y comportamiento de checkpoints.

Tarea 16: Revisa binlog y señales de retraso de replicación (las soluciones pueden mover el dolor)

cr0x@server:~$ mysql -e "SHOW MASTER STATUS\G; SHOW SLAVE STATUS\G" | sed -n '1,40p'
*************************** 1. row ***************************
             File: mysql-bin.002341
         Position: 91822310
     Binlog_Do_DB:
 Binlog_Ignore_DB:
Executed_Gtid_Set:

*************************** 1. row ***************************
               Slave_IO_Running: Yes
              Slave_SQL_Running: Yes
         Seconds_Behind_Master: 47

Qué significa: La replicación está atrasada. Consultas pesadas o DDL pueden empeorar el lag. Algunas “optimizaciones” aumentan la amplificación de escritura (nuevos índices) y hacen la replicación más lenta temporalmente.

Decisión: Programa construcción de índices cuidadosamente y vigila el lag. Si el lag es crítico para el negocio, considera limitar las migraciones o usar herramientas de cambio de esquema online.

Patrones de slow log que suelen darte 2×

La mayoría de las mejoras 2× no son exóticas. Son eliminar trabajo accidental. Aquí están los patrones que aparecen en slow logs en flotas MySQL y MariaDB.

1) “ORDER BY … LIMIT” sin un índice que lo soporte

Este es el clásico: filtras por algunas columnas, ordenas por timestamp, limit 20. Sin un índice compuesto que coincida filtro + orden, el motor lee muchas filas y las ordena. Tu slow log muestra tiempo moderado por llamada, gran volumen de llamadas y Rows_examined mucho mayor que Rows_sent.

Solución: Un índice compuesto con predicados de igualdad primero y luego la columna de orden. Usa índices DESC donde estén soportados y sensatos; de lo contrario el motor puede leer hacia atrás en algunos casos, pero no lo asumas. Valida con EXPLAIN ANALYZE.

2) Predicados no sargables

Si tienes WHERE DATE(created_at)=… o WHERE LOWER(email)=…, fuerzas al optimizador a una esquina. No puede usar el índice eficientemente porque envolviste la columna indexada en una función.

Solución: Reescribe a un rango (created_at >= … AND created_at < …) o almacena valores normalizados en una columna generada indexada (según versión), o normaliza en el momento de escritura.

3) Listas IN() grandes y “joins hechos por la aplicación”

Los slow logs suelen mostrar consultas como WHERE id IN (…miles…). A menudo es un patrón de ORM o un intento de batching que se descontroló. Puede causar gran sobrecarga de parseo, planes pobres y grandes estructuras temporales.

Solución: Usa una tabla temporal, un join contra una tabla real o rediseña el patrón de acceso. Si debes, limita el tamaño del lote y mide.

4) Actualizaciones de filas calientes y esperas por locks

Los problemas de bloqueo parecen “lentitud aleatoria” hasta que observas Lock_time en las entradas del slow log y ves estados de espera en processlist. Una sola fila contador caliente puede ralentizar un servicio entero.

Solución: Reduce la contención: shardea contadores, usa INSERT … ON DUPLICATE KEY en claves distribuidas o mueve la ruta caliente a algo diseñado para gran contención de escrituras. También acorta transacciones y asegura orden consistente de locks.

5) GROUP BY que vuelca a disco

Si agregas conjuntos grandes y las tablas temporales vuelcan a disco, la latencia de almacenamiento se convierte en tu latencia de consulta. El slow log muestra Query_time grande con uso de CPU relativamente bajo, y tus contadores de tmp disk tables suben.

Solución: Añade índices que soporten agregación, reduce el ancho de fila, preagrega o mueve cargas de reporting fuera del OLTP primario.

6) “SELECT *” en tablas anchas

Leer filas anchas cuesta memoria, buffer pool, red y CPU. También sabotea índices covering. Los slow logs muestran consultas que piden columnas que la aplicación ni usa.

Solución: Selecciona solo las columnas necesarias. Si quieres velocidad, deja de transportar muebles cuando solo necesitas las llaves.

Tres microhistorias corporativas desde el frente

Microhistoria 1: El incidente causado por una suposición equivocada

Una empresa mediana gestionaba una plataforma de e-commerce con un primario MySQL y un par de réplicas. El equipo de app acababa de añadir “órdenes recientes” al panel de usuario. Parecía inofensivo: filtrar por user_id, status, ordenar por created_at, limitar 20. La consulta era rápida en staging, porque staging tenía tablas pequeñas y caches calientes—como todos los entornos de staging, era una mentira reconfortante.

En producción, las latencias p95 se duplicaron en pico. El ingeniero on-call hizo lo habitual: escaló pods de app, reinició algunas cosas, y vio que empeoraba. La CPU en la BD subió, pero no al máximo. El disco no estaba saturado. Era “misterioso”.

La suposición equivocada fue simple: “LIMIT 20 significa que solo lee 20 filas.” Sin un índice compuesto que coincida, el motor leyó miles de filas por usuario, las ordenó y devolvió 20. Fue la misma historia repetida para miles de usuarios, miles de veces. El slow log mostró una consulta que no era aterradora individualmente—unos 350ms—pero se ejecutaba constantemente.

Añadieron un índice compuesto (user_id, status, created_at). La consulta cayó a unos pocos milisegundos. El incidente terminó no con una sesión de tuning heroica, sino con un índice y el recordatorio de que SQL no adivina lo que piensas.

La acción de seguimiento fue más interesante: cambiaron su checklist de despliegue para requerir la captura de una muestra de slow log de 15 minutos tras cualquier feature que añada una nueva ruta de consulta. No porque amen el proceso. Porque aman dormir.

Microhistoria 2: La optimización que salió mal

Otra organización usaba MariaDB para un sistema de facturación. Consultas de reporting estaban golpeando el primario durante horario laboral. Alguien propuso un arreglo rápido: “Activemos log_queries_not_using_indexes y añadimos índices para todo lo que aparezca.” La lógica sonaba nítida. También convirtió el slow log en una manguera de incendios.

En un día, habían creado varios índices nuevos, incluidos algunos compuestos y anchos en columnas casi no selectivas. Las escrituras se ralentizaron. El churn del buffer pool aumentó. El cambio también aumentó el lag de replicación porque cada insert/update ahora tenía que mantener más estructuras de índice. Mientras tanto, las consultas lentas no mejoraron mucho—porque los peores infractores eran escaneos completos legítimos sobre particiones mensuales para resúmenes financieros. Log_queries_not_using_indexes estaba acusando a la base de datos de hacer exactamente lo que la consulta pedía.

El revés fue sutil: el equipo pasó tiempo persiguiendo avisos de “falta de índice” en lugar de abordar la forma de la carga. La solución correcta fue mover reporting a una réplica (o a una ruta analítica dedicada), añadir algunos índices selectivos y cambiar un par de consultas para preagregar en tablas resumen.

Eventualmente revertieron varios índices, que siempre es un día divertido porque dropear índices también consume tiempo e IO. Lección aprendida: no dejes que una bandera de diagnóstico se convierta en un requisito de producto.

broma #2: Activar todas las opciones de logging “por visibilidad” es como reemplazar tu detector de humo por una máquina de niebla.

Microhistoria 3: La práctica aburrida pero correcta que salvó el día

Una compañía SaaS usaba MySQL 8 para cargas multi-tenant. Tenían una práctica que nadie presumía: el slow query log siempre estaba activado a un umbral moderado, rotado cada hora y resumido diariamente. Sin drama. No “lo activaremos cuando haga falta”. Estaba ahí, capturando la verdad en silencio.

Una tarde, la latencia subió. No catastrófica, pero notable. El on-call no adivinó. Sacó la última hora de slow log, ejecutó pt-query-digest y vio una nueva fingerprint de consulta en la cima. Era un join generado por un ORM con un predicado inesperado en una columna de baja cardinalidad, que causaba explosiones de filas.

Porque tenían resúmenes diarios base, pudieron probar que la consulta era nueva y cuantificar impacto sin especulación. Además tenían una política: cualquier cambio de índice requería un EXPLAIN ANALYZE antes/después y una rápida comprobación de lag de replicación. Esa política no es cool. Es efectiva.

Enviaron una pequeña reescritura de consulta y un índice compuesto estrecho. La latencia volvió a la normalidad. La nota post-incident fue corta: “slow log lo detectó; solución validada; sin daños colaterales.” Ese es el sueño—aburrido, correcto, repetible.

Errores comunes: síntoma → causa raíz → solución

Esta sección es intencionalmente específica. Si puedes mapear un síntoma a una causa raíz rápido, recuperas tu vida.

1) Síntoma: El slow log muestra Query_time alto pero Rows_examined bajo

Causa raíz: Espera, no escaneo—bloqueos, presión de fsync o paradas de red. A menudo Lock_time es alto, o hay picos de latencia de almacenamiento.

Solución: Revisa Lock_time en las entradas del slow log, estados de SHOW PROCESSLIST y contadores de locks de InnoDB. Para IO, correlaciona con iostat y checkpointing. No añadas índices “porque está lento”.

2) Síntoma: Consultas rápidas en réplica, lentas en primario

Causa raíz: El primario hace trabajo extra: escrituras, fsync, churn del buffer pool o contención de locks. También posible: hardware distinto o drift de configuración.

Solución: Compara variables de configuración y estadísticas del buffer pool. Valida la carga: ¿estás corriendo informes en el primario? Para de hacerlo primero.

3) Síntoma: La consulta lenta principal es un SELECT simple con ORDER BY + LIMIT

Causa raíz: Falta de índice compuesto que coincida con filtro y orden, causando filesort y escaneo.

Solución: Añade un índice con predicados de igualdad primero y luego la columna de orden. Confirma con EXPLAIN ANALYZE que el sort desaparece y las filas tocadas bajan.

4) Síntoma: Pico repentino de consultas lentas tras despliegue, pero sólo para algunos usuarios/tenants

Causa raíz: Sesgo de datos. El plan es aceptable para tenants típicos pero terrible para “los grandes”. Las estadísticas pueden engañar al optimizador.

Solución: Prueba con tamaños representativos de tenant. Considera índices mejores, cambios de consulta o dividir tenants grandes. Actualiza estadísticas y valida planes en casos sesgados.

5) Síntoma: Muchas Created_tmp_disk_tables y GROUP BY lento

Causa raíz: Tablas temporales que vuelcan a disco por índices insuficientes, filas anchas, grandes conjuntos de resultados o límites de memoria.

Solución: Reduce ancho de fila, añade índices de soporte, preagrega y solo entonces considera ajustar tmp_table_size/max_heap_table_size con presupuesto de memoria.

6) Síntoma: El archivo del slow log es enorme, el análisis es doloroso, sube el uso de disco

Causa raíz: long_query_time demasiado bajo para estar siempre activado, o una avalancha de consultas, o falta de rotación.

Solución: Implementa rotación y retención; muestrea durante el pico; ajusta el umbral. Usa pt-query-digest en una ventana temporal acotada.

7) Síntoma: Tras añadir un índice, las lecturas mejoraron pero el lag de replicación empeoró

Causa raíz: El mantenimiento adicional del índice aumentó el coste de escritura. En réplicas, aplicar eventos de fila o statement se vuelve más pesado.

Solución: Mantén índices mínimos y con propósito. Añade solo el que coincide con tu infractor principal. Para migraciones, regula y monitoriza el lag; considera programar o descargar la carga.

Listas de verificación / plan paso a paso

Checklist: capturar una hora de slow logs útil (seguro para producción)

  1. Confirma slow log habilitado y ubicación del archivo (Tarea 1).
  2. Configura long_query_time a un umbral útil para tu carga (Tarea 2).
  3. FLUSH SLOW LOGS para empezar con un archivo limpio (Tarea 4).
  4. Captura durante una hora representativa de actividad, no durante un periodo calmado.
  5. Rota al final (Tarea 4 de nuevo) para que el análisis esté acotado.
  6. Registra contexto: volumen de tráfico, versiones, migraciones en curso.

Checklist: convierte la hora en una lista de acciones priorizada

  1. Ejecuta pt-query-digest y ordena por tiempo total de respuesta (Tarea 6).
  2. Elige una fingerprint superior que sea frecuente y arreglable.
  3. Extrae esquema e índices existentes de las tablas implicadas (Tarea 7).
  4. Ejecuta EXPLAIN ANALYZE en una instancia representativa de la consulta (Tarea 8).
  5. Clasifica el cuello de botella: escaneo, ordenamiento, tablas temporales, bloqueo, IO.
  6. Propón un cambio mínimo: un índice o una reescritura.
  7. Implementa con cuidado; vigila metadata locks y lag de replicación (Tareas 12 y 16).
  8. Vuelve a ejecutar EXPLAIN ANALYZE y compara el ranking del slow log tras el cambio (Tarea 10 y digest otra vez).

Checklist: medidas de seguridad (porque te sentirás tentado)

  • No añadas índices “covering” por defecto. Los índices anchos inflan el buffer pool y ralentizan escrituras.
  • No ajustes knobs globales antes de tener una consulta identificada como infractora.
  • No confíes en el rendimiento de staging si la distribución de datos en producción difiere.
  • No ejecutes DDL en pico a menos que hayas probado el comportamiento online para tu versión y forma de tabla.

Hechos interesantes y contexto histórico

Algo de contexto ayuda porque la gente repite mitos con confianza, y la producción castiga ese hábito.

  1. MariaDB empezó como un fork de MySQL tras la adquisición de Sun por Oracle (2009). La narrativa “es lo mismo” fue cierta al principio; la divergencia es real ahora.
  2. Performance Schema de MySQL maduró significativamente en versiones mayores. MySQL moderno se apoya en él para diagnósticos; despliegues antiguos confiaban más en slow logs y herramientas externas.
  3. El slow query log antecede a la moda actual de observabilidad. Es una de las funcionalidades más antiguas de “simplemente escribe la verdad” en el ecosistema MySQL.
  4. pt-query-digest (Percona Toolkit) se volvió popular porque los slow logs son verbosos pero no inmediatamente accionables. Los digests convierten un diario en una lista de tareas ordenada.
  5. MySQL eliminó el legado Query Cache en 8.0. Fue famoso por ayudar en benchmarks y perjudicar la concurrencia real. Si alguien sugiere activarlo “para velocidad”, pregunta en qué año vive.
  6. InnoDB se convirtió en el motor por defecto hace mucho, reemplazando MyISAM en despliegues serios. Muchas soluciones a consultas lentas asumen semántica transaccional y bloqueo a nivel de fila.
  7. EXPLAIN ANALYZE es un regalo relativamente reciente comparado con EXPLAIN simple. Mide la ejecución real, lo cual es invaluable cuando las estimaciones del optimizador están equivocadas con confianza.
  8. Los metadata locks sorprenden a los equipos porque no son “consultas lentas” hasta que lo son. El DDL puede bloquear lecturas/escrituras de formas que aparecen como consultas lentas en la aplicación.

Preguntas frecuentes

1) ¿Debo usar el slow query log o Performance Schema?

Usa el slow query log como tu verdad base para “qué fue lento”. Usa Performance Schema para “por qué fue lento” (esperas, stages, señales tipo CPU). Si solo puedes elegir uno, elige primero el slow log.

2) ¿Qué long_query_time debería poner para OLTP?

Empieza en 0.2s en un sistema ocupado si quieres datos accionables rápido. Si eso es demasiado ruidoso, sube a 0.5s. Si lo pones en 2–10 segundos solo capturarás desastres, no el desperdicio crónico.

3) ¿Es buena idea log_queries_not_using_indexes?

Como ajuste permanente: normalmente no. Como experimento breve y deliberado: a veces. Señala escaneos legítimos y fomenta la creación indiscriminada de índices. Úsalo cuando ya sabes que los escaneos son accidentales, no cuando aún estás diagnosticando.

4) ¿Por qué la “misma” consulta muestra distinto rendimiento en MySQL vs MariaDB?

Diferentes decisiones del optimizador, diferentes valores por defecto, comportamiento estadístico distinto y combinaciones de motor/versión. Trata cada servidor como su propio sistema: valida planes, no confíes en la memoria tribal.

5) ¿Puede un índice hacer una consulta más lenta?

Sí. El optimizador puede elegir un índice peor, o el mantenimiento del índice puede ralentizar las escrituras lo suficiente como para que la latencia global suba. Por eso verificas con EXPLAIN ANALYZE y observas métricas a nivel sistema, no solo el tiempo de una consulta.

6) ¿Y si mi slow log está lleno de sentencias admin como ALTER TABLE?

Entonces tu cuello de botella es la gestión de cambios. Programa DDL, usa técnicas online adecuadas a tu versión y evita migraciones sorpresivas en pico. La “solución” es disciplina operativa.

7) ¿Cómo sé si mi problema es IO de disco o consultas malas?

Correlaciona: alto Innodb_buffer_pool_reads más picos de await de almacenamiento apuntan a sensibilidad IO. Pero aun así, la mejor primera solución suele ser reducir lecturas vía índices y forma de consultas. Las mejoras de hardware son válidas, pero no sustituyen parar el desperdicio.

8) ¿Cómo convierto una hora de registros en un 2× de manera fiable?

Ordena por tiempo total, elige un infractor frecuente, valida el plan con EXPLAIN ANALYZE, aplica un cambio mínimo de índice/consulta y vuelve a medir. Evita “tunear por sensaciones” (cambios múltiples simultáneos sin evidencia).

9) ¿Habilitar el slow query log perjudica el rendimiento?

Añade sobrecarga, pero para la mayoría de sistemas es manejable cuando se configura sensatamente. El riesgo mayor es el crecimiento de disco si registras demasiado sin rotación. Si el rendimiento es extremadamente sensible, muestrea en ventanas pico y rota agresivamente.

10) Mis consultas lentas muestran todas “Sending data” en processlist. ¿Ahora qué?

“Sending data” a menudo significa que la consulta está leyendo filas y procesándolas (no necesariamente enviando por red). Revisa Rows_examined, uso de índices y si estás escaneando/ordenando. También puede ocultar esperas de IO al leer páginas.

Próximos pasos que puedes hacer hoy

Haz la configuración aburrida una vez, luego recoge victorias repetidamente:

  1. Habilita el registro de consultas lentas a un umbral sensato y mantenlo activado con rotación. Hazlo parte del sistema, no una palanca de emergencia.
  2. Automatiza un digest diario (aunque sea un cron que ejecute pt-query-digest y almacene las top 20 fingerprints). Las tendencias importan.
  3. Adopta una regla de un solo cambio durante incidentes: un índice o una reescritura de consulta, validado por EXPLAIN ANALYZE y una rápida comprobación de replicación/locks.
  4. Haz hábito comparar “filas examinadas vs filas retornadas”. Es el indicador más rápido de trabajo desperdiciado.
  5. Apunta tus 5 fingerprints principales y trátalos como dependencias de producción. Porque lo son.

Si quieres el acelerón 2×, no busques ajustes mágicos. Abre el slow log, encuentra la clase de consulta dominante por tiempo total y quítale su trabajo accidental. Producción no necesita heroísmos. Necesita menos lecturas inútiles.

← Anterior
Planes de consulta en MariaDB vs PostgreSQL: detecta el verdadero cuello de botella, no los síntomas
Siguiente →
Active Directory sobre VPN: qué falla primero (DNS, hora, puertos) y cómo arreglarlo

Deja un comentario