MySQL vs MariaDB: asesinos de consultas en WordPress — cómo arreglar sin reescribir el sitio

¿Te fue útil?

WordPress no se “vuelve lento al azar”. Se vuelve lento de maneras muy específicas y repetibles, casi siempre dentro de la base de datos. Un día tu página principal carga en 400 ms, al día siguiente tarda 8 segundos y los workers de PHP se acumulan como equipaje en un vuelo cancelado.

La parte difícil no es elegir MySQL frente a MariaDB. La parte difícil es atrapar a los asesinos de consultas que WordPress (y sus plugins) introducen silenciosamente: índices faltantes, autoload inflado, LIKEs patológicos, tormentas de bloqueo, thrash de tablas temporales y suposiciones de caché que nunca fueron ciertas. Puedes arreglar la mayor parte sin reescribir el sitio y sin convertir la base de datos en un proyecto artesanal.

MySQL vs MariaDB: qué importa realmente para WordPress

WordPress no es una suite de benchmarks. Es una colección de patrones de consulta previsibles (core) más patrones imprevisibles (plugins) más la ocasional herida autoinfligida (código personalizado, importaciones malas, “optimizaciones” bienintencionadas).

Elegir MySQL o MariaDB importa menos de lo que la gente piensa. Ambos usan InnoDB, ambos pueden hacer cosas muy buenas y ambos pueden hacer cosas muy malas si dejas crecer unas pocas tablas sin límites.

Diferencias rápidas que aparecen en producción

  • Los valores por defecto y el comportamiento por versión difieren. Los valores por defecto de MySQL 8 (y cambios del optimizador) pueden comportarse distinto que MariaDB 10.x/11.x en casos límite. Para WordPress, la mayor parte del dolor sigue siendo esquema y carga de trabajo, no la genialidad del optimizador.
  • Las herramientas de observabilidad difieren. Performance Schema y el esquema sys de MySQL 8 son sólidos y ampliamente documentados. MariaDB tiene equivalentes, pero a veces usarás vistas diferentes o habilitarás plugins distintos.
  • La nostalgia por el query cache es una trampa. Si eliges MariaDB porque escuchaste “query cache ayuda a WordPress”, para. No arreglas un sitio dinámico moderno resucitando un mutex global de 2009.
  • El thread pool puede importar. El thread pool de MariaDB (según la edición/compilación) puede ayudar con churn alto de conexiones. Pero si estás lanzando 800 children de PHP-FPM y cada uno abre su propia conexión, has construido una fábrica de conexiones, no un sitio web.
  • Las semánticas GTID/replicación difieren. Esto afecta más al failover/HA que a la latencia de consultas, pero las caídas de WordPress a menudo se atribuyen a “la base de datos” incluso cuando el disparador fue retraso de replicación.

Qué deberías decidir (en lugar de lealtad a la marca)

Para WordPress, los puntos reales de decisión son:

  • ¿Puedes obtener una versión mayor moderna? MySQL 5.6/5.7 antiguos y builds viejos de MariaDB tienen menos herramientas de diagnóstico y más trampas. Ejecuta algo actual y soportado.
  • ¿Tu equipo conoce cómo manejarlo de guardia? La mejor base de datos es la que tu equipo puede depurar a las 02:00 sin hacer una danza interpretativa sobre un réplica en vivo.
  • ¿Tienes un plan para la deriva de esquema/índices? Las instalaciones de WordPress son copos de nieve. Tus asesinos de consultas no son teóricos: están en ese copo de nieve.

Hechos interesantes y contexto histórico (lo corto y útil)

  1. MySQL fue adquirido por Sun en 2008, y Sun fue adquirido por Oracle en 2010—esta es la genealogía corporativa detrás de muchos debates “MySQL vs MariaDB”.
  2. MariaDB fue iniciada por los creadores originales de MySQL como un fork para mantener un camino comunitario tras la adquisición por Oracle.
  3. WordPress nació en la era de MySQL 4, lo que explica algunas decisiones de esquema de larga duración (como la fuerte dependencia en tablas meta) que no fueron diseñadas para consultas de estilo analítico moderno.
  4. InnoDB se convirtió en el motor por defecto en MySQL 5.5; antes de eso, MyISAM era común. Algunas “consejos de optimización” heredados aún asumen comportamiento MyISAM y son erróneos para WordPress hoy.
  5. MySQL 8 eliminó por completo el query cache; MariaDB mantuvo una forma de él por más tiempo. Esa divergencia alimentó mitos sobre “MariaDB es más rápido para WordPress”, incluso cuando el cuello de botella es PHP o índices faltantes.
  6. Performance Schema maduró dramáticamente en MySQL 5.7 y 8; si lo usaste una vez y lo odiaste, probablemente lo usaste en su adolescencia incómoda.
  7. El diseño de wp_postmeta de WordPress es intencionalmente genérico (pares clave/valor), lo que facilita escribir plugins y hace más difícil mantener la base de datos rápida a escala.
  8. UTF8MB4 se volvió la opción práctica por defecto para WordPress moderno para soportar Unicode completo (incluidos emojis, que tu equipo de marketing usará aunque no te guste).

Los asesinos de consultas en WordPress (y por qué perjudican)

La mayoría de los incidentes de “rendimiento de base de datos en WordPress” se reducen a un pequeño conjunto de modos de fallo. El truco es reconocerlos rápido y aplicar soluciones aburridas.

1) Autoload inflado en wp_options

WordPress carga las opciones con autoload='yes' en cada petición. Cada petición. Si ese conjunto crece a megabytes de arrays serializados—gracias, plugins—has añadido un impuesto a cada vista de página antes incluso de ejecutar una sola consulta “interesante”.

Síntomas: TTFB sube, CPU aumenta, las consultas parecen pequeñas pero frecuentes, las fallas de cache de objetos se vuelven caras y las páginas de administración se sienten como si fueran a través de jarabe.

2) El impuesto de las tablas meta: wp_postmeta y afines

Las tablas meta son flexibles, pero convierten “encontrar posts con X” en “escanear muchas filas con joins repetidos”. Si un plugin construye una consulta con múltiples condiciones meta y sin índices de apoyo, puede fijar la CPU y desgastar las páginas del buffer pool.

3) Índices faltantes o incorrectos (especialmente en meta_key + meta_value)

Los índices del core de WordPress son mínimos. Los plugins suelen consultar columnas que no están indexadas (o están indexadas en el orden equivocado), creando escaneos completos de tabla. A medida que los datos crecen, pasas de “bien” a “en llamas” sin aviso excepto el slow log que no activaste.

4) ORDER BY RAND() y otros crímenes sin alegría

Este merece una mención especial: parece inocente y se siente como un atajo. También es una bomba de tiempo de rendimiento en cualquier tabla con más que “una cantidad linda” de filas.

Broma #1: ORDER BY RAND() es como pedirle a la base de datos que baraje un almacén una caja a la vez.

5) Bloqueos y contención: actualizaciones, transients, cron y comportamiento administrativo

WordPress tiene patrones de escritura que pueden causar contención: limpieza de transients, tareas programadas (WP-Cron), migraciones de plugins y operaciones masivas en administración. Si tienes tráfico y un admin ejecuta una edición masiva, puedes obtener esperas de bloqueo y una acumulación de solicitudes.

6) Tablas temporales y derrames a disco

Cuando MySQL/MariaDB no puede mantener resultados intermedios en memoria, derrama a disco. Si tu tmpdir está en almacenamiento lento o estás saturando IOPS, verás explotar la latencia durante sorts, agrupaciones y joins complejos. WordPress puede desencadenar esto vía búsquedas, plugins de informes y pantallas de administración que “filtran todo”.

7) Confusión de cachés: caché de página vs caché de objetos vs caché de base de datos

El caché en WordPress es un sistema por capas, y los equipos confunden frecuentemente:

  • Caché de página (CDN, proxy inverso) reduce trabajo de PHP y DB para tráfico anónimo.
  • Caché de objetos (Redis/Memcached) reduce lecturas repetidas dentro y entre peticiones.
  • Caché de base de datos (buffer pool) mantiene páginas frecuentemente accedidas en memoria.

Cuando “activas caché” y nada mejora, suele ser porque activaste la capa equivocada o la tasa de aciertos es mala porque autoload/patterns meta generan demasiadas claves únicas.

Guía rápida de diagnóstico (primero/segundo/tercero)

Esta es la guía para cuando el sitio está lento y quieres evitar empeorarlo. El objetivo es identificar si estás limitado por CPU, I/O, bloqueos o conexiones—rápido.

Primero: confirma la clase del cuello de botella (CPU, I/O, bloqueos o conexiones)

  • CPU-bound: CPU de base de datos alta, muchas operaciones “Sending data”, muchos escaneos completos, el buffer pool no ayuda porque las consultas son ineficientes.
  • I/O-bound: latencia de lectura/escritura alta, misses del buffer pool, derrames de tablas temporales a disco, almacenamiento lento o volumen saturado.
  • Lock-bound: muchos hilos en “Waiting for table metadata lock” o “Waiting for row lock”, transacciones de larga duración.
  • Connection-bound: demasiadas conexiones/hilos, conmutación de contexto, max connections alcanzado, estampida de PHP-FPM.

Segundo: captura los principales culpables (no adivines)

  • Habilita/inspecciona el slow query log (ventana corta) o usa vistas de Performance Schema.
  • Extrae SHOW PROCESSLIST / snapshots de actividad de information_schema.
  • Ejecuta EXPLAIN sobre la peor consulta y busca “Using temporary; Using filesort” y escaneos completos.

Tercero: aplica la corrección de más bajo riesgo que elimine la mayor parte del dolor

  • Si es autoload inflado: reduce autoload, limpia transients, confirma la reducción de tamaño.
  • Si son índices faltantes: añade índices compuestos dirigidos (con plan de rollback).
  • Si es contención: encuentra la transacción larga y detenla/evítala; ajusta timeouts con cautela.
  • Si es I/O: aumenta el buffer pool (si la memoria lo permite), ajusta el tamaño de tablas temporales, mueve tmpdir a almacenamiento rápido.
  • Si son conexiones: añade estrategia de pooling (o al menos reduce la concurrencia de PHP-FPM) y establece límites sensatos.

Tareas prácticas: comandos, salida, decisiones (12+)

Estas son tareas reales que puedes ejecutar hoy en un host Linux típico. Cada una incluye: el comando, qué significa la salida típica y qué decisión tomar después.

Task 1: Identify server flavor and version (you can’t tune what you can’t name)

cr0x@server:~$ mysql -e "SELECT VERSION() AS version, @@version_comment AS comment, @@version_compile_machine AS arch;"
+-------------------------+------------------------------+------+
| version                 | comment                      | arch |
+-------------------------+------------------------------+------+
| 8.0.36                  | MySQL Community Server - GPL | x86_64 |
+-------------------------+------------------------------+------+

Qué significa: MySQL 8.0.36. Si esto fuera MariaDB, comment mencionaría MariaDB y los formatos de versión suelen incluir -MariaDB.

Decisión: Usa las herramientas de MySQL 8 (Performance Schema, esquema sys). Si estás en algo antiguo, planifica tiempo para una actualización porque muchos problemas de “lentitud misteriosa” son más fáciles de ver en versiones modernas.

Task 2: Check current connection pressure

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections'; SHOW VARIABLES LIKE 'max_connections';"
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 186   |
+-------------------+-------+
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 412   |
+----------------------+-------+
+-----------------+------+
| Variable_name   | Value|
+-----------------+------+
| max_connections | 500  |
+-----------------+------+

Qué significa: Estás operando cerca del límite en picos. Si PHP-FPM tiene 300 workers y cada uno usa su propia conexión, puedes tocar el techo bajo un pico de tráfico.

Decisión: Si Threads_connected está regularmente > 60–70% de max_connections, reduce la concurrencia de la aplicación y/o añade un proxy/pooler (o al menos conexiones persistentes ajustadas con seguridad). También revisa el thread cache.

Task 3: Confirm whether slow query logging is on (and where)

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

Qué significa: El slow log está desactivado y el umbral es demasiado alto para diagnóstico en WordPress.

Decisión: Actívalo temporalmente con un umbral bajo (0.5–1s) durante el pico, luego bájalo de nuevo. Captura evidencia en lugar de sensaciones.

Task 4: Enable slow query log for a short diagnostic window

cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.5; SET GLOBAL log_queries_not_using_indexes = 'ON';"
...output omitted...

Qué significa: Nuevas consultas lentas serán registradas. log_queries_not_using_indexes puede ser ruidoso; lo estás usando como linterna, no como estilo de vida.

Decisión: Déjalo correr 10–30 minutos bajo tráfico típico y luego analiza. Desactiva o sube los umbrales después de tener suficientes muestras.

Task 5: Summarize slow log offenders quickly

cr0x@server:~$ sudo mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
Reading mysql slow query log from /var/log/mysql/slow.log
Count: 212  Time=3.12s (661s)  Lock=0.00s (0s)  Rows=10234.0 (2169608), root[root]@localhost
  SELECT SQL_NO_CACHE * FROM wp_postmeta WHERE meta_key = 'S' AND meta_value LIKE '%S%' ORDER BY meta_id DESC LIMIT N
Count: 88  Time=1.44s (126s)  Lock=0.03s (2s)  Rows=1.0 (88), wp[wp]@10.0.2.10
  SELECT option_value FROM wp_options WHERE autoload = 'yes'

Qué significa: Tienes un patrón de búsqueda en meta que hace un LIKE '%...%' sobre meta_value (dolor) y una consulta frecuente de autoload que puede estar tirando un payload enorme.

Decisión: Para la búsqueda meta: corrige la función o constriéndela; los índices no salvarán un LIKE con comodín inicial en una columna de texto largo. Para autoload: mide el tamaño de autoload y podalo.

Task 6: Check top current queries and lock states

cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;"
+-----+------+----------------+------+---------+------+------------------------------+-----------------------+
| Id  | User | Host           | db   | Command | Time | State                        | Info                  |
+-----+------+----------------+------+---------+------+------------------------------+-----------------------+
| 821 | wp   | 10.0.2.10:53214| wpdb | Query   |  12  | Waiting for table metadata lock | ALTER TABLE wp_postmeta ... |
| 844 | wp   | 10.0.2.11:53302| wpdb | Query   |   8  | Sending data                 | SELECT ... FROM wp_posts ...|
| 865 | wp   | 10.0.2.12:54100| wpdb | Query   |  15  | Waiting for row lock         | UPDATE wp_options SET ...   |
+-----+------+----------------+------+---------+------+------------------------------+-----------------------+

Qué significa: Hay un ALTER TABLE bloqueando metadata y un UPDATE esperando bloqueos de fila. Esto es un incidente de contención, no un incidente de “necesita más RAM”.

Decisión: No hagas DDL durante el pico. Si un plugin está migrando esquema en vivo, pausa/retrocede o hazlo en una ventana de mantenimiento. Encuentra la transacción bloqueante a continuación.

Task 7: Find InnoDB lock waits and the blocker

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,160p'
...
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 18392019, ACTIVE 14 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 865, OS thread handle 140055, query id 998201 10.0.2.12 wpdb updating
UPDATE wp_options SET option_value = '...' WHERE option_name = '_transient_timeout_wc_count_comments'
*** (2) TRANSACTION:
TRANSACTION 18392011, ACTIVE 22 sec fetching rows
...

Qué significa: Los transients están involucrados; un plugin (aquí, comportamiento similar a WooCommerce) está golpeando wp_options y chocando.

Decisión: Mueve transients a Redis/Memcached (caché de objetos persistente) para reducir escrituras en la base de datos y/o reduce la concurrencia de cron. También asegura que wp_options.option_name esté indexado (debería estarlo).

Task 8: Measure buffer pool pressure (are you I/O bound?)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages%';"
+----------------------------------+----------+
| Variable_name                    | Value    |
+----------------------------------+----------+
| Innodb_buffer_pool_read_requests | 94811234 |
| Innodb_buffer_pool_reads         | 2211903  |
+----------------------------------+----------+
+----------------------------------+--------+
| Variable_name                    | Value  |
+----------------------------------+--------+
| Innodb_buffer_pool_pages_total   | 524288 |
| Innodb_buffer_pool_pages_free    | 120    |
| Innodb_buffer_pool_pages_dirty   | 18240  |
+----------------------------------+--------+

Qué significa: Reads vs read requests te da una aproximación de la tasa de aciertos: 1 - reads/read_requests. Aquí es aceptable, pero pages_free es pequeño (buffer pool totalmente usado).

Decisión: Si tu working set no entra y la latencia de disco es alta, aumenta innodb_buffer_pool_size (dentro de límites seguros de RAM). Si la tasa de aciertos ya es buena, enfócate en la eficiencia de consultas y en bloqueos.

Task 9: Confirm tmp table spills (classic hidden latency)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables'; SHOW VARIABLES LIKE 'tmp_table_size'; SHOW VARIABLES LIKE 'max_heap_table_size';"
+-------------------------+--------+
| Variable_name           | Value  |
+-------------------------+--------+
| Created_tmp_disk_tables | 92341  |
| Created_tmp_tables      | 145220 |
+-------------------------+--------+
+-------------------+----------+
| Variable_name     | Value    |
+-------------------+----------+
| tmp_table_size    | 16777216 |
+-------------------+----------+
+-------------------+----------+
| Variable_name     | Value    |
+-------------------+----------+
| max_heap_table_size | 16777216 |
+-------------------+----------+

Qué significa: Una alta proporción de tablas temporales a disco suele correlacionar con sorts/agrupar lentos. Límites de 16MB son conservadores.

Decisión: Si tienes RAM disponible, incrementa ambos tamaños juntos (capan efectivamente tablas tmp en memoria). También localiza qué consultas lo causan—no solo subas parámetros esperando suerte.

Task 10: Inspect the worst query plan (EXPLAIN, not prayer)

cr0x@server:~$ mysql -e "EXPLAIN SELECT p.ID FROM wp_posts p JOIN wp_postmeta pm ON pm.post_id=p.ID WHERE pm.meta_key='color' AND pm.meta_value='blue' AND p.post_type='product' AND p.post_status='publish' ORDER BY p.post_date DESC LIMIT 20;"
+----+-------------+-------+------+---------------+------+---------+-------------------+--------+-----------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref               | rows   | Extra                       |
+----+-------------+-------+------+---------------+------+---------+-------------------+--------+-----------------------------+
|  1 | SIMPLE      | pm    | ref  | meta_key      | meta_key | 767  | const             | 980000 | Using where; Using filesort |
|  1 | SIMPLE      | p     | eq_ref | PRIMARY     | PRIMARY | 8     | wpdb.pm.post_id   | 1      | Using where                 |
+----+-------------+-------+------+---------------+------+---------+-------------------+--------+-----------------------------+

Qué significa: Está escaneando casi un millón de filas en wp_postmeta para ese meta_key y luego filtrando por meta_value. El filesort sugiere trabajo extra para ordenar.

Decisión: Añade un índice compuesto que coincida con el patrón del filtro (comúnmente (meta_key, meta_value(…prefijo…), post_id) dependiendo de tipos de datos y selectividad). Luego vuelve a ejecutar EXPLAIN y valida la reducción de filas.

Task 11: Measure autoload payload size in wp_options

cr0x@server:~$ mysql -e "SELECT ROUND(SUM(LENGTH(option_value))/1024/1024,2) AS autoload_mb, COUNT(*) AS autoload_rows FROM wp_options WHERE autoload='yes';"
+-------------+--------------+
| autoload_mb | autoload_rows|
+-------------+--------------+
| 14.73       | 3129         |
+-------------+--------------+

Qué significa: ~15MB de opciones se cargan en cada petición. Eso no es “un pequeño overhead”. Es una factura recurrente.

Decisión: Identifica las filas de autoload más grandes y desactiva autoload para opciones no esenciales, o elimina opciones obsoletas de plugins muertos. Apunta a < 1–2MB para la mayoría de sitios; sitios más grandes pueden justificar más, pero 15MB suele ser accidental.

Task 12: Find the biggest autoload offenders (safely)

cr0x@server:~$ mysql -e "SELECT option_name, ROUND(LENGTH(option_value)/1024,1) AS kib FROM wp_options WHERE autoload='yes' ORDER BY LENGTH(option_value) DESC LIMIT 15;"
+-----------------------------------+--------+
| option_name                        | kib    |
+-----------------------------------+--------+
| myplugin_big_cache_blob            | 2048.4 |
| theme_mods_mytheme                 | 612.7  |
| rewrite_rules                      | 512.2  |
| widget_custom_html                 | 498.9  |
+-----------------------------------+--------+

Qué significa: Un plugin está metiendo “cache” de varios megabytes en autoload. Además, algunas opciones core (como rewrite_rules) pueden ser grandes pero normalmente no de varios megabytes.

Decisión: Para blobs de plugins: establece autoload a ‘no’ (si es seguro), migra a caché de objetos o evita que el plugin persista datos computados gigantes. Para opciones core: considera regenerar las rewrite rules (con cuidado) si están corruptas.

Task 13: Check table sizes and growth (spot the real monster)

cr0x@server:~$ mysql -e "SELECT table_name, ROUND((data_length+index_length)/1024/1024,1) AS total_mb FROM information_schema.tables WHERE table_schema='wpdb' ORDER BY (data_length+index_length) DESC LIMIT 10;"
+--------------+----------+
| table_name   | total_mb |
+--------------+----------+
| wp_postmeta  | 8420.5   |
| wp_posts     | 2150.2   |
| wp_options   | 390.4    |
| wp_comments  | 280.7    |
+--------------+----------+

Qué significa: wp_postmeta domina. Eso es normal a escala—pero significa que las decisiones de índices importan y “consultas meta por todas partes” te castigarán.

Decisión: Enfoca la optimización en patrones de acceso de wp_postmeta y reduce las consultas pesadas en endpoints de alto tráfico (búsqueda, filtros, listados de productos).

Task 14: Check for long-running transactions (hidden lock bombs)

cr0x@server:~$ mysql -e "SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND,trx_started,NOW()) AS trx_age_s, trx_mysql_thread_id FROM information_schema.innodb_trx ORDER BY trx_started ASC LIMIT 10;"
+--------+----------+---------------------+----------+---------------------+
| trx_id | trx_state| trx_started         | trx_age_s| trx_mysql_thread_id |
+--------+----------+---------------------+----------+---------------------+
| 183910 | RUNNING  | 2025-12-31 10:21:02 | 1843     | 512                 |
+--------+----------+---------------------+----------+---------------------+

Qué significa: Una transacción ha estado abierta ~30 minutos. En el mundo WordPress, eso es sospechoso. Puede bloquear purges, causar crecimiento de listas de historia y amplificar contención de bloqueos.

Decisión: Identifica la consulta/usuario que la mantiene (processlist) y corrige el comportamiento de la aplicación (a menudo un informe/export, migración de plugin o trabajo administrativo).

Task 15: Check disk latency and saturation (because physics still runs the datacenter)

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

avg-cpu:  %user %nice %system %iowait  %steal  %idle
          22.1  0.0   6.4    18.8     0.0   52.7

Device            r/s     w/s   rkB/s   wkB/s  await  svctm  %util
nvme0n1         980.0   420.0 52000.0 18000.0  12.4   0.8   98.7

Qué significa: Utilización muy alta y await significativo. Si las tablas temporales derraman o los misses del buffer pool suben, sentirás esto inmediatamente como latencia en el sitio.

Decisión: Reduce trabajo en disco primero (mejores índices, ajuste del buffer pool, evitar derrames tmp) antes de comprar discos más rápidos. Si ya estás en NVMe y sigue al máximo, probablemente estás haciendo consultas poco sensatas a escala.

Puntos críticos del esquema: wp_options, wp_postmeta, wp_posts

wp_options: autoload y transients

Qué te mata: autoload inflado + churn de transients.

  • Autoload: trátalo como configuración de arranque, no como almacenamiento de caché.
  • Transients: si no tienes un caché de objetos persistente, los transients van a wp_options y se convierten en escrituras bajo tráfico.

Estrategia de arreglo:

  • Mide el tamaño de autoload, recórtalo y mantenlo bajo.
  • Mueve transients a Redis/Memcached (plugin de caché de objetos persistente + backend).
  • Evita que los plugins escriban blobs grandes como opciones autoloaded.

wp_postmeta: haz que las consultas meta paguen su renta

Qué te mata: joins repetidos y filtros sin indexar.

Los índices del core de WordPress típicamente incluyen post_id y meta_key. Los plugins a menudo filtran por meta_value, ordenan por él o hacen consultas de rango sobre valores numéricos almacenados como cadenas. Así es como terminas con una base de datos que “está bien hasta que no lo está”.

Estrategia de arreglo:

  • Añade índices compuestos selectivos que coincidan con tu carga real (no una orgía de “indexa todo”).
  • Para rangos numéricos: asegúrate de que las consultas hagan casts consistentes y considera almacenar valores numéricos en tablas dedicadas si alguna vez tienes oportunidad. Si no, indexa un prefijo o replantea la funcionalidad.
  • Para búsquedas de texto en meta_value: deja de hacer LIKE con comodín inicial. Usa soluciones de búsqueda adecuadas o limita el alcance.

wp_posts: patrones previsibles, fácil de perjudicar

Consultas sobre post_type, post_status, post_date y joins de taxonomía son comunes. Los índices del core suelen ayudar, pero consultas personalizadas que ordenan por expresiones sin indexar o que unen meta excesiva aún pueden causar derrames a tmp y filesorts.

Broma #2: Una “optimización” de base de datos que añade cinco joins para evitar una consulta es como tomar tres vuelos para evitar una escala.

Ajustes que funcionan en ambos (y qué difiere)

Aquí va la regla opinada: ajusta con evidencia, no con sensaciones de posts en blogs. Para WordPress, la mayoría de ganancias vienen del esquema y de modelar la carga. Después de eso, dimensionamiento de memoria y límites de concurrencia.

Comienza con estos parámetros (porque realmente importan)

1) innodb_buffer_pool_size

Si tu servidor DB está dedicado a MySQL/MariaDB, una línea base común es 60–75% de la RAM para el buffer pool. En máquinas de uso mixto, sé conservador. El rendimiento de WordPress a menudo mejora más por encajar páginas calientes en memoria que por cualquier otro cambio aislado.

2) innodb_log_file_size / dimensionado del redo log

Logs de redo demasiado pequeños pueden causar presión de checkpoint durante ráfagas de escritura (imports, tormentas de cron). Demasiado grandes pueden aumentar el tiempo de recuperación tras crash. Redimensión según patrones de escritura; no lo pongas en “lo que alguien tuiteó”.

3) tamaño de tablas temporales y ubicación de tmpdir

Si las tablas temporales a disco son altas, aumentar tmp_table_size y max_heap_table_size (juntos) puede ayudar—hasta cierto punto. Asegura también que tu tmpdir esté en almacenamiento local rápido, no en un montaje de red lento.

4) Concurrencia: mantén conexiones sensatas

WordPress no se beneficia de concurrencia ilimitada. Si saturas la base de datos, la latencia sube y el throughput frecuentemente cae. Limita los workers de PHP-FPM basándote en la capacidad medida de la DB, no en “nucleos de CPU por 20”.

5) Ajustes de observabilidad (para poder demostrar mejoras)

Activa lo que necesites cuando lo necesites: slow logs, consumidores de Performance Schema (donde proceda) y métricas OS básicas. Déjate migas de pan.

Dónde difieren MySQL y MariaDB en la práctica

  • Diferencias del optimizador: ocasionalmente verás que un plan diverge. Cuando eso pasa, la solución suele ser la misma: añade el índice correcto, reescribe la consulta más terrible (a menudo dentro de un plugin) o cambia el patrón de acceso.
  • Instrumentación: en MySQL 8, apóyate en Performance Schema y sys schema. En MariaDB, puede que prefieras information_schema y el estado del motor, más vistas propias de MariaDB según la compilación.
  • Disponibilidad de thread pool: puede cambiar cómo se comporta el servidor bajo tormentas de conexiones. No excusa las tormentas de conexiones.

Una idea de fiabilidad útil para tener en mente, atribuida a John Allspaw (parafraseada): “La fiabilidad es lo que haces antes del incidente, no durante él”.

Tres mini-historias corporativas desde la trinchera

Mini-historia 1: El incidente causado por una suposición equivocada

Manejaban un sitio de contenido ocupado con WordPress y un plugin de comercio popular añadido. El tráfico no era masivo, pero era picado: campañas, envíos de email y la ocasional mención en un foro grande.

Un nuevo manager de ingeniería promovió un esfuerzo de “estandarización” de la base de datos. El plan: reemplazar MariaDB por MySQL 8 en toda la flota porque “MySQL 8 es más rápido y moderno”. Eso no era una locura. La suposición equivocada fue: la base de datos es intercambiable mientras el esquema sea igual.

Migraron limpiamente, las pruebas pasaron y la primera semana se veía bien. Entonces empezó una venta. La latencia del checkout pasó de sub-segundo a varios segundos y empezaron a ver “Error establishing a database connection”. El equipo culpó al nuevo motor y comenzó a revertir en pánico.

El problema real: durante la migración también cambiaron modos SQL y valores por defecto. Una consulta de un plugin que había ido tirando con conversiones implícitas ahora obtuvo un plan distinto y empezó a usar una ruta de índice terrible. Bajo carga, causó derrames a tmp y saturó I/O. La misma consulta en MariaDB había tenido “suerte” y elegido un plan menos malo.

La solución fue poco glamorosa: añadir un índice compuesto que coincidiera con el filtro y ajustar la consulta del plugin para evitar un sort computado. Después de eso, MySQL 8 funcionó bien. La lección: los cambios de versión exponen problemas latentes de esquema. La base de datos no los traicionó; simplemente dejó de tolerar el desorden.

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

Otra compañía tenía el problema clásico: búsqueda en wp-admin y filtrado de productos lentos. Alguien propuso “simplemente añade índices en todo wp_postmeta”. Crearon múltiples índices compuestos anchos, incluidos prefijos largos de meta_value, y los desplegaron en horario laboral porque “DDL online es seguro ahora”.

Por un momento, las búsquedas se aceleraron. Luego la latencia de escrituras subió. Imports y jobs de cron empezaron a fallar por timeout. La CPU de la base de datos subió y se quedó ahí. El uso de disco se infló porque cada nuevo índice agrandó la tabla, y el buffer pool ya no podía mantener el working set caliente.

Peor aún: algunas consultas no usaban los nuevos índices, porque la forma de los predicados no coincidía con el orden del índice. Otras los usaban pero pagaban un coste alto en mantenimiento en cada escritura. Habían optimizado una pequeña fracción de lecturas a costa de gravar cada escritura y cada línea de caché.

Se recuperaron haciendo lo que debieron hacer primero: identificar los 5 patrones de consulta principales e indexar solo esos, con el menor ancho posible. Eliminieron índices inútiles, planificaron los cambios de esquema correctamente y movieron características con mucho tránsito a Redis.

La lección duradera: “más índices” no es una estrategia. Es una forma de convertir el dolor de lectura en dolor de escritura mientras te quedas sin RAM.

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

Una organización de medios tenía un despliegue multisite y un pequeño equipo SRE que insistía en dos prácticas “aburridas”: (1) slow query log activado con un umbral razonable, rotado y archivado, y (2) una revisión semanal de los principales ofensores con los dueños de plugins.

La gente puso los ojos en blanco porque todo parecía bien la mayor parte del tiempo. Entonces una versión nueva de un plugin cambió cómo manejaba posts relacionados. Introdujo una consulta que unía wp_posts con wp_postmeta varias veces y añadió un sort sobre una expresión sin índice.

Porque el slow log ya estaba activado, el equipo vio el nuevo ofensor en horas. No necesitaron una sala de guerra ni novela detectivesca. Tenían la huella de la consulta y una línea base antes/después.

La corrección fue quirúrgica: un índice, un cambio de configuración del plugin para limitar el pool de relacionados y una opción de rollback si el índice causaba dolor en escrituras. Los usuarios no notaron. La dirección nunca se enteró, que es el mayor cumplido que da producción.

Eso es lo que compra la corrección aburrida: menos fines de semana heroicos.

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

1) Síntoma: la página de inicio lenta, pero la CPU de la base de datos baja

Causa raíz: acumulación de conexiones y esperas de bloqueo; las solicitudes están esperando, no computando.

Solución: revisa SHOW PROCESSLIST por estados de bloqueo; encuentra y detén transacciones largas; programa DDL fuera de horas pico; reduce la concurrencia de PHP-FPM.

2) Síntoma: páginas de administración (especialmente “Productos” u “Órdenes”) muy lentas

Causa raíz: filtrado y ordenación con muchas metas causando derrames a tmp o escaneos completos.

Solución: slow log + EXPLAIN de la consulta administrativa; añade un índice compuesto para el predicado dominante; aumenta límites de tablas temporales si es necesario; desactiva columnas/filtros costosos en vistas de lista de administración.

3) Síntoma: picos repentinos de I/O de escritura y esperas de bloqueo cada pocos minutos

Causa raíz: ráfagas de WP-Cron, limpieza de transients o tareas programadas de plugins.

Solución: mueve cron a system cron; asegura que solo haya un runner; mueve transients a caché de objetos persistente; reduce la frecuencia de tareas programadas donde sea seguro.

4) Síntoma: la búsqueda es lenta y empeora con el crecimiento de contenido

Causa raíz: consultas LIKE '%term%' en campos de texto grandes, a menudo en postmeta.

Solución: deja de usar LIKE con comodín inicial a escala. Usa full-text search apropiado donde aplique, limita el alcance de búsqueda o externaliza la búsqueda. Los índices no rescatarán fiable este patrón.

5) Síntoma: “Error establishing a database connection” bajo carga

Causa raíz: max connections alcanzado, overhead de scheduling de hilos o DB atascada en I/O/bloqueos y las conexiones expiran.

Solución: mide Threads_connected, Max_used_connections; reduce workers de PHP-FPM; añade retropresión; asegura que la DB no esté saturada de I/O; evita transacciones largas.

6) Síntoma: añadir un índice empeoró el rendimiento

Causa raíz: overhead de mantenimiento de índices, working set inflado o el optimizador eligiendo el índice equivocado.

Solución: valida con EXPLAIN y estadísticas de consultas; elimina índices no usados/de poco valor; mantiene índices estrechos y alineados con predicados; considera histogramas (MySQL) solo después de lo básico.

7) Síntoma: la base de datos es rápida, pero la replicación se atrasa durante el tráfico

Causa raíz: carga con muchas escrituras (options/transients), transacciones grandes o limitaciones de aplicación en la réplica según la configuración.

Solución: reduce escrituras (caché de objetos persistente), evita transacciones enormes, ajusta configuración de replicación según motor/versión; no uses réplicas para lecturas si siempre van retrasadas.

8) Síntoma: stalls intermitentes durante backups o jobs analíticos

Causa raíz: job de backup/ETL causando contención de I/O o lecturas consistentes de larga duración que impactan purge/undo.

Solución: programa backups fuera de pico; usa métodos de backup no bloqueantes; limita; asegura que undo/redo estén dimensionados para la carga; monitorea I/O y edad de transacciones.

Listas de verificación / plan paso a paso

Paso a paso: estabilizar una base de datos WordPress lenta sin reescribir

  1. Define qué significa “lento”. Captura latencia p95/p99 de páginas, CPU DB, await de disco y conexiones activas durante una ventana mala.
  2. Activa el slow query logging brevemente. Usa umbral 0.5–1s por 10–30 minutos bajo tráfico real.
  3. Identifica las 3 huellas de consulta principales. No persigas la cola larga.
  4. Ejecuta EXPLAIN en la peor consulta. Busca escaneos completos, filesort y uso de tablas temporales.
  5. Arregla el mayor “impuesto siempre presente” primero. Normalmente autoload inflado o una consulta meta caliente en un endpoint de alto tráfico.
  6. Añade un índice dirigido a la vez. Valida la mejora y vigila la latencia de escrituras.
  7. Mueve transients a un caché de objetos persistente. Esto reduce churn de escritura y contención de bloqueos en wp_options.
  8. Controla la concurrencia. Limita workers de PHP-FPM a lo que la DB puede manejar; evita estampidas de conexiones.
  9. Revisa de nuevo derrames a tablas temporales y estadísticas del buffer pool. Ajusta memoria solo después de que la forma de las consultas sea razonable.
  10. Devuelve los umbrales del slow log a la normalidad. Manténlo habilitado en un valor sensato y rota los logs.
  11. Documenta lo que cambiaste. No por burocracia—para que el próximo incidente no sea arqueología.

Checklist: antes de añadir un índice

  • ¿Tienes el texto exacto de la consulta (o la huella normalizada)?
  • ¿Ejecutaste EXPLAIN y capturaste estimaciones de “rows”?
  • ¿Puedes probar en staging con volumen de datos similar a producción?
  • ¿Conoces el impacto en escrituras (imports, cron, checkouts)?
  • ¿Tienes plan de rollback (drop index) y ventana de mantenimiento si es necesario?

Checklist: limpieza de autoload con seguridad

  • Mide MB actuales de autoload y los principales ofensores.
  • Confirma qué opciones pertenecen a plugins/temas desinstalados.
  • Cambia flags de autoload con cautela (prefiere ajustes del plugin si están disponibles).
  • Vuelve a medir MB de autoload y vigila la latencia frontend.
  • Mantén una snapshot/backup antes de cambios masivos.

Preguntas frecuentes

1) ¿Debo elegir MySQL o MariaDB para el rendimiento en WordPress?

Elige el que puedas ejecutar en una versión actual y soportada con fuerte familiaridad en guardia. Para WordPress, la higiene de esquema/consultas supera la elección de motor la mayoría de los días.

2) ¿MariaDB es “más rápido para WordPress” por el query cache?

No. El query cache no es la solución moderna que la gente piensa, y MySQL 8 lo eliminó por buenas razones. Usa caché de página/objetos y arregla patrones de consulta en su lugar.

3) ¿Cuál es la única mayor corrección de base de datos para muchos sitios WordPress?

La limpieza de autoload en wp_options, además de un caché de objetos persistente para reducir churn de transients. No es glamoroso, pero suele ser el ROI más alto.

4) Habilité Redis como caché de objetos; ¿por qué MySQL sigue lento?

Porque Redis no arregla consultas meta sin índice, derrames a tmp o contención por transacciones largas. Confirma la tasa de aciertos del caché y sigue diagnosticando las consultas lentas principales.

5) ¿Puedo arreglar consultas meta lentas sin reescribir el plugin?

A menudo sí: añade uno o dos índices compuestos dirigidos y reduce el alcance de filtros/ordenaciones vía configuración. Si el plugin hace LIKE '%term%' en meta_value, puede que necesites cambiar la funcionalidad, no el índice.

6) ¿Aumentar innodb_buffer_pool_size siempre es seguro?

Sólo es seguro si el host tiene RAM de sobra para el SO, cache de sistema de archivos y otros servicios. Un tamaño excesivo provoca swapping, y el swapping convierte las bases de datos en obras de arte de rendimiento (malas).

7) ¿Por qué veo “Waiting for table metadata lock”?

Usualmente porque alguien está ejecutando DDL (ALTER TABLE) mientras el tráfico continúa, o una transacción larga está bloqueando cambios de metadata. Programa DDL fuera de pico y evita transacciones largas.

8) ¿Cómo sé si estoy limitado por I/O o por CPU?

Revisa métricas a nivel OS de disco (await, %util) y lecturas/misses del buffer pool de InnoDB. Await alto y misses altos apuntan a I/O. CPU alta con muchos escaneos completos apunta a ineficiencia de consultas.

9) ¿Debo ejecutar lecturas de WordPress en réplicas?

Sólo si puedes tolerar el lag de réplica y has confirmado que tu carga es suficientemente lectora para beneficiarse. WordPress tiene escrituras sorprendentes (options, sessions/transients por plugins), que pueden mantener réplicas retrasadas durante picos.

10) ¿Qué hago si la consulta lenta está dentro del core de WordPress?

Es raro que el core sea el principal ofensor a escala; suele ser un plugin o una forma de datos específica (meta/options masivas). Si realmente es el core, comienza indexando y caché en lugar de bifurcar el core.

Conclusión: próximos pasos que puedes hacer esta semana

MySQL versus MariaDB no es tu problema de primer orden. Tu problema de primer orden es que las cargas de WordPress castigan esquemas y suposiciones descuidadas, y los plugins son profesionales en generar ambos.

Haz esto, en este orden:

  1. Activa el slow query logging por una ventana controlada y extrae los principales ofensores.
  2. Mide el tamaño de autoload de wp_options y recórtalo agresiva pero seguramente.
  3. Ejecuta EXPLAIN en las peores consultas meta y añade índices compuestos dirigidos—uno a la vez.
  4. Mueve transients a un caché de objetos persistente y toma en serio el cron (un runner, programación predecible).
  5. Pon límites a la concurrencia para que la base de datos no sea estampida hasta timeouts.
  6. Revisa disco y métricas de tablas temporales. Si aún derramas a disco, arregla eso a continuación.

Si no haces nada más: deja de adivinar. Enciende las luces (slow log + métricas básicas) y deja que la base de datos te diga la verdad. Usualmente lo hace.

← Anterior
Licenciamiento de VMware ESXi en 2026: qué cambió, cuánto cuesta y las mejores alternativas (incluye Proxmox)
Siguiente →
Proxmox “pve-apt-hook failed”: por qué se bloquean las actualizaciones y cómo desbloquearlas de forma segura

Deja un comentario