PostgreSQL vs Percona Server: mitos de rendimiento — por qué «es más rápido» depende de la carga de trabajo

¿Te fue útil?

Si alguna vez has estado en una reunión donde alguien dice: «Deberíamos cambiar de base de datos, será más rápido», conoces la sensación: tu atención se vuelve más urgente. La mayoría de las afirmaciones de “más rápido” provienen de benchmarks con el esquema equivocado, el conjunto de datos equivocado, la concurrencia equivocada y los modos de fallo equivocados.

PostgreSQL y Percona Server (una variante de MySQL orientada al rendimiento) pueden ser ambos brutalmente rápidos. También pueden ser ambos dolorosamente lentos. La diferencia no es cuestión de sensaciones. Es la forma de la carga de trabajo, la disciplina operativa y si por accidente estás midiendo tu caché.

Mitos de rendimiento que no mueren

Mit o 1: «Percona Server es más rápido que PostgreSQL»

Percona Server puede ser más rápido que PostgreSQL para el OLTP clásico con forma MySQL: muchas consultas puntuales indexadas, patrones de acceso predecibles y equipos que saben operar InnoDB correctamente. PostgreSQL también puede ser más rápido que Percona Server para la misma carga si tus patrones de consulta prefieren el comportamiento del planificador de Postgres, o si los instintos de afinación de tu equipo se alinean mejor con los parámetros y la observabilidad de Postgres.

«Más rápido» aquí es una afirmación que necesita una unidad: ¿latencia p95? ¿Rendimiento máximo a latencia fija? ¿Tiempo de recuperación tras un failover? ¿Horas gastadas persiguiendo deriva en replicación? La base de datos que “gana” depende del dolor que estés midiendo.

Mito 2: «PostgreSQL es lento por el bloat de MVCC»

MVCC no es un impuesto inevitable. Es un compromiso de diseño que ofrece lecturas consistentes y concurrencia. El impuesto aparece cuando ignoras el vacuum, las transacciones de larga duración y el bloat de tablas/índices. Si lo operas correctamente, Postgres maneja alta concurrencia con limpieza. Si lo operas de forma perezosa, irá convirtiendo tu almacenamiento en un álbum de tuplas muertas.

Mito 3: «MySQL siempre es más rápido en lecturas»

Las lecturas no son una única cosa. Hay lecturas puntuales, barridos por rango, joins con fan-out, agregaciones analíticas y “lecturas” que en realidad son esperas por bloqueos. InnoDB puede rendir muchísimo en lecturas puntuales con un buffer pool caliente y un esquema simple. Postgres también puede rendir muchísimo —especialmente con buenos índices, caché eficaz y planes que evitan sorpresas de bucles anidados indeseables.

Mito 4: «Los benchmarks lo resuelven»

Los benchmarks resuelven exactamente una cosa: si una base de datos específica, en una configuración específica, sobre un conjunto de datos específico, con un generador de carga específico, en una máquina específica, bajo un escenario sin fallos específico, alcanzó un número. Útil. No universal.

Un chiste corto, como limpiador de paladar: Los benchmarks son como los currículos: todo parece increíble hasta que verificas referencias bajo carga.

Mito 5: «Cambiar de motor arregla el rendimiento»

Cambiar de motor cambia tu cuello de botella, no tu responsabilidad. No escapas a la física. Cambias un conjunto de trampas operativas por otro. Si tu problema son índices faltantes, consultas malas o almacenamiento subdimensionado, migrar es solo una forma cara de posponer el mismo trabajo.

Hechos interesantes y contexto histórico (lo que explica el comportamiento actual)

  • El linaje de PostgreSQL se remonta a POSTGRES en UC Berkeley en los años 80; el nombre “PostgreSQL” refleja la adición temprana de SQL que se mantuvo.
  • InnoDB se convirtió en el motor por defecto para MySQL en la versión 5.5; antes de eso, MyISAM era común y traía una historia muy diferente de bloqueo y durabilidad.
  • Percona Server surgió porque muchas empresas querían compatibilidad con MySQL más instrumentación y características de rendimiento sin esperar los ciclos de lanzamiento del upstream.
  • Postgres añadió consultas paralelas de manera progresiva (no todo a la vez), y eso cambió la ecuación para ciertos workloads de reporting que antes se exportaban a Spark por defecto.
  • La historia de replicación de MySQL empezó con replicación basada en sentencias, que fue más rápida en algunos casos pero podía ser no determinista; la replicación por filas mejoró la corrección a costa de mayor volumen.
  • El WAL de Postgres (write-ahead log) ha sido central siempre, pero características como logical decoding facilitaron construir pipelines de cambio de datos sin triggers.
  • El buffer pool de InnoDB es una de las palancas más grandes para el rendimiento en MySQL/Percona; configurarlo mal puede hacer que un servidor rápido parezca almacenamiento lento.
  • Vacuum no es opcional en Postgres; forma parte de cómo MVCC mantiene el rendimiento estable. Existe autovacuum porque los humanos son poco fiables.
  • Ambos ecosistemas han madurado hasta sistemas de grado empresarial; la discusión hoy rara vez trata sobre capacidad y más sobre ajuste y operación.

Qué significa realmente “más rápido”: arquetipos de carga

1) OLTP con claves calientes: muchas lecturas/escrituras puntuales sobre un conjunto de trabajo pequeño

Aquí es donde Percona Server (InnoDB) a menudo luce muy bien: las páginas calientes están en el buffer pool, el buffering de cambios y los comportamientos adaptativos entran en juego, y el motor está optimizado para el estilo de “muchas consultas pequeñas”. Postgres también puede hacerlo, pero te importará más la gestión de conexiones, la elección de índices y evitar patrones de transacción que bloqueen el vacuum.

Trampa típica: haces un benchmark contra un conjunto de datos diminuto que cabe en RAM y declaras victoria. En producción el conjunto de trabajo es mayor, y de repente mides latencia de almacenamiento y churn de caché en lugar de SQL.

2) OLTP con alta escritura: inserciones/actualizaciones sostenidas con durabilidad

Aquí “más rápido” está limitado por el volumen de logs, el comportamiento de fsync y el checkpointing. En Postgres importan el volumen de WAL y el ajuste de checkpoints, así como que autovacuum pueda seguir el ritmo. En InnoDB importan la capacidad del redo log, el comportamiento de flushing y las implicaciones del doublewrite. Ambos pueden chocar contra paredes de I/O; simplemente golpean paredes distintas primero según la configuración y el esquema.

3) Carga mixta con joins complejos

El planificador de Postgres es potente, y sus estadísticas y opciones de indexación (incluyendo índices sobre expresiones e índices parciales) pueden producir planes muy buenos para consultas relacionales complejas. MySQL/Percona ha mejorado mucho, pero todavía hay cargas donde Postgres suele ser el motor “menos sorprendente” para joins y semántica SQL avanzada.

Punto de decisión: si tu aplicación se apoya en diseño relacional y consultas de reporting complejas junto al OLTP, Postgres suele reducir el número de incidentes de “reescribimos SQL para complacer al optimizador”.

4) Réplicas de lectura y fan-out de lecturas

La replicación es rendimiento. Si no puedes escalar lecturas sin que la latencia de replicación sea un error de producto, tu primario “rápido” no importa. Las tiendas Percona suelen depender de flotas de réplicas; las tiendas Postgres también, pero los patrones operativos difieren (replicación física por streaming, replicación lógica y extensiones).

5) Tareas administrativas “aburridas”: backups, cambios de esquema y recuperaciones

La velocidad incluye cuán rápido puedes desplegar cambios. Los cambios de esquema en línea, el tiempo de creación de índices, el vacuum y el tiempo de restauración deciden si estás durmiendo. Ambos motores pueden operarse de forma segura; ambos pueden llevarse al precipicio si tratas el mantenimiento como opcional.

Dónde difieren los motores (en las partes que te duelen)

Control de concurrencia: MVCC no es igual en todas partes

Ambos sistemas usan conceptos de MVCC, pero las consecuencias operativas difieren. Postgres conserva múltiples versiones de filas en la propia tabla, y la visibilidad se determina por IDs de transacción. Eso significa que las tuplas muertas se acumulan hasta que vacuum las recupera. No es un fallo; es el trato.

InnoDB también usa MVCC con logs de undo. Las versiones antiguas viven en segmentos de undo en lugar de como tuplas muertas en las páginas de la tabla. Eso cambia la forma del “bloat” y los síntomas que verás: transacciones largas pueden impedir el purge, agrandando la historia y causando degradación de rendimiento de otra manera.

Durabilidad y patrones de I/O: WAL vs redo/undo + doublewrite

Postgres escribe registros WAL y luego hace flush de páginas sucias; los checkpoints pueden causar ráfagas de escritura si están mal ajustados. InnoDB tiene redo logs y archivos de datos, además de mecanismos como el doublewrite para proteger contra escrituras parciales de página. Las opciones de ajuste difieren, pero la física es la misma: tu subsistema de almacenamiento tiene un presupuesto, y la base de datos lo gastará.

El “impuesto del optimizador”: los planificadores son máquinas opinativas

El planificador de Postgres puede hacer elecciones brillantes y ocasionalmente terribles cuando las estadísticas están obsoletas o las distribuciones son extrañas. El optimizador de MySQL/Percona tiene sus propias peculiaridades, especialmente en joins complejos y ciertos patrones de subconsultas. No eliges el “mejor optimizador”. Eliges el optimizador que puedas predecir, instrumentar y corregir.

Conexiones: Postgres castiga las tormentas de conexiones

Postgres usa por defecto un modelo de proceso por conexión. Una avalancha de conexiones de corta duración puede ser un DoS auto-infligido. La solución no es “hacer la base de datos más grande”. Es pooling (a menudo PgBouncer), dimensionar correctamente max connections y enseñar a la aplicación a reutilizar conexiones.

MySQL típicamente maneja muchas conexiones con características de overhead distintas. Eso no significa que debas dejar a los clientes descontrolados. Significa que el modo de fallo puede aparecer más tarde, con una firma distinta.

Observabilidad: ambos tienen herramientas, pero los valores por defecto difieren

Postgres con pg_stat_statements te da visibilidad de consultas de alto valor. Percona Server es famoso por instrumentación adicional y el ecosistema de herramientas Percona. En cualquier caso, si no recoges las métricas y trazas de consultas adecuadas, acabarás “afinando” basándote en supersticiones.

Una cita, porque las personas de operaciones merecen al menos una buena frase: La esperanza no es una estrategia. — Vince Lombardi

Guía rápida de diagnóstico

Esto es lo que haces cuando alguien dice: «La base de datos está lenta» y tienes 15 minutos antes de que el canal de incidentes se convierta en arte del rendimiento.

Primero: confirma el síntoma y la unidad

  • ¿Es latencia o rendimiento? Latencia p95 de consultas frente a QPS total. Fallan de forma distinta.
  • ¿Es la base de datos? Pools de hilos de la app, red y llamadas downstream pueden hacerse pasar por lentitud de BD.
  • ¿Es una clase de consulta? Una consulta mala puede envenenar todo un pool.

Segundo: clasifica el cuello de botella en uno de cuatro grupos

  1. Limitado por CPU: CPU alta, I/O estable, consultas lentas con mucho cómputo (sorts, hash joins, procesamiento JSON).
  2. Limitado por I/O: latencia de lectura/escritura alta, tasas de acierto de caché bajas, presión de checkpoints/flush.
  3. Limitado por bloqueos/transacciones: esperas, contención de bloqueos, transacciones largas, retrasos en la aplicación de la replicación.
  4. Desajuste memoria/conjunto de trabajo: thrash de caché, churn del buffer pool, uso frecuente de archivos temporales.

Tercero: escoge la ruta más corta hacia la prueba

  • En Postgres: empieza con pg_stat_activity, pg_stat_statements, tasa de aciertos de caché, estadísticas de checkpoints y progreso de vacuum.
  • En Percona: empieza con SHOW PROCESSLIST, métricas del InnoDB buffer pool, estadísticas de redo/flush y resúmenes de consultas lentas.
  • En el host: revisa iostat, stalls por presión, latencia del sistema de ficheros y si el almacenamiento está gritando en silencio.

Segundo chiste corto (y terminamos): La base de datos más rápida es la que no señalaste por accidente al job de reporting.

Tareas prácticas: comandos, qué significa la salida y qué decisión tomar

Estas son tareas reales que puedes ejecutar durante un incidente o una revisión de rendimiento. Cada una incluye: comando, salida de ejemplo, interpretación y la siguiente decisión. Los comandos se dividen entre PostgreSQL y Percona/MySQL, más comprobaciones a nivel OS que deciden hacia dónde profundizar.

Task 1 (PostgreSQL): Ver qué está corriendo y qué está esperando

cr0x@server:~$ psql -X -d appdb -c "select pid, usename, state, wait_event_type, wait_event, now()-query_start as age, left(query,80) as query from pg_stat_activity where state <> 'idle' order by age desc limit 10;"
 pid  | usename | state  | wait_event_type |  wait_event   |   age    |                                      query
------+--------+--------+-----------------+---------------+----------+--------------------------------------------------------------------------------
 8123 | app    | active | Lock            | transactionid | 00:02:11 | update orders set status='paid' where id=$1
 7991 | app    | active | IO              | DataFileRead  | 00:01:05 | select * from order_items where order_id=$1
 7902 | app    | active |                 |               | 00:00:40 | select count(*) from orders where created_at > now()-interval '1 day'

Qué significa: Tienes una espera por bloqueo en transactionid (a menudo conflictos a nivel de fila que escalan a esperas a nivel de transacción) y una espera de I/O en lecturas de archivos de datos.

Decisión: Si dominan los bloqueos, encuentra el PID bloqueante y corrige el comportamiento de transacciones. Si domina el I/O, revisa la tasa de aciertos de caché y la latencia de almacenamiento antes de tocar el SQL.

Task 2 (PostgreSQL): Identificar al bloqueador

cr0x@server:~$ psql -X -d appdb -c "select blocked.pid as blocked_pid, blocker.pid as blocker_pid, blocked.query as blocked_query, blocker.query as blocker_query from pg_locks blocked_locks join pg_stat_activity blocked on blocked_locks.pid=blocked.pid join pg_locks blocker_locks on blocked_locks.locktype=blocker_locks.locktype and blocked_locks.database is not distinct from blocker_locks.database and blocked_locks.relation is not distinct from blocker_locks.relation and blocked_locks.page is not distinct from blocker_locks.page and blocked_locks.tuple is not distinct from blocker_locks.tuple and blocked_locks.virtualxid is not distinct from blocker_locks.virtualxid and blocked_locks.transactionid is not distinct from blocker_locks.transactionid and blocked_locks.classid is not distinct from blocker_locks.classid and blocked_locks.objid is not distinct from blocker_locks.objid and blocked_locks.objsubid is not distinct from blocker_locks.objsubid and blocker_locks.pid != blocked_locks.pid join pg_stat_activity blocker on blocker_locks.pid=blocker.pid where not blocked_locks.granted;"
 blocked_pid | blocker_pid |              blocked_query               |                blocker_query
------------+------------+------------------------------------------+---------------------------------------------
       8123 |       7001 | update orders set status='paid' where id=$1 | begin; update orders set ...; -- long txn

Qué significa: El PID 7001 está reteniendo el bloqueo. Observa el patrón begin;: transacción larga.

Decisión: Corrige el alcance de las transacciones en la app. Durante el incidente: considera terminar el bloqueador si es seguro. A largo plazo: reduce el tiempo de transacción, evita flujos en los que el usuario mantiene la transacción “abierta mientras piensa”.

Task 3 (PostgreSQL): Consultas top por tiempo total (requiere pg_stat_statements)

cr0x@server:~$ psql -X -d appdb -c "select calls, round(total_exec_time::numeric,1) as total_ms, round(mean_exec_time::numeric,2) as mean_ms, rows, left(query,100) as query from pg_stat_statements order by total_exec_time desc limit 5;"
 calls | total_ms | mean_ms | rows  |                                                query
-------+----------+---------+-------+-----------------------------------------------------------------------------------------------------
 12000 | 980000.0 |   81.67 | 12000 | select * from order_items where order_id = $1
   800 | 410000.0 |  512.50 |   800 | select count(*) from orders where created_at > now()-interval '1 day'
   300 | 220000.0 |  733.33 |  9000 | select * from orders o join customers c on c.id=o.customer_id where c.email like $1

Qué significa: Una consulta domina el tiempo total por su alto número de llamadas; otra es lenta por llamada.

Decisión: Para la consulta de alto volumen, focalízate en índices y caché. Para la consulta lenta, ejecuta EXPLAIN (ANALYZE, BUFFERS) y corrige la forma del plan o añade el índice adecuado.

Task 4 (PostgreSQL): Comprobar la tasa de aciertos de caché (señal, no religión)

cr0x@server:~$ psql -X -d appdb -c "select datname, round(100*blks_hit/nullif(blks_hit+blks_read,0),2) as cache_hit_pct, blks_read from pg_stat_database where datname='appdb';"
 datname | cache_hit_pct | blks_read
--------+---------------+-----------
 appdb  |         93.41 |   1829934

Qué significa: 93% puede ser aceptable o terrible dependiendo de la carga. Pero si cayó de 99% ayer, tu conjunto de trabajo creció o la memoria disminuyó.

Decisión: Si la tasa de aciertos baja y las lecturas aumentan, verifica presión de RAM, sanity de shared_buffers y si un nuevo patrón de consulta está escaneando tablas grandes.

Task 5 (PostgreSQL): Encontrar tablas con señales de bloat (tuplas muertas)

cr0x@server:~$ psql -X -d appdb -c "select relname, n_live_tup, n_dead_tup, round(100*n_dead_tup/nullif(n_live_tup+n_dead_tup,0),2) as dead_pct from pg_stat_user_tables order by n_dead_tup desc limit 5;"
  relname   | n_live_tup | n_dead_tup | dead_pct
------------+------------+------------+---------
 orders     |   12500000 |    4100000 |   24.69
 sessions   |     800000 |    2900000 |   78.38
 events     |   90000000 |    1200000 |    1.32

Qué significa: sessions es una fábrica de bloat. Probablemente actualizaciones/eliminaciones frecuentes más progreso de vacuum pobre, quizá transacciones de larga duración.

Decisión: Corrige el patrón de churn (particionamiento por TTL, menos actualizaciones) y ajusta autovacuum para esa tabla. Si ya es enorme, planifica un VACUUM (FULL) controlado o reescritura de tabla durante ventana de mantenimiento.

Task 6 (PostgreSQL): Ver si los checkpoints te castigan

cr0x@server:~$ psql -X -d appdb -c "select checkpoints_timed, checkpoints_req, round(checkpoint_write_time/1000.0,1) as write_s, round(checkpoint_sync_time/1000.0,1) as sync_s, buffers_checkpoint from pg_stat_bgwriter;"
 checkpoints_timed | checkpoints_req | write_s | sync_s | buffers_checkpoint
------------------+-----------------+---------+--------+-------------------
              124 |             198 |  905.2  |  210.3 |          88412211

Qué significa: Muchas solicitudes de checkpoint sugieren presión de WAL o ajustes que fuerzan checkpoints frecuentes. Eso puede crear picos de I/O y latencia.

Decisión: Considera aumentar max_wal_size, ajustar checkpoint_completion_target y revisar la latencia de escritura del almacenamiento. No lo “arregles” desactivando durabilidad.

Task 7 (Percona/MySQL): Quién está ejecutando y quién está atascado

cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G"
*************************** 1. row ***************************
     Id: 22341
   User: app
   Host: 10.0.2.19:51244
     db: appdb
Command: Query
   Time: 132
  State: Waiting for row lock
   Info: update orders set status='paid' where id=?
*************************** 2. row ***************************
     Id: 22110
   User: app
   Host: 10.0.2.20:49821
     db: appdb
Command: Query
   Time: 141
  State: Sending data
   Info: select * from order_items where order_id=?

Qué significa: Tienes esperas por bloqueos de fila y una consulta en “Sending data” (a menudo significa que está escaneando/devolviendo muchas filas, o esperando I/O).

Decisión: Para bloqueos de fila, identifica la transacción bloqueante. Para “Sending data”, revisa el plan de consulta y los índices y confirma el hit del buffer pool y lecturas desde disco.

Task 8 (Percona/MySQL): Encontrar esperas de bloqueo InnoDB y al bloqueador

cr0x@server:~$ mysql -e "SELECT r.trx_id waiting_trx_id, r.trx_mysql_thread_id waiting_thread, r.trx_query waiting_query, b.trx_id blocking_trx_id, b.trx_mysql_thread_id blocking_thread, b.trx_query blocking_query FROM information_schema.innodb_lock_waits w JOIN information_schema.innodb_trx b ON b.trx_id=w.blocking_trx_id JOIN information_schema.innodb_trx r ON r.trx_id=w.requesting_trx_id\G"
*************************** 1. row ***************************
 waiting_trx_id: 9012231
 waiting_thread: 22341
  waiting_query: update orders set status='paid' where id=?
blocking_trx_id: 9012198
blocking_thread: 21990
 blocking_query: update orders set status='processing' where id=?; -- txn open

Qué significa: El thread 21990 está bloqueando. Probablemente una transacción larga o un flujo de aplicación que mantiene bloqueos demasiado tiempo.

Decisión: Si es seguro, mata al bloqueador. A largo plazo, reduce el alcance de las transacciones y asegúrate de que los índices soporten actualizaciones dirigidas (para evitar bloquear filas extra).

Task 9 (Percona/MySQL): Comprobar la efectividad del buffer pool

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

Qué significa: La ratio es decente (las lecturas desde disco son una fracción pequeña de las solicitudes). Si Innodb_buffer_pool_reads sube rápidamente, estás perdiendo caché.

Decisión: Si las lecturas desde disco son altas, aumenta el tamaño del buffer pool (dentro de los límites de RAM) o reduce el conjunto de trabajo mediante índices y cambios de consulta.

Task 10 (Percona/MySQL): Buscar presión de redo log / checkpoints

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
=====================================
2025-12-30 10:12:01 0x7f2c3c0a0700 INNODB MONITOR OUTPUT
=====================================
Log sequence number          146990331122
Log flushed up to            146990220981
Last checkpoint at           146988102000
0 pending log flushes, 0 pending chkp writes

Qué significa: LSN, lo escrito y la posición de checkpoints indican cuán atrasado está el flushing/checkpointing. Grandes brechas bajo carga pueden significar presión de redo o límites de I/O.

Decisión: Si el checkpointing se queda atrás en picos, revisa el dimensionamiento del redo log y las opciones de flush, y verifica la latencia de escritura del almacenamiento. No cambies a ciegas knobs de durabilidad sin aprobación de riesgo.

Task 11 (OS): Verificar latencia de disco y si estás I/O-bound

cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (db01)  12/30/2025  _x86_64_ (32 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          18.21    0.00    4.12   22.33    0.00   55.34

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   w_await wareq-sz  aqu-sz  %util
nvme0n1         220.0  18944.0     0.0   0.00    9.40    86.10   480.0  61200.0   18.70   127.50   12.30  98.00

Qué significa: %iowait alto, w_await elevado y %util cerca del 100% gritan saturación de almacenamiento. Tu base de datos está esperando que el disco cumpla promesas que no puede cumplir.

Decisión: Deja de afinar SQL primero. Reduce la amplificación de escrituras (índices, churn), reparte I/O (separa WAL/redo y datos si es posible) o mejora el almacenamiento. También revisa vecinos ruidosos en discos compartidos.

Task 12 (OS): Comprobar presión de memoria e intercambio

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0  81264  14000 612000    0    0   120   980 9200 8800 18  4 56 22  0
 3  1      0  64220  13890 590100    0    0   110  1100 9400 9100 17  4 54 25  0

Qué significa: No hay intercambio activo (si/so son 0), pero hay espera de I/O. Si vieras swapping, esperarías latencias dramáticas y reportes aleatorios de “está lento”.

Decisión: Si hay swapping, detente y corrige el dimensionamiento de memoria (buffer pools, shared_buffers, work_mem, tablas tmp). Poner swapping a bases de datos es un hobby, no una estrategia.

Task 13 (PostgreSQL): Confirmar uso de archivos temporales (ordenaciones/hash que vierten a disco)

cr0x@server:~$ psql -X -d appdb -c "select datname, temp_files, pg_size_pretty(temp_bytes) as temp_size from pg_stat_database where datname='appdb';"
 datname | temp_files | temp_size
--------+------------+-----------
 appdb  |      41290 | 98 GB

Qué significa: 98 GB de datos temporales sugiere que consultas están vertiendo. Eso suele ser índices pobres, planes malos o work_mem insuficiente para la carga específica (no globalmente).

Decisión: Identifica las consultas que vierten con EXPLAIN (ANALYZE, BUFFERS). Prefiere arreglar la consulta/índice primero. Si aumentas work_mem, hazlo con cuidado para evitar OOM bajo concurrencia.

Task 14 (Percona/MySQL): Validar que el slow query log diga la verdad

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

Qué significa: El slow log está activado y el umbral es lo bastante agresivo para capturar comportamiento p95 (0.2s aquí). Si está puesto a 10s, es básicamente una herramienta sólo para postmortem.

Decisión: Asegura que el logging lento esté activo en producción con un umbral sensato y rotación. Si no, acabarás adivinando qué consulta te dañó.

Task 15 (PostgreSQL): Comprobar rápidamente el lag de replicación

cr0x@server:~$ psql -X -d appdb -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.3.12   | streaming | 00:00:01  | 00:00:02  | 00:00:08

Qué significa: El replay lag es de 8 segundos. Eso puede ser aceptable o desastroso dependiendo de los requisitos de read-your-writes.

Decisión: Si el lag es alto y creciente, revisa I/O/CPU del réplica, consultas largas en réplicas y el volumen de WAL en el primario. Considera mover el reporting fuera de réplicas que sirven lecturas de producto.

Task 16 (MySQL/Percona): Comprobar salud y lag de replicación

cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep 'Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running|Relay_Log_Space'
 Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 37
Relay_Log_Space: 1849423872

Qué significa: El hilo SQL está corriendo pero el lag es de 37 segundos y los relay logs son grandes. La réplica no aplica lo suficientemente rápido.

Decisión: Investiga límites de recursos de la réplica, lecturas pesadas en la réplica y si el apply single-threaded es un cuello de botella (y si la replicación paralela está configurada apropiadamente para tu carga).

Tres mini-historias corporativas desde las trincheras de rendimiento

1) Incidente causado por una suposición equivocada: «El benchmark dijo que Percona era más rápido»

La empresa estaba en crecimiento, con una carga de tipo pagos: muchas transacciones cortas, pero también una molesta rama de reporting que hacía filtros ad-hoc sobre algunas tablas grandes. El líder de ingeniería ejecutó un benchmark: búsquedas simples por clave primaria, algunas inserciones y un conjunto de datos ordenado que cabía cómodamente en memoria. Percona Server mostró números más bonitos que Postgres en la prueba elegida. Se tomó la decisión.

La migración ocurrió. La primera semana fue bien. La tercera semana fue cuando la realidad apareció: las consultas de reporting no eran las del benchmark. Eran joins amplios con filtros selectivos, y los usuarios de negocio cambiaban los filtros constantemente. El optimizador elegía planes “razonables” hasta que la skew de datos creció. Entonces empezó a escanear, a verter y, en general, a convertir la capa de réplicas en un calefactor.

El incidente en sí fue clásico: un informe trimestral se ejecutó durante el pico. El lag de replicación creció. Las lecturas de producto comenzaron a golpear réplicas obsoletas mostrando estado inconsistente. Soporte lo llamó “corrupción de datos”. No era corrupción. Era lag más suposiciones. Los ingenieros persiguieron bugs fantasma durante horas antes de que alguien mirara Seconds_Behind_Master y el slow query log.

Arreglarlo requirió admitir una verdad incómoda: el motor no era el problema; era la comprensión de la carga. Movieron el reporting a un sistema separado y rediseñaron la estrategia de índices. La base de datos se volvió “más rápida” de la noche a la mañana sin cambiar un solo binario—porque dejaron de pedirle a la máquina equivocada que hiciera el trabajo equivocado en el momento equivocado.

2) Optimización que salió mal: «Aumentamos memoria y eliminamos esperas de fsync»

Otra organización usaba Postgres para un pipeline de ingestión de eventos con muchas escrituras. Vieron picos de latencia periódicos. Alguien apuntó a los checkpoints. Cierto: se veían ráfagas de escritura en checkpoints y los p99 tenían un latido.

El ajuste inicial fue sensato: controlar el ritmo de checkpoints, aumentar el tamaño de WAL, vigilar I/O. Pero luego la “optimización” escaló: subieron work_mem globalmente porque unas pocas consultas estaban vertiendo, y aumentaron el límite de conexiones porque el equipo de app quería más paralelismo.

Dos días después, la base de datos comenzó a matarse por OOM bajo carga. No porque work_mem sea «malo», sino porque es por ordenación/por hash por conexión, multiplicado por sesiones concurrentes. Combínalo con demasiadas conexiones y obtienes una bomba de memoria con mecha lenta. Los picos de checkpoint eran el problema visible; la configuración de memoria era el asesino silencioso.

La solución fue aburrida: limitar conexiones y usar pooling, establecer work_mem de forma conservadora y aplicar overrides por rol para las pocas consultas analíticas que lo necesitaban. El equipo aprendió que “más memoria” no es una estrategia de tuning; es una factura que pagas después en uptime.

3) Práctica aburrida pero correcta que salvó el día: «Ensayamos las restauraciones»

Esta no tiene heroísmos. Tiene adultos en la sala.

Una compañía SaaS operaba Percona Server con réplicas y backups nocturnos. También hacía algo poco fashion: simulacros trimestrales de restauración. No solo “backup completado”, sino restaurar en staging, validar consultas a nivel de aplicación y cronometrar pasos de recuperación. El runbook tenía comandos reales y estimaciones reales.

Un día, un bug de firmware en el almacenamiento del primario causó stalls intermitentes de escritura. La base de datos no se cayó; simplemente se volvió poco fiable y lenta. El equipo decidió hacer failover. Durante el failover descubrieron que la cadena de backups más reciente tenía un hueco porque un job de retención estaba mal configurado. Normalmente aquí el incidente se convierte en una noche larga y sudorosa.

En cambio, restauraron con calma desde el último backup conocido bueno, aplicaron binlogs hasta un punto seguro y levantaron un primario de reemplazo. Aún llevó tiempo, pero fue tiempo que ya habían medido. El negocio notó una ventana degradada, no una catástrofe. La práctica que “nadie tiene tiempo para” se pagó sola en un incidente.

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

1) Síntoma: p99 con picos cada pocos minutos (Postgres)

Causa raíz: ráfagas de escritura por checkpoints debido a checkpointing agresivo y presión de WAL; el almacenamiento no puede absorber la ráfaga.

Solución: Aumentar max_wal_size, afinar el pacing de checkpoints, verificar la latencia del almacenamiento y considerar separar WAL a medios más rápidos si aplica.

2) Síntoma: declive constante del rendimiento en días (Postgres)

Causa raíz: autovacuum no da abasto; transacciones largas impiden limpieza; bloat en crecimiento.

Solución: Encontrar transacciones largas, corregir patrones de la app, afinar autovacuum por tabla y programar remediación de bloat (reindex, reescritura) cuando sea necesario.

3) Síntoma: «La CPU está bien pero las consultas son lentas» (ambos)

Causa raíz: espera de I/O y fallos de caché; el conjunto de datos superó la memoria; un nuevo patrón de consultas hace barridos.

Solución: Revisa ratios de caché/lecturas de buffer pool, valida índices, reduce consultas que escanean y mejora almacenamiento o memoria según misses medidos.

4) Síntoma: tormentas de bloqueos tras un despliegue (ambos)

Causa raíz: nuevo alcance de transacciones, falta de índice en el predicado de update o patrones de “select then update” que aumentan conflictos.

Solución: Identificar el bloqueador, añadir el índice correcto, reducir tiempo de retención de bloqueos y rediseñar filas calientes (shard counters, evitar updates globales).

5) Síntoma: réplicas con lag durante picos pero se recuperan fuera de pico (MySQL/Percona)

Causa raíz: el hilo de apply de la réplica no da abasto, lecturas pesadas en la réplica o picos de volumen de binlog por updates masivos.

Solución: Optimizar patrones de escritura (evitar updates multi-row masivos), usar settings de replicación paralela adecuados y aislar reporting de réplicas críticas.

6) Síntoma: Postgres “demasiadas conexiones” y alto cambio de contexto

Causa raíz: la app abre demasiadas conexiones; el overhead de proceso por conexión domina.

Solución: Añadir PgBouncer, limitar tamaños de pool de la app, reducir max_connections y tratar el conteo de conexiones como una métrica de capacidad.

7) Síntoma: MySQL «Sending data» por todas partes

Causa raíz: consultas que devuelven demasiadas filas, índices faltantes o escaneos ligados al disco. A veces también backpressure de red.

Solución: EXPLAIN la consulta, añadir índices covering, paginar y verificar saturación de red frente a lecturas de disco.

8) Síntoma: el rendimiento empeora tras “añadir un índice” (ambos)

Causa raíz: aumentaste la amplificación de escritura; cada insert/update ahora paga el mantenimiento de índice extra. O el optimizador escogió un plan peor.

Solución: Añade solo los índices que puedas costear. Verifica planes de consulta antes/después. Usa índices parciales/por expresión (Postgres) o índices covering de forma reflexiva (MySQL).

Listas de verificación / plan paso a paso

Paso a paso: elegir entre PostgreSQL y Percona Server por rendimiento (sin engañarte)

  1. Escribe tu carga en 10 consultas que importan, con concurrencia y ratios lectura/escritura. Si no puedes, no estás listo para elegir.
  2. Define métricas de éxito: latencia p95/p99 para las consultas principales, throughput a latencia fija, presupuesto de lag de replicación, objetivo de tiempo de recuperación.
  3. Prueba con datos realistas y skew realista. Al menos, excede la RAM para medir comportamiento de almacenamiento.
  4. Prueba modos de fallo: lag de réplica bajo pico, comportamiento de failover y velocidad de restauración.
  5. Instrumenta primero: activa pg_stat_statements o slow logs de MySQL, y recoge métricas del sistema.
  6. Haz el tuning aburrido: dimensionamiento de memoria, sizing de checkpoints/logs, pooling de conexiones y configuraciones sensatas de autovacuum/flush.
  7. Solo entonces compara. Si un motor gana, sabrás por qué—y podrás mantenerlo ganador en producción.

Checklist operativa: mantener Postgres rápido

  • Monitoriza actividad de autovacuum y tuplas muertas; alerta sobre transacciones largas que bloquean vacuum.
  • Limita conexiones y usa pooling para tráfico con picos.
  • Controla crecimiento de archivos temporales; trata los spills como un problema de consulta/índice antes de aumentar memoria.
  • Vigila estadísticas de checkpoints y volumen de WAL; ajusta para escrituras suaves, no para ráfagas heroicas.
  • Ensaya restauraciones y verifica backups con pruebas reales de restauración.

Checklist operativa: mantener Percona Server rápido

  • Dimensiona InnoDB buffer pool para el conjunto de trabajo y verifica con métricas de lectura.
  • Usa slow query logging con un umbral que coincida con tus SLOs.
  • Monitoriza esperas por bloqueos y transacciones largas; ajusta filas calientes y alcance de transacciones.
  • Vigila lag de replicación y crecimiento de relay logs; mantén reporting fuera de réplicas críticas.
  • Valida que las configuraciones de durabilidad coincidan con el riesgo del negocio, no con fantasías de benchmark.

Preguntas frecuentes

1) ¿Percona Server es “solo MySQL”?

Es compatible con MySQL pero trae características de rendimiento e instrumentación y un ecosistema operativo distinto. La compatibilidad es el punto; los extras son la razón por la que la gente lo elige.

2) ¿PostgreSQL es siempre más lento para OLTP?

No. Postgres puede hacer OLTP excelente. Tiende a ser menos tolerante con manejo de conexiones descuidado y vacuum descuidado. Si lo operas bien, es muy competitivo.

3) ¿Cuál es la forma más rápida de saber si estoy I/O-bound?

Revisa iostat -xz para latencia y utilización, y correlaciona con métricas a nivel BD (Postgres blks_read, lecturas del buffer pool de InnoDB). Si la latencia del disco sube con la latencia de consultas, ahí tienes la respuesta.

4) ¿Debo afinar Postgres aumentando mucho shared_buffers?

No a ciegas. shared_buffers importa, pero la caché de página del SO también. Enfócate en el conjunto de trabajo, tendencias de aciertos y evitar scans. Números grandes sin mediciones suelen traer nuevos modos de fallo.

5) ¿Debo simplemente poner un enorme InnoDB buffer pool?

Dimensiónalo para el conjunto de trabajo dejando margen para el SO, otros servicios y picos. Sobredimensionar puede causar swapping o starvación de caché del filesystem, que es una forma cara de volverse más lento.

6) ¿Por qué mis benchmarks muestran ganancias enormes que la producción nunca ve?

Porque probablemente benchmarkeaste caches calientes, datasets pequeños y localidad perfecta. Producción tiene skew, jobs de fondo, backups, cambios de esquema y usuarios haciendo cosas raras en picos.

7) ¿Cuál es mejor para SQL complejo y reporting?

A menudo Postgres, especialmente si aprovechas indexación avanzada y consultas relacionales. Pero “mejor” depende de tus consultas exactas y de si puedes aislar reporting del OLTP.

8) ¿Cómo evito elegir la base de datos equivocada por sensaciones?

Define las consultas principales, prueba a escala realista, prueba bajo concurrencia y en modos de fallo. Toma la decisión basada en comportamiento medible que puedas reproducir.

9) ¿El lag de replicación es un problema de rendimiento o de corrección?

Ambos. El lag es “rendimiento” cuando rompe tus SLOs y “corrección” cuando tu producto asume read-your-writes. Trata el lag como una métrica de primera clase.

Conclusión: pasos siguientes que no te avergonzarán

Si quieres una respuesta real a “PostgreSQL vs Percona Server: ¿cuál es más rápido?”, deja de preguntarlo como si fuera un único número. Pregunta cuál es más rápido para tu carga de trabajo, bajo tus restricciones operativas, con los hábitos de tu equipo y los modos de fallo que no puedes evitar.

  1. Inventaria la carga: top 10 de patrones de consulta, volumen de escrituras, concurrencia y objetivos de latencia.
  2. Instrumenta ambos mundos: Postgres con pg_stat_statements y vistas de actividad; Percona con slow logs y estado de InnoDB, además de métricas del host.
  3. Ejecuta una prueba realista: dataset más grande que la RAM, mismos índices, misma mezcla de consultas, mismas expectativas de durabilidad.
  4. Haz un drill rápido de diagnóstico: introduce carga deliberadamente, encuentra el cuello de botella con la guía y ve qué sistema tu equipo entiende más rápido.
  5. Elige el motor que puedas mantener rápido: no el que gana en una demo, sino el que puedas operar con limpieza a las 3 a.m.
← Anterior
Ubuntu 24.04: Rendimiento se desploma tras la actualización — las 6 primeras comprobaciones que revelan al culpable
Siguiente →
El ingeniero del “bus factor”: cuando una sola persona controla todo el sistema

Deja un comentario