MySQL vs PostgreSQL: «Se puso lento de repente» — plan de diagnóstico de 15 minutos para ambos

¿Te fue útil?

Son las 10:17 a.m. Un product manager escribe “el checkout está lento”. Grafana parece una escena del crimen. Abres una consola de BD y todo se siente pegajoso: consultas que antes tardaban 20 ms ahora tardan segundos, el número de conexiones sube y tu aplicación empieza a agotar tiempos de espera como si tuviera alergia al éxito.

En estos momentos, la teología (“MySQL vs Postgres”) es inútil. Necesitas una secuencia disciplinada y rápida que encuentre el cuello de botella, evite autolesiones y produzca una acción segura siguiente. Esta es esa secuencia: construida a partir de operar ambos motores en producción, en discos reales, con personas reales preguntando por qué está lento.

Guion de diagnóstico rápido (15 minutos)

Este es el orden que más confiablemente encuentra el cuello de botella sin empeorar las cosas. Está diseñado para el incidente “se puso lento de repente” donde necesitas señal rápido.

Minuto 0–2: Confirma que es la base de datos (y no que tu app está mintiendo)

  1. Compara latencia de la app vs latencia de la BD. Si tu aplicación tiene temporización por consulta, úsala. Si no, ejecuta una consulta trivial a la BD desde el host de la app (no desde tu portátil) para eliminar tonterías de red y DNS.
  2. Revisa métricas del pool de conexiones. Un pool saturado parece una BD lenta. A menudo es solo “demasiados hilos esperando una conexión”.

Decisión: Si las consultas triviales son rápidas y la espera en el pool es alta, tu BD puede estar bien y el incendio está en el tamaño del pool / concurrencia de la app.

Minuto 2–5: Busca el muro de recursos obvio (CPU, memoria, E/S)

  1. ¿CPU al 100%? Piensa: plan de consulta malo, índice ausente, consulta de reporte repentina, o una avalancha de peticiones.
  2. ¿Presión de memoria? El swapping hace que las bases de datos rindan como si corrieran bajo el agua.
  3. ¿Latencia de disco? Si el almacenamiento está lento, todo está lento. No debatas SQL mientras tus discos se están derritiendo.

Decisión: Si la latencia de almacenamiento es alta, triagea la ruta de almacenamiento y reduce la presión de escritura. Si la CPU está al máximo, identifica las consultas principales y detén al peor infractor.

Minuto 5–9: Revisa la contención (locks, esperas, agotamiento del pool)

  1. Bloqueos: encuentra sesiones bloqueantes y la sentencia que mantiene el bloqueo. Mata al bloqueador solo cuando entiendas qué es (¿cambio de esquema? ¿transacción larga? ¿job por lotes?).
  2. Eventos de espera: MySQL: esperas de InnoDB, locks de metadatos. PostgreSQL: esperas de lock, LWLocks, esperas de E/S.

Decisión: Si ves un único bloqueador causando acumulación, resuélvelo primero. La contención multiplica el dolor.

Minuto 9–13: Identifica las consultas de mayor “coste” ahora mismo

  1. Encuentra huellas de consulta que consumen tiempo, no solo por conteo.
  2. Verifica regresión de plan (índice no usado, estadísticas obsoletas, cambio de parámetro, valores bind diferentes).

Decisión: Si una consulta domina, mitiga (limitar tasa, matar, añadir índice, desactivar una feature flag) antes de hacer algo “inteligente”.

Minuto 13–15: Elige una mitigación segura y captura evidencia

  • Mitiga: detén la hemorragia (cancelar/matar sesiones fuera de control, reducir concurrencia, desactivar temporalmente rutas de características costosas, añadir un índice focalizado si es seguro, aumentar timeouts del pool para evitar estampidas).
  • Captura: snapshot del processlist / pg_stat_activity, esperas, consultas top, iostat y estado de replicación. Necesitarás esto cuando la adrenalina se vaya.

Idea parafraseada de Gene Kim: “Mejorar la fiabilidad es en gran parte mejorar los bucles de retroalimentación y acortar el tiempo para diagnosticar.”

La realidad compartida: la lentitud suele ser una de cinco cosas

MySQL y PostgreSQL tienen interiores diferentes, pero la lentitud en producción tiende a rimar. La mayoría de los incidentes se reducen a una de estas categorías:

  1. Saturación de recursos: CPU, memoria, E/S de disco, red o descriptores de archivos.
  2. Contención: locks, contención de latch, hotspots de filas o demasiados escritores concurrentes.
  3. Planes malos: índices faltantes, estadísticas obsoletas, cambios de parámetro, peculiaridades de caché de planes o cambios en la distribución de los datos.
  4. Amplificación de escritura: checkpoints, tormentas de flush, deuda de autovacuum/vacuum o presión de redo/WAL.
  5. Dependencias externas: picos de latencia del almacenamiento, vecinos ruidosos, caché RAID fallando, problemas en el host de virtualización o un job de backup haciendo algo “inocente”.

Por eso el guion empieza por el host y por las esperas. No puedes resolver con SQL un controlador de almacenamiento que está haciendo una rabieta.

Broma #1: La forma más rápida de hacer una base de datos lenta es declarar, “Probablemente está bien”, y luego iniciar una consulta de informe de tabla completa durante el tráfico pico.

Datos interesantes e historia que importan en incidentes

Esto no es trivia para una noche de preguntas. Son del tipo que explica por qué el motor se comporta como lo hace cuando estás de guardia.

  1. MVCC de PostgreSQL es más antiguo que muchos runbooks de producción. Su control de concurrencia por múltiples versiones significa que los lectores no bloquean a los escritores, pero crea tuplas muertas que vacuum debe limpiar — o siguen el bloat y la lentitud.
  2. InnoDB no siempre fue “MySQL”. Históricamente MySQL tuvo múltiples motores de almacenamiento; InnoDB se convirtió en el predeterminado porque la corrección transaccional y la recuperación tras fallo se volvieron no negociables en sistemas reales.
  3. Los locks de metadatos de MySQL pueden parar el mundo. Algunos DDL e incluso ciertas sentencias “inocentes” toman locks de metadatos, lo que puede bloquear otras consultas de formas sorprendentes durante cambios de esquema.
  4. PostgreSQL trata “VACUUM” como una preocupación operacional de primera clase. Autovacuum no es una decoración opcional; es cómo Postgres paga su deuda MVCC y previene el wraparound de IDs de transacción.
  5. La replicación es parte del rendimiento. El lag de replicación de MySQL puede convertir el “escalado de lectura” en un incidente de corrección de datos, y el lag de Postgres puede crecer cuando la generación de WAL sobrepasa el envío o la reproducción.
  6. Ambos motores han mejorado dramáticamente en introspección. Performance Schema de MySQL y las vistas estadísticas de Postgres (más extensiones como pg_stat_statements) convirtieron “adivinar” en “medir”, si realmente las habilitas.
  7. Los ajustes por defecto reflejan compromisos, no tu carga de trabajo. Ambos sistemas se distribuyen con configuraciones pensadas para arrancar en máquinas modestas. Producción requiere decisiones explícitas sobre memoria, checkpoints y conexiones.
  8. El planificador de consultas de PostgreSQL es basado en costes y sensible a las estadísticas. Cuando las estadísticas están obsoletas o cambia la distribución de datos, los planes pueden cambiar de “index scan” a “sequential scan” y jurarás que alguien saboteó la base de datos.
  9. La historia de MySQL incluye bordes afilados alrededor de comportamientos implícitos. Cosas como truncamiento silencioso (según el SQL mode) y distintos valores por defecto de aislamiento causaron historias de “funcionó en staging”—a menudo combinando rendimiento y corrección.

Tareas prácticas: comandos, salidas y decisiones (MySQL + PostgreSQL + Linux)

Quieres comandos que puedas ejecutar durante un incidente, más qué significa la salida y qué decisión tomar después. Aquí están los que uso primero.

Tarea 1: Comprueba carga del host, CPU steal y cola de ejecución

cr0x@server:~$ uptime
 10:21:33 up 187 days,  3:12,  2 users,  load average: 28.14, 26.90, 19.77

Significado: Un load average muy por encima de los núcleos CPU sugiere contención (CPU, E/S o cola runnable). Si estás en virtualización/nube, carga alta con uso moderado de CPU a menudo indica esperas de E/S o throttling.

Decisión: Si la carga es enorme, comprueba inmediatamente iowait y latencia de disco (Tarea 3). No empieces a “optimizar” en SQL a ciegas.

Tarea 2: Comprueba presión de memoria y swapping

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:            62Gi        55Gi       1.2Gi       512Mi       5.8Gi       2.3Gi
Swap:           8.0Gi       6.4Gi       1.6Gi

Significado: Uso intensivo de swap en un host de base de datos suele ser una espiral de muerte para el rendimiento. “Available” memoria baja y swapping implica que el SO está expulsando conjuntos de trabajo.

Decisión: Detén nueva carga (limita tasa de tráfico), reduce picos de memoria de la BD (por ejemplo, demasiadas conexiones) y considera reiniciar solo si puedes hacerlo de forma segura y has encontrado la fuga. También verifica ajustes de memoria de MySQL/Postgres frente a la RAM total.

Tarea 3: Mide latencia de disco y saturación

cr0x@server:~$ iostat -xz 1 3
Linux 6.2.0 (db01)  12/29/2025  _x86_64_  (16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.41    0.00    6.23   38.77    0.00   42.59

Device            r/s     w/s   rkB/s   wkB/s  await  aqu-sz  %util
nvme0n1         120.0  1800.0   4096  98304   24.8   12.4    98.9

Significado: %util cerca de 100% con await alto indica que el dispositivo está saturado y las solicitudes se ponen en cola. iowait también es alto. Esto es lentitud ligada al almacenamiento.

Decisión: Encuentra qué está escribiendo (Tarea 4), reduce la presión de escritura (pausa jobs por lotes, limita intensidad de autovacuum con cuidado, ralentiza ingestiones) y comprueba tormentas de checkpoint/flush en la BD.

Tarea 4: Identifica los mayores escritores de disco (rápido y sucio)

cr0x@server:~$ pidstat -d 1 5
Linux 6.2.0 (db01)  12/29/2025  _x86_64_  (16 CPU)

12:22:11      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
12:22:12      106      2143      0.00  84210.00      0.00  mysqld
12:22:12      106      1987      0.00   5200.00      0.00  postgres
12:22:12        0      8891      0.00   1200.00      0.00  rsync

Significado: Puedes ver qué proceso está golpeando los discos con escrituras. Si rsync/backup está escribiendo mucho durante el pico, felicidades: encontraste al villano con placa de “mantenimiento”.

Decisión: Si procesos no-BD son intensos, detén/reprograma. Si la BD es la escritora, revisa checkpointing, flushing y transacciones grandes.

Tarea 5: Confirma espacio en sistema de archivos y presión de inodos

cr0x@server:~$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  900G  842G   13G  99% /var/lib

Significado: 99% lleno puede hundir el rendimiento (fragmentación, demoras de asignación) y arriesga fallo total. Muchas bases de datos se comportan mal cerca de discos llenos.

Decisión: Libera espacio ahora (rota logs, mueve backups), luego planifica capacidad. También revisa logs binarios/WAL descontrolados.

Tarea 6: Revisa errores de red y retransmisiones (a menudo “BD lenta” es pérdida de paquetes)

cr0x@server:~$ ss -s
Total: 1892 (kernel 0)
TCP:   1583 (estab 1242, closed 251, orphaned 0, timewait 251)

Transport Total     IP        IPv6
RAW	  0         0         0
UDP	  17        13        4
TCP	  1332      1210      122
INET	  1349      1223      126
FRAG	  0         0         0

Significado: Conexiones establecidas en alto número pueden implicar mal comportamiento del pool o picos de tráfico. Combínalo con los conteos de conexiones de BD.

Decisión: Si las conexiones explotaron, busca un deploy que cambió el pooling o los timeouts. Si las conexiones son estables pero la latencia alta, céntrate en esperas y E/S.

Tarea 7 (MySQL): Mira qué están haciendo los hilos ahora mismo

cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G" | sed -n '1,80p'
*************************** 1. row ***************************
     Id: 93218
   User: app
   Host: 10.0.4.21:51344
     db: shop
Command: Query
   Time: 42
  State: Waiting for table metadata lock
   Info: ALTER TABLE orders ADD COLUMN promo_code varchar(32)
*************************** 2. row ***************************
     Id: 93244
   User: app
   Host: 10.0.3.19:51002
     db: shop
Command: Query
   Time: 38
  State: Sending data
   Info: SELECT * FROM orders WHERE user_id=... ORDER BY created_at DESC LIMIT 20

Significado: “Waiting for table metadata lock” es una flecha roja grande apuntando a DDL bloqueando consultas normales. “Sending data” por mucho tiempo puede indicar escaneos completos, conjuntos de resultados enormes o esperas de E/S.

Decisión: Si un lock de metadatos bloquea, pausa/mata la sesión DDL (con cuidado) o espera a que termine si está por completarse. Luego programa el cambio de esquema adecuadamente (DDL en línea donde sea posible).

Tarea 8 (MySQL): Identifica esperas de lock InnoDB y bloqueadores

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
------------
TRANSACTIONS
------------
Trx id counter 89423311
Purge done for trx's n:o < 89423000 undo n:o < 0 state: running
History list length 19876
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 89423288, ACTIVE 61 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 93244, OS thread handle 140228, query id 5129312 10.0.3.19 app Sending data
UPDATE inventory SET qty=qty-1 WHERE sku='A-1841'
------- TRX HAS BEEN WAITING 61 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 421 page no 183 n bits 72 index PRIMARY of table `shop`.`inventory` trx id 89423288 lock_mode X locks rec but not gap waiting

Significado: InnoDB te está diciendo que una transacción está esperando un lock de registro. El “History list length” grande también puede sugerir retraso en el purge, lo que puede correlacionar con transacciones largas.

Decisión: Encuentra la transacción bloqueante (más abajo en la salida, o vía performance_schema) y decide: matar bloqueador, matar el que espera, o reducir concurrencia sobre esa clave caliente (SKU, fila de usuario, etc.). Si la history list length se mantiene alta, busca transacciones largas.

Tarea 9 (MySQL): Comprueba lag de replicación y estado del hilo SQL

cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running|Last_SQL_Error|Relay_Master_Log_File|Exec_Master_Log_Pos"
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 487
Relay_Master_Log_File: binlog.003214
Exec_Master_Log_Pos: 98122314

Significado: El réplica está ~8 minutos detrás. Las lecturas desde réplicas pueden estar obsoletas; promover una réplica con lag puede perder datos.

Decisión: Si la app usa réplicas para lecturas, considera enrutar lecturas críticas al primario temporalmente. Investiga por qué el hilo SQL no puede seguir el ritmo (escrituras intensas, aplicación single-thread, transacciones largas).

Tarea 10 (PostgreSQL): Revisa sesiones activas y quién está esperando

cr0x@server:~$ psql -c "SELECT pid, usename, state, wait_event_type, wait_event, now()-query_start AS runtime, left(query,120) AS q FROM pg_stat_activity WHERE state <> 'idle' ORDER BY runtime DESC LIMIT 10;"
  pid  | usename | state  | wait_event_type |  wait_event  |  runtime  |                          q
-------+---------+--------+-----------------+--------------+-----------+----------------------------------------------------------
 18421 | app     | active | Lock            | relation     | 00:01:12  | ALTER TABLE orders ADD COLUMN promo_code text
 18502 | app     | active | Lock            | tuple        | 00:00:55  | UPDATE inventory SET qty=qty-1 WHERE sku='A-1841'
 18611 | app     | active | IO              | DataFileRead | 00:00:49  | SELECT * FROM orders WHERE user_id=$1 ORDER BY created_at
(3 rows)

Significado: Postgres te dice si las sesiones están esperando por locks o por E/S. Esperas por “relation” suelen apuntar a DDL o transacciones largas que mantienen locks.

Decisión: Si dominan las esperas por locks, encuentra el bloqueador (Tarea 11). Si dominan las esperas de E/S, vuelve a iostat y revisa hit de caché y checkpoints.

Tarea 11 (PostgreSQL): Encuentra PIDs bloqueantes (la persona que cierra la puerta)

cr0x@server:~$ psql -c "SELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query, blocker.pid AS blocker_pid, blocker.query AS blocker_query FROM pg_catalog.pg_locks blocked_locks JOIN pg_catalog.pg_stat_activity blocked ON blocked.pid = blocked_locks.pid JOIN pg_catalog.pg_locks blocker_locks ON blocker_locks.locktype = blocked_locks.locktype AND blocker_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE AND blocker_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocker_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocker_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple AND blocker_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid AND blocker_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid AND blocker_locks.classid IS NOT DISTINCT FROM blocked_locks.classid AND blocker_locks.objid IS NOT DISTINCT FROM blocked_locks.objid AND blocker_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid AND blocker_locks.pid != blocked_locks.pid JOIN pg_catalog.pg_stat_activity blocker ON blocker.pid = blocker_locks.pid WHERE NOT blocked_locks.granted;"
 blocked_pid |              blocked_query              | blocker_pid |              blocker_query
------------+-----------------------------------------+------------+-----------------------------------------
      18502 | UPDATE inventory SET qty=qty-1 ...      |      18111 | BEGIN; SELECT * FROM inventory WHERE ...
(1 row)

Significado: Tienes un PID bloqueador específico. A menudo es una transacción dejada abierta por un job, una migración o una sesión de depuración “temporal”.

Decisión: Si el bloqueador es claramente accidental y no realiza trabajo crítico, termínalo. Si es una migración, coordina y elige la opción menos dañina.

Tarea 12 (PostgreSQL): Comprueba presión de vacuum/autovacuum y tuplas muertas

cr0x@server:~$ psql -c "SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_vacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
   relname   | n_live_tup | n_dead_tup |    last_autovacuum     |      last_vacuum
-------------+------------+------------+------------------------+------------------------
 orders      |    5821934 |    2149921 | 2025-12-29 09:41:12+00 | 2025-12-27 02:10:04+00
 inventory   |     402113 |     210993 | 2025-12-29 10:02:01+00 |
(2 rows)

Significado: Muchas tuplas muertas sugiere bloat y/o que el vacuum no está al día, lo que puede aumentar E/S y degradar la eficiencia de índices.

Decisión: Si las tuplas muertas son altas y autovacuum va retrasado, ajusta autovacuum para las tablas calientes (configuración por tabla) e investiga transacciones largas que impiden la limpieza.

Tarea 13 (PostgreSQL): Comprueba lag de replicación vía LSN

cr0x@server:~$ psql -c "SELECT client_addr, state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
 client_addr |   state   | write_lag | flush_lag | replay_lag
-------------+-----------+-----------+-----------+-----------
 10.0.2.15    | streaming | 00:00:01  | 00:00:03  | 00:00:07
(1 row)

Significado: El lag es bajo. Si replay_lag es alto, tu réplica puede ser lenta al aplicar WAL (CPU o E/S limitado, o conflictos de hot standby).

Decisión: Si el lag es alto y usas réplicas para lecturas, considera enrutar lecturas o reducir volumen de escrituras. Investiga picos de generación de WAL y recursos de réplicas.

Tarea 14 (MySQL): Verifica salud del buffer pool y presión de lectura

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| Innodb_buffer_pool_reads| 982134221 |
+-------------------------+-----------+
+---------------------------------+------------+
| Variable_name                   | Value      |
+---------------------------------+------------+
| Innodb_buffer_pool_read_requests| 3812234910 |
+---------------------------------+------------+

Significado: Si Innodb_buffer_pool_reads crece rápido en relación con read_requests, estás fallando en la caché y golpeando disco. Los números absolutos importan menos que la tasa durante el incidente.

Decisión: Si los misses de caché se disparan, encuentra la consulta que cambió el patrón de acceso o considera aumentar el buffer pool (a largo plazo). A corto plazo, reduce la carga o añade el índice que evita escaneos amplios.

Tarea 15 (PostgreSQL): Comprueba ratio de aciertos de caché y derrames a archivos temporales

cr0x@server:~$ psql -c "SELECT datname, blks_hit, blks_read, temp_files, temp_bytes FROM pg_stat_database WHERE datname = current_database();"
 datname | blks_hit  | blks_read | temp_files | temp_bytes
---------+-----------+-----------+------------+------------
 shop    | 991233221 | 11233211  |      12933 |  987654321
(1 row)

Significado: temp_files/temp_bytes en ascenso significa que consultas están derramando a disco (sorts, hashes). Eso suele ser un problema de memoria por consulta (work_mem) o un plan malo que produce resultados intermedios enormes.

Decisión: Identifica las consultas que derraman (pg_stat_statements ayuda) y arréglalas. Evita subir work_mem a lo loco en sistemas con alta concurrencia — se multiplica por conexión.

Tarea 16: Captura un perfil rápido de hilos de BD (perspectiva Linux)

cr0x@server:~$ top -b -n 1 | sed -n '1,25p'
top - 10:23:02 up 187 days,  3:14,  2 users,  load average: 27.92, 26.80, 20.01
Tasks: 612 total,   3 running, 609 sleeping,   0 stopped,   0 zombie
%Cpu(s): 14.1 us,  5.9 sy,  0.0 ni, 41.2 id, 38.1 wa,  0.0 hi,  0.7 si,  0.0 st
MiB Mem :  63488.0 total,   1320.2 free,  56502.4 used,   5665.4 buff/cache
MiB Swap:   8192.0 total,   6553.7 used,   1638.3 free.  2401.9 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 2143 mysql      20   0 24.1g   36.2g   154m S  182.4  58.3  884:12.11 mysqld
 1987 postgres   20   0 15.2g    9.1g    88m S   42.1  14.7  311:09.22 postgres

Significado: Alto iowait (wa) dice que el almacenamiento está limitando. Alto CPU en mysqld sugiere una tormenta de queries CPU-bound. Mucha memoria RES con swap indica sobreasignación.

Decisión: Si iowait domina, detén las escrituras. Si CPU domina, aísla las consultas top y mátalas o mitígalas. Si swap domina, reduce concurrencia y huella de memoria.

Específico de MySQL: InnoDB, locks de metadatos, replicación y las trampas habituales

Cómo se manifiesta “se puso lento de repente” en MySQL

Los incidentes en MySQL a menudo se presentan como uno de estos patrones:

  • Acumulación de locks de metadatos: un DDL o LOCK TABLES bloquea una tabla concurrida; todo se pone en cola detrás.
  • Contención en una fila/clave caliente: contadores, filas de inventario, actualizaciones “last seen”—clásico “una fila, muchos escritores”.
  • Fallos de buffer pool: el conjunto de datos creció o cambió el patrón de consultas, convirtiendo hits de memoria en lecturas de disco.
  • Presión de flushing/checkpoint de InnoDB: páginas sucias alcanzan un umbral, aparecen tormentas de flushing y las escrituras se estancan.
  • Lag de replicación: réplicas quedando atrás; lecturas obsoletas; seguridad de failover disminuida.

Dos prioridades de diagnóstico en MySQL que la gente se salta (y luego lamenta)

1) Locks de metadatos. Si solo compruebas locks de filas, te perderás el evento “un ALTER TABLE que los domina a todos”. El processlist lo deja claro. Cuando lo veas, no lo discutas.

2) Tormentas de conexiones. A MySQL no le entusiasma que crees cantidades masivas de conexiones con alta memoria por hilo. “Simplemente aumentaremos max_connections” es cómo conviertes un incidente lento en un OOM.

Movidas de mitigación en MySQL que suelen ser seguras

  • Mata SELECTs claramente fuera de control que consumen CPU y E/S, especialmente consultas ad hoc.
  • Limita la tasa del infractor en la capa de app (feature flag, shaping de requests) en lugar de hacer thrashing en la BD.
  • Evita cambios de esquema a mediodía a menos que uses la aproximación online correcta y la hayas ensayado.
  • Mueve lecturas pesadas (si las réplicas están sanas y consistentes para el caso de uso).

Cuando “solo son lecturas lentas” es en realidad un problema de escritura

En InnoDB, escrituras intensas pueden ralentizar lecturas vía saturación de E/S y churn del buffer pool. Lo verás como “las lecturas son lentas”, pero iostat muestra la verdad. Trata al disco como el recurso compartido que es.

Específico de PostgreSQL: vacuum, bloat, WAL y las trampas habituales

Cómo se manifiesta “se puso lento de repente” en PostgreSQL

La lentitud en Postgres tiende a agruparse en estos puntos:

  • Esperas por locks debido a transacciones largas: alguien mantuvo una transacción abierta haciendo… lo que sea, y ahora vacuum y DDL no pueden progresar.
  • Deuda de autovacuum y bloat: las consultas se vuelven más lentas en días/semanas, luego se precipitan en “de repente” cuando la caché y la E/S ya no lo ocultan.
  • Presión de checkpoint/WAL: ráfagas de escritura causan picos de WAL; los checkpoints se vuelven caros; la latencia de E/S sube.
  • Derrames a archivos temporales: un sort/hash de una consulta derrama a disco y arrastra el sistema a esperas de E/S.
  • Cambios de plan: estadísticas, valores de parámetros o cambios en la distribución de datos llevan a scans secuenciales donde esperabas índices.

Dos prioridades de diagnóstico en Postgres que la gente se salta (y luego lamenta)

1) Transacciones de larga duración. Una sola sesión idle in transaction puede impedir que vacuum recupere tuplas muertas y puede mantener locks. Es el equivalente operacional de dejar un coche estacionado cruzado en un callejón estrecho.

2) Actividad de archivos temporales. Si temp_bytes sube rápido, estás derramando a disco y lo vas a notar. Arregla la consulta primero; “subir work_mem” a veces es correcto pero a menudo imprudente en alta concurrencia.

Movidas de mitigación en Postgres que suelen ser seguras

  • Termina al bloqueador accidental (idle in transaction, sesión ad hoc) tras verificar que no es un batch crítico.
  • Cancela consultas fuera de control en lugar de matar todo el proceso de la base de datos.
  • Reduce la concurrencia en el pool para detener la estampida. Postgres prefiere menos conexiones bien comportadas.
  • Aplica soluciones de vacuum por tabla en lugar de ajustes globales “vacuum más agresivo” que pueden castigar discos.

Broma #2: Autovacuum es como sacar la basura: saltártelo no ahorra tiempo, solo cambia el olor de tu incidente.

Tres micro-historias del mundo corporativo (anónimas, plausibles, técnicamente precisas)

Micro-historia 1: El incidente causado por una suposición errónea

Tenían un proceso de migración limpio: despliegues blue/green, checks previos y un mantra de “migraciones DB son seguras” que se repetía tanto que se volvió física. Un equipo lanzó un release de aplicación con un cambio de esquema que añadía una columna nullable. “Está bien”, dijeron. “Es online”.

En MySQL no fue así. El ALTER tomó un lock de metadatos y la tabla estaba caliente. El lock no bloqueó todo instantáneamente; bloqueó en el peor momento posible: a medida que otras sesiones se pusieron en cola detrás, mantenían conexiones por más tiempo, el pool se saturó y la app empezó a agotar tiempos. El SRE de guardia vio “BD lenta” en el dashboard, luego vio que la CPU no estaba al máximo. Casi se fueron por la madriguera de tunear consultas.

El punto de inflexión fue ejecutar SHOW FULL PROCESSLIST y ver “Waiting for table metadata lock” repetido como un susto de película de terror. La “suposición errónea” no era que existe DDL online. Era asumir que “online” significa “sin impacto operativo”. El DDL online aún compite por recursos y puede bloquear bajo carga, dependiendo de la operación y la versión exacta.

La solución fue aburrida: matar la sesión de migración, revertir el deploy y programar el cambio usando un procedimiento ensayado con ventanas de mantenimiento y checks de carga. Tras el incidente añadieron una barrera: los cambios de esquema debían probarse con carga tipo producción y las migraciones se limitaban tras feature flags. No fue glamuroso. Funcionó.

Micro-historia 2: La optimización que salió mal

Un clúster Postgres tenía un problema clásico de rendimiento: demasiadas conexiones. La base de datos lo manejó por un tiempo, hasta que dejó de hacerlo. Alguien propuso lo obvio: “Subamos max_connections. El hardware es barato.” Se aprobó porque sonaba a planificación de capacidad.

Lo aumentaron. Y en el siguiente pico, la latencia empeoró. No un poco, empeoró mucho. “Los clientes no pueden iniciar sesión” mucho peor. La máquina empezó a hacer swap. El monitoreo se veía absurdo: muchas sesiones haciendo cosas pequeñas lentamente, como si la base de datos decidiera volverse pasivo-agresiva.

Postgres asigna memoria por conexión e incurre en overhead por backend. Subir max_connections sin cambiar el comportamiento del pool convirtió la BD en un festival de cambios de contexto. Peor: con más concurrencia, aumentaron los derrames a temporal porque la memoria se fragmentó entre más backends, y subió la E/S.

La resolución no fue un ajuste GUC ingenioso. Fue implementar un pool de conexiones apropiado, bajar el número de sesiones activas y alinear work_mem con la concurrencia realista. La lección fue clara: aumentar un límite no es optimizar; es dar permiso para que tu sistema falle de maneras nuevas y más costosas.

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

Una compañía ejecutaba MySQL y PostgreSQL en distintos productos. La restricción unificadora: la capa de almacenamiento era compartida, y “incidentes de almacenamiento” eran el asesino silencioso. Un equipo llevaba meses insistiendo en añadir una práctica simple: capturar snapshots periódicos de métricas clave de BD y SO durante operación normal y almacenarlos junto con marcadores de deploy. Todos asintieron educadamente. Nadie quería pagar por lo “aburrido”.

Al final lo hicieron, porque un SRE se cansó de depurar a ciegas. Cada cinco minutos: resúmenes de iostat, lag de replicación, stats de buffer/cache, principales esperas, huellas de consultas top. Sin URLs crudas, sin tooling elegante—solo snapshots consistentes que podían compararse en el tiempo. También registraron cuándo corrían backups y cuándo había compactación en el array de almacenamiento.

Llegó el día de la “lentitud repentina”. La latencia se disparó en múltiples servicios. La primera pregunta fue predecible: “¿Qué base de datos está rota?” Los snapshots respondieron más rápido que las opiniones. El await del disco saltó al mismo minuto en todos los hosts DB. El lag de replicación subió después, no antes. La CPU se mantuvo casi normal. El sistema no estaba “SQL lento”. Estaba “almacenamiento lento”.

Porque tenían líneas base, escalaron al equipo de almacenamiento con evidencia: await del dispositivo, profundidad de cola y la correlación entre hosts. El equipo de almacenamiento encontró una tarea de mantenimiento mal comportada en el array y la detuvo. El incidente terminó sin matar consultas al azar y sin que un equipo culpara a otro. Práctica aburrida. Práctica correcta. Se pagó sola en una tarde.

Listas de verificación / plan paso a paso

Lista A: Los primeros cinco minutos (úsala cada vez)

  1. Desde el host DB: ejecuta uptime, free -h, iostat -xz 1 3.
  2. Decide: CPU-bound vs I/O-bound vs memory/swap-bound. Si no puedes clasificarlo, no estás mirando la capa correcta.
  3. Revisa conexiones: conteos de conexiones BD y espera del pool de la app.
  4. Revisa esperas: processlist + InnoDB status en MySQL; pg_stat_activity wait_event en Postgres.
  5. Captura evidencia: pega las salidas en el canal del incidente. El tú del futuro te lo agradecerá.

Lista B: Si es I/O-bound

  1. Confirma saturación del disco: await de iostat, aqu-sz, %util.
  2. Identifica escritores: pidstat -d; busca backups/rotación de logs/rsync.
  3. Sospechosos en BD:
    • MySQL: presión de flushing/checkpoint, transacciones grandes, picos de binlog.
    • Postgres: checkpoints, picos de WAL, actividad de vacuum, derrames a temporales.
  4. Mitiga: detén escritores por lotes, reduce concurrencia, retrasa jobs no críticos.
  5. Post-incidente: evalúa QoS de almacenamiento, volúmenes separados y establece líneas base de latencia de disco.

Lista C: Si es CPU-bound

  1. Encuentra consultas top por tiempo:
    • MySQL: performance schema / muestreo de processlist.
    • Postgres: pg_stat_statements si está habilitado; si no, pg_stat_activity + logs.
  2. Valida el plan: ¿está escaneando? ¿ordenando? ¿haciendo joins demasiado grandes?
  3. Mitiga: mata consultas fuera de control, añade el índice faltante si es seguro, reduce concurrencia, aplica feature flags.
  4. Después: añade una prueba de regresión para el plan de la consulta y rastrea huellas de consultas tras despliegues.

Lista D: Si es por locks/contención

  1. Encuentra bloqueadores: InnoDB status / performance_schema en MySQL; join de pg_locks en Postgres.
  2. Clasifica el bloqueador: DDL, transacción larga, update de fila caliente o job por lotes.
  3. Mitiga: termina al bloqueador si es seguro, o pausa la feature que genera contención.
  4. Después: rediseña hotspots (shard counters, usa colas, evita updates single-row en alta concurrencia).

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

1) “La BD está lenta” pero las consultas triviales son rápidas

Síntoma: Requests de la app agotan tiempo; la BD “se ve bien”.

Causa raíz: Saturación del pool de conexiones o agotamiento de hilos en la app; las consultas se encolan antes de llegar a la BD.

Solución: Revisa tiempos de espera del pool y max connections; reduce concurrencia de la app; arregla leaks (no devolver conexiones); añade retropresión.

2) Muchas sesiones MySQL atascadas en “Waiting for table metadata lock”

Síntoma: De repente todo lo relacionado con una tabla se cuelga; la CPU puede verse normal.

Causa raíz: Un DDL o una sesión manteniendo un lock de metadatos bloquea otras sentencias.

Solución: Identifica la sesión DDL y detén/mátala; reprograma cambios de esquema; usa técnicas de schema change online apropiadas para tu versión de MySQL y la operación.

3) Consultas de Postgres se ralentizan con el tiempo, luego “de repente” caen en picado

Síntoma: Latencia creciente, aumento de lecturas de disco, tablas/índices más grandes.

Causa raíz: Autovacuum no da abasto; el bloat crece; las tuplas muertas permanecen por transacciones largas o settings de vacuum insuficientes.

Solución: Encuentra y elimina transacciones largas; ajusta autovacuum por tabla caliente; programa vacuum manual cuando sea necesario; asegúrate de que maintenance_work_mem y capacidad I/O estén alineados.

4) Réplicas atrasadas y la app obtiene “lecturas lentas”

Síntoma: Endpoints de lectura lentos o inconsistentes; el failover se siente arriesgado.

Causa raíz: La aplicación de replicación no da abasto por picos de escritura, cuellos de botella de E/S, o aplicador single-thread (dependiente del motor/config).

Solución: Reduce volumen de escrituras, optimiza transacciones, asegura que las réplicas tengan I/O igual o mejor, y verifica settings de replicación. Enruta lecturas críticas al primario temporalmente.

5) “Añadimos un índice y empeoró”

Síntoma: Escrituras más lentas, lag de replicación aumenta, disco se llena más rápido.

Causa raíz: El índice aumentó la amplificación de escritura; subió el overhead de mantenimiento; las consultas no lo usan o es el índice equivocado.

Solución: Valida uso con planes y estadísticas; elimina índices ineficaces; prefiere índices estrechos y selectivos; considera índices parciales (Postgres) o índices compuestos diseñados para los predicados exactos.

6) Derrames a temporales o tormentas de sorts en Postgres

Síntoma: temp_bytes se dispara; latencia de disco sube; sorts lentos.

Causa raíz: Plan malo produciendo conjuntos intermedios enormes; work_mem insuficiente para esa consulta; demasiadas ordenaciones concurrentes.

Solución: Arregla la consulta (añade índice, reescribe joins/order), usa LIMIT antes si es posible y ajusta work_mem cuidadosamente con la concurrencia en mente.

7) Comienza el swapping y la BD se vuelve impredecible

Síntoma: Varianza de latencia explota; la CPU idle puede verse alta; iowait sube.

Causa raíz: Demasiadas conexiones, memoria por sesión demasiado grande o presión de caché del SO; a veces colocation/vecino ruidoso.

Solución: Reduce concurrencia; aplica pooling de conexiones; dimensiona memoria correctamente; investiga contención a nivel de host.

Preguntas frecuentes (FAQ)

1) ¿Cuál es más fácil de diagnosticar bajo presión: MySQL o PostgreSQL?

Postgres suele ser más explícito sobre esperas y bloqueos vía vistas del sistema. MySQL puede ser igual de diagnosable si Performance Schema está habilitado y sabes dónde mirar, pero muchas tiendas lo dejan infrautilizado.

2) ¿Cuál es la señal más rápida de “el almacenamiento es el problema”?

iostat -xz mostrando alto await y alto %util, más un CPU con alto iowait. Si eso es cierto, debatir sobre índices es prematuro.

3) ¿Es seguro matar consultas?

Es más seguro que dejar que todo agote los tiempos. Pero hazlo deliberadamente: identifica la sesión, confirma que no es una migración/limpieza crítica y prefiere cancelar una sentencia (Postgres) antes que matar todo el proceso salvo que sea necesario.

4) ¿Por qué importan tanto las sesiones “idle in transaction” en Postgres?

Pueden evitar que vacuum elimine tuplas muertas y pueden mantener locks más tiempo del que crees. También inflan horizontes de transacción, lo que convierte mantenimiento rutinario en un incidente más adelante.

5) ¿Por qué aumentar max_connections suele hacer Postgres más lento?

Porque incrementa overhead por backend y fomenta concurrencia sin control. Más sesiones activas significa más contención, más churn de caché y más derrames temporales. Pooling suele ser la solución correcta.

6) ¿Por qué MySQL a veces “se cuelga” durante cambios de esquema?

Por locks de metadatos y comportamiento de DDL. Incluso cuando una operación es “online”, puede bloquear en fases o bajo ciertas cargas. El efecto de encolamiento puede parecer que toda la BD se congeló.

7) ¿Cómo sé si es una regresión de plan?

Síntomas: picos de CPU con aumento de lecturas lógicas, o una consulta que antes era rápida se vuelve lenta tras un deploy o cambio de estadísticas. Confírmalo revisando el plan (EXPLAIN) y si la consulta dejó de usar un índice.

8) ¿Debo ajustar buffers/caches durante el incidente?

Rara vez. La mayoría de cambios de tuning requieren reinicio (a veces), tienen efectos retardados y son fáciles de equivocarse. Durante el incidente: mitiga la carga, detén bloqueadores, mata runaway y captura evidencia. Tunea después, con una hipótesis clara.

9) ¿Y si ambas bases de datos están lentas al mismo tiempo?

Asume primero infraestructura compartida: almacenamiento, red, DNS, host de virtualización o un job de backup compartido. La lentitud correlada entre motores usualmente no es mala suerte simultánea en planes de consulta.

10) Si solo puedo habilitar una característica de introspección, ¿cuál debería ser?

Para Postgres: pg_stat_statements (con retención sensata). Para MySQL: Performance Schema con instrumentos de statement y wait habilitados a un nivel que puedas soportar. El mejor incidente es el que no tienes que adivinar.

Próximos pasos que puedes hacer esta semana

Si ejecutas bases de datos en producción, “se puso lento de repente” no es un si. Es un cuándo. Aquí tienes cómo hacer que el próximo sea más corto y menos dramático:

  1. Escribe tu secuencia de 15 minutos (toma prestado el guion arriba) y hazla visible para la rotación on-call.
  2. Habilita fingerprinting de consultas y esperas en el motor que uses, y verifica que puedes responder: “¿cuáles son las 5 consultas principales por tiempo total ahora mismo?”
  3. Establece línea base de latencia de disco durante horas normales. Sin una línea base, cada gráfico es solo arte moderno.
  4. Haz cumplir disciplina de conexiones: pooliza conexiones, limita concurrencia y convierte el tiempo de espera del pool en una alerta de primera clase.
  5. Haz los cambios de esquema aburridos: ensáyalos, prográmalos y trata al DDL como carga de producción con radio de impacto.

Cuando suene la próxima alerta, no necesitas un debate sobre motores. Necesitas una clasificación rápida—CPU, memoria, E/S, locks o plan—y una acción segura a la vez.

← Anterior
Tareas atascadas en Proxmox: Cómo limpiar trabajos y procesos colgados de forma segura
Siguiente →
WireGuard hub-and-spoke para 3 oficinas a través de una puerta de enlace central

Deja un comentario