Cuando un WordPress empieza a ir lento el panel de administración se arrastra, WooCommerce tarda una eternidad en cargar variaciones de producto o cualquier acción cotidiana como pasar de página o añadir algo al carrito se convierte en una prueba de paciencia, la base de datos casi siempre suele ser mi primer sospechoso.
Pero ojo, no siempre es cuestión de que tengas la base de datos «sucia» o llena de basura acumulada. Eso es una parte del problema, pero hay otra que es igual de importante o más, las consultas SQL que se ejecutan contra esa base de datos.
Hay tres tipos de consultas problemáticas, y cada una machaca tu servidor de una forma distinta:
- Consultas lentas: tardan demasiado en ejecutarse porque escanean tablas enormes sin índices, usan búsquedas con comodines o hacen JOINs pesados entre varias tablas. Una sola consulta lenta puede bloquear todo lo demás.
- Consultas agresivas: puede que no tarden tanto en ejecutarse, pero devuelven cantidades enormes de datos a la memoria RAM del servidor. Piensa en un plugin que hace
SELECT *sobre una tabla con millones de filas sin poner unLIMIT, o una precarga de caché que lanza consultas masivas de golpe. - Consultas excesivas o frecuentes: individualmente son rápidas, pero se disparan tantas veces por cada carga de página que el efecto acumulado revienta el servidor. El típico caso de un tema o plugin que hace una consulta por cada entrada dentro de un bucle, en vez de traer todo de una sola vez.
Muchas veces el problema es una combinación de las tres, o sea, una consulta que es lenta, se ejecuta muchas veces y además devuelve más datos de los que necesita. Ahí es donde la web se cae pero ya, sin avisar, y luego todo son lloros .
En este tutorial quiero ayudarte a ver cómo identificar exactamente qué consultas están causando problemas en tu base de datos, cómo entender por qué son problemáticas y cómo solucionarlas. Te daré varias opciones según el acceso que tengas a tu servidor, desde un simple plugin hasta comandos por SSH, pasando por phpMyAdmin y códigos PHP que puedes usar sin depender de nadie.
Importante: antes de tocar nada en tu base de datos, haz siempre una copia de seguridad completa. Cualquier operación que hagas directamente sobre la base de datos puede ser irreversible si algo sale mal.
Herramientas para encontrar las consultas problemáticas
Lo primero es lo primero, y antes de arreglar nada necesitas saber qué está pasando exactamente. No vale con suponer que «será tal plugin» o «será que tengo muchas revisiones», necesitas datos concretos.
Dependiendo del nivel de acceso que tengas a tu servidor, tienes varias opciones, así que vamos de menos a más acceso.
Con plugin: Query Monitor
Si solo puedes instalar plugins y no tienes acceso al servidor, Query Monitor es tu mejor aliado. Es gratuito, está en el repositorio oficial y te da una cantidad brutal de información sobre lo que pasa por debajo cada vez que se carga una página.
Instálalo, actívalo y recarga la página que va lenta. Verás una barra nueva en la parte superior del admin con datos de rendimiento. Haz clic en ella y ve directamente a la pestaña Queries.
Lo que tienes que mirar para cada tipo de problema:
- Para encontrar consultas lentas: ordena la lista por la columna de tiempo (duración). Las que estén por encima de 0,05 segundos ya merecen atención. Las que superen 0,5 segundos son un problema serio. Query Monitor las marca en un color distinto para que las veas rápido.
- Para encontrar consultas excesivas: mira el número total de consultas arriba del todo. Un WordPress limpio puede hacer entre 20 y 40 consultas por página. Si ves más de 100, algo va mal. Ve a Queries by Component para ver cuántas hace cada plugin, el tema y el núcleo de WordPress. Así pillas al responsable enseguida. Mira también la pestaña Duplicate Queries, que te muestra consultas que se repiten exactamente iguales, lo que indica que algún componente está pidiendo los mismos datos varias veces sin necesidad.
- Para encontrar consultas agresivas: fíjate en las que devuelven un número muy alto de filas (columna Rows). Una consulta que devuelve miles de filas cuando la página solo necesita mostrar 10 resultados es una señal clara de que algo no está bien optimizado.
Detalle importante: Query Monitor solo te muestra lo que pasa en la carga de página actual. Si el problema ocurre con picos de tráfico, con el cron de WordPress o con procesos en segundo plano, no lo vas a ver aquí. Para eso necesitas las otras herramientas.
Con PHP: detecta consultas lentas sin acceso SSH
Si no tienes acceso SSH y quieres algo más permanente que Query Monitor, que solo te muestra datos de la página actual, puedes usar un mu-plugin que registre automáticamente las consultas problemáticas en un archivo de registro (log).
Lo primero es activar SAVEQUERIES en tu archivo wp-config.php. Añade esta línea antes de donde dice "That's all, stop editing!":
define( 'SAVEQUERIES', true );
Esto hace que WordPress guarde en memoria todas las consultas SQL que ejecuta, junto con el tiempo que tarda cada una y la función que la ha llamado. Pero por sí solo no hace nada visible, necesitas algo que lea esos datos y registre los problemáticos.
Crea un archivo llamado ayudawp-log-slow-queries.php y súbelo a la carpeta /wp-content/mu-plugins/ de tu instalación de WordPress (si la carpeta no existe, créala).
Este es el código:
<?php
/**
* AyudaWP - Log de consultas SQL lentas, agresivas o excesivas.
*
* Registra en un archivo de log las consultas que superen
* el umbral de tiempo definido y alerta cuando el número
* total de consultas por página es excesivo.
*
* Personalizable: ajusta los umbrales según tu caso.
* Uso temporal: desactívalo cuando termines de diagnosticar.
*
* @package AyudaWP
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Solo registrar si SAVEQUERIES está activo.
if ( ! defined( 'SAVEQUERIES' ) || ! SAVEQUERIES ) {
return;
}
/**
* Umbral en segundos para considerar una consulta como lenta.
* Ajústalo según tu caso. 0.05 es un buen punto de partida.
*/
define( 'AYUDAWP_SLOW_QUERY_THRESHOLD', 0.05 );
/**
* Número máximo de consultas por página antes de alertar.
* Un WordPress normal no debería superar las 80-100.
*/
define( 'AYUDAWP_MAX_QUERIES_ALERT', 100 );
/**
* Registra las consultas problemáticas al finalizar la carga.
*/
function ayudawp_log_slow_queries() {
global $wpdb;
if ( empty( $wpdb->queries ) ) {
return;
}
$log_file = WP_CONTENT_DIR . '/ayudawp-slow-queries.log';
$total = count( $wpdb->queries );
$slow = array();
$total_time = 0;
$request_uri = isset( $_SERVER['REQUEST_URI'] )
? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) )
: 'unknown';
foreach ( $wpdb->queries as $query_data ) {
$sql = $query_data[0];
$time = $query_data[1];
$caller = $query_data[2];
$total_time += $time;
if ( $time >= AYUDAWP_SLOW_QUERY_THRESHOLD ) {
$slow[] = array(
'sql' => trim( $sql ),
'time' => round( $time, 4 ),
'caller' => $caller,
);
}
}
// Si no hay consultas lentas y el total no supera el umbral, no registrar.
if ( empty( $slow ) && $total < AYUDAWP_MAX_QUERIES_ALERT ) {
return;
}
$log_entry = "\n" . str_repeat( '=', 70 ) . "\n";
$log_entry .= gmdate( 'Y-m-d H:i:s' ) . ' | URL: ' . $request_uri . "\n";
$log_entry .= 'Total consultas: ' . $total
. ' | Tiempo total DB: ' . round( $total_time, 4 ) . "s\n";
if ( $total >= AYUDAWP_MAX_QUERIES_ALERT ) {
$log_entry .= '** ALERTA: Numero excesivo de consultas (' . $total . ") **\n";
}
if ( ! empty( $slow ) ) {
$log_entry .= 'Consultas lentas encontradas: ' . count( $slow ) . "\n";
$log_entry .= str_repeat( '-', 70 ) . "\n";
foreach ( $slow as $index => $q ) {
$log_entry .= '[' . ( $index + 1 ) . '] Tiempo: '
. $q['time'] . "s\n";
$log_entry .= 'SQL: ' . $q['sql'] . "\n";
$log_entry .= 'Llamada: ' . $q['caller'] . "\n\n";
}
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
file_put_contents( $log_file, $log_entry, FILE_APPEND | LOCK_EX );
}
add_action( 'shutdown', 'ayudawp_log_slow_queries' );
El registro se guardará en /wp-content/ayudawp-slow-queries.log. Navega por tu web durante un rato, especialmente por las páginas que van lentas, y luego descarga el archivo para analizarlo.
Cada entrada del log te dice la URL que se estaba cargando, el número total de consultas, el tiempo total invertido en la base de datos, y el detalle de cada consulta lenta con la función exacta que la ha disparado. Con eso tienes suficiente para saber dónde buscar.
Dato muy importante: esto es para diagnóstico temporal.
SAVEQUERIEShace que WordPress almacene en memoria un registro de todas las consultas, lo que consume recursos adicionales. Cuando termines de diagnosticar, elimina el mu-plugin y quita la línea deSAVEQUERIESdelwp-config.php. No lo dejes activado en producción.
Con phpMyAdmin: ver qué pasa en tiempo real
Si tienes acceso a phpMyAdmin a través del panel de tu hosting (cPanel, Plesk, Site Tools o similar), puedes ver directamente qué está haciendo la base de datos en cada momento.
SHOW PROCESSLIST — Ver consultas en tiempo real:
Ve a la pestaña SQL de tu base de datos y ejecuta:
SHOW FULL PROCESSLIST;
Esto te muestra todas las consultas que se están ejecutando en ese instante. Las columnas importantes son:
Time: cuántos segundos lleva ejecutándose esa consulta. Si ves valores altos (más de 2 o 3 segundos) tienes un problema.State: en qué fase está la consulta. Valores comoSending data,Sorting resultoCreating tmp tabledurante mucho tiempo indican problemas.Info: la consulta SQL completa. Aquí ves exactamente qué se está ejecutando.
El truco es ejecutar SHOW FULL PROCESSLIST varias veces seguidas mientras la web está lenta. Si ves la misma consulta apareciendo una y otra vez, o una consulta que lleva muchos segundos ejecutándose, ya tienes al culpable.
SHOW TABLE STATUS — Estado y tamaño de las tablas:
SHOW TABLE STATUS;
Esto te da una visión general de todas las tablas: cuántas filas tienen, cuánto espacio ocupan los datos, cuánto ocupan los índices y si hay fragmentación. Fíjate especialmente en:
- Tablas con un número de filas desproporcionado (
wp_postmetaywp_optionssuelen ser las que más crecen). - La columna
Data_free, que indica espacio desperdiciado por fragmentación. - El
Engine: si ves alguna tabla con MyISAM en vez de InnoDB, eso puede ser parte del problema (lo vemos más adelante).
Con SSH y WP-CLI: diagnóstico avanzado
Si tienes acceso por SSH a tu servidor, tienes las herramientas más potentes a tu disposición.
WP-CLI Profile — Perfilar tiempos de carga:
Primero instala el paquete de perfilado:
wp package install wp-cli/profile-command
Luego, desde la carpeta de tu instalación de WordPress, ejecuta:
wp profile stage --fields=stage,time,cache_ratio,query_time,query_count --spotlight
Esto te muestra cuánto tiempo se invierte en cada etapa de la carga de WordPress (bootstrap, carga de plugins, tema, consulta principal, plantilla), cuántas consultas hace cada una y cuánto tiempo se gasta en la base de datos. Si ves que bootstrap tarda mucho y tiene muchas consultas, el problema está en los plugins o en la configuración.
Para profundizar en los plugins concretos:
wp profile hook plugins_loaded --fields=callback,time,location --spotlight
Y para ver todos los hooks y su impacto:
wp profile hook --fields=hook,time,cache_ratio,query_count --spotlight
El parámetro --spotlight oculta los valores a cero para que solo veas lo relevante.
WP-CLI Doctor — Diagnóstico rápido:
Instala el paquete:
wp package install wp-cli/doctor-command
Y ejecuta todas las comprobaciones:
wp doctor check --all
Esto te avisa automáticamente de problemas como opciones con autoload que superan los 900 KB, si SAVEQUERIES está activado en producción (no debería), si el cron tiene tareas duplicadas o excesivas, y otras cosas que afectan al rendimiento de la base de datos.
Slow query log de MySQL — El registro definitivo:
El slow_query_log es un registro que genera el propio servidor MySQL/MariaDB con todas las consultas que superan un tiempo determinado. Es lo más fiable que existe porque registra absolutamente todo lo que pasa en la base de datos, no solo lo que WordPress te deja ver.
Si administras tu propio servidor, actívalo añadiendo esto a la configuración de MySQL (my.cnf o my.ini):
[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql-slow.log long_query_time = 1 log_queries_not_using_indexes = 1
El parámetro long_query_time define el umbral en segundos. Un valor de 1 es razonable para empezar, luego puedes bajarlo a 0.5 si quieres más detalle. La opción log_queries_not_using_indexes es muy útil porque registra también las consultas que no utilizan ningún índice, aunque sean rápidas, ya que cuando la tabla crezca se convertirán en un problema.
Si estás en un hosting compartido, no podrás editar la configuración de MySQL directamente. Pero muchos hostings pueden activar el slow_query_log si se lo pides. Manda un ticket de soporte con algo así:
"Estoy diagnosticando problemas de rendimiento en mi base de datos. ¿Podéis activar el slow query log con un umbral de 1 segundo y facilitarme los resultados? También me sería útil que activaseis log_queries_not_using_indexes."
Algunos hosting como SiteGround, Raiola o Webempresa suelen colaborar con este tipo de peticiones sin problema.
Interpretar lo que encuentras: EXPLAIN
Ya tienes identificada la consulta sospechosa, ahora necesitas entender por qué es lenta o por qué consume tantos recursos. Para eso existe EXPLAIN, un comando de MySQL que te dice exactamente cómo se ejecuta una consulta internamente.
Simplemente pon EXPLAIN delante de cualquier consulta SELECT y ejecútala en phpMyAdmin o en la consola de MySQL.
Por ejemplo:
EXPLAIN SELECT * FROM wp_postmeta WHERE meta_key = '_price';
El resultado es una tabla con varias columnas.
Las que te interesan para diagnosticar problemas son:
type — Cómo accede MySQL a la tabla. De mejor a peor:
system/const: perfecto, accede a un solo registro por clave primaria.eq_ref/ref: bien, usa un índice para encontrar las filas.range: aceptable, escanea un rango de filas usando un índice.index: regular, escanea todo el índice (mejor que escanear toda la tabla, pero no ideal).ALL: malo. Escaneo completo de tabla. Si ves esto en una tabla con miles de filas, ahí está tu problema.
key — Qué índice usa MySQL para la consulta. Si ves NULL significa que no usa ningún índice, lo que suele ser sinónimo de consulta lenta en tablas grandes.
rows — Cuántas filas estima MySQL que necesita examinar. Si este número es enorme comparado con las filas que realmente necesitas, la consulta está haciendo mucho más trabajo del necesario.
Extra — Información adicional sobre cómo se ejecuta. Valores que indican problemas:
Using filesort: MySQL tiene que ordenar los resultados sin poder usar un índice. Lento en tablas grandes.Using temporary: MySQL crea una tabla temporal para procesar la consulta. Consume memoria y disco.Using where: filtra filas después de leerlas, lo que no es malo por sí solo pero combinado contype: ALLsignifica que lee toda la tabla y luego descarta lo que no necesita.
Para un diagnóstico aún más detallado, puedes usar el formato JSON:
EXPLAIN FORMAT=JSON SELECT * FROM wp_postmeta WHERE meta_key = '_price';
El formato JSON te da información adicional como el coste estimado de la consulta, que es útil para comparar antes y después de aplicar una optimización.
Si usas MySQL Workbench (puedes conectarte remotamente si tu hosting lo permite o activando la opción de MySQL remoto en cPanel), la vista gráfica de EXPLAIN te marca en rojo los pasos lentos, en naranja los que se pueden mejorar y en verde los que están bien. Muy visual y práctico para entender de un vistazo dónde está el cuello de botella.
Los culpables habituales
Con las herramientas anteriores vas a encontrar las consultas problemáticas. Pero para que sepas qué estás buscando, estos son los patrones que se repiten una y otra vez en instalaciones de WordPress con problemas de rendimiento en la base de datos.
Consultas lentas
wp_options con autoload descontrolado:
Cada vez que se carga una página, WordPress ejecuta una consulta que trae de golpe todas las opciones de la tabla wp_options que tienen el campo autoload con valor yes. En una instalación recién hecha esto no es problema, pero con el tiempo y con plugins que van añadiendo opciones con autoload activado, esa consulta inicial puede tener que cargar varios megabytes de datos en cada petición.
Lo peor es que muchas de esas opciones con autoload ni siquiera se necesitan en cada página. Son configuraciones de plugins que solo se usan en contextos específicos, pero que se cargan siempre por defecto.
Si quieres profundizar en este tema concreto, tengo un artículo detallado sobre cómo identificar y optimizar las tablas lentas de la base de datos con índices.
meta_query sobre wp_postmeta sin índices:
Este es el clásico de WooCommerce con catálogos grandes, pero también afecta a cualquier web que use campos personalizados (custom fields) de forma intensiva. La tabla wp_postmeta no tiene un índice compuesto sobre meta_key y meta_value, así que cuando haces una meta_query buscando productos por precio, por atributo o por cualquier campo personalizado, MySQL tiene que recorrer una tabla que puede tener cientos de miles o millones de filas.
Haz EXPLAIN de una consulta típica de filtrado por meta y verás type: ALL o type: ref con un número de filas absurdo.
Búsqueda interna con LIKE sobre tablas grandes:
La búsqueda nativa de WordPress usa LIKE '%término%' para buscar en títulos y contenido. El problema del comodín al principio (%) es que impide usar cualquier índice, así que MySQL se ve obligado a leer cada fila de wp_posts para buscar coincidencias. En una web con miles de entradas, esto es muy lento.
Consultas con múltiples JOIN pesados:
Cuando un plugin o un tema necesita combinar datos de varias tablas (posts + postmeta + terms + termmeta), el número de filas que MySQL tiene que procesar se multiplica. Si además alguna de esas tablas no tiene los índices adecuados, el resultado es una consulta que tarda segundos en ejecutarse.
Consultas agresivas (alto consumo de recursos)
Precarga o precalentamiento de caché que lanza consultas masivas:
Algunos plugins de caché tienen opciones para precalentar la caché generando todas las páginas de golpe. Lo que hacen internamente es lanzar una consulta para obtener todas las URLs del sitio y luego empezar a visitarlas una detrás de otra (o incluso varias a la vez). Cada visita genera su propio conjunto de consultas a la base de datos, y si se ejecutan muchas a la vez, la base de datos se satura.
También pasa con plugins de sitemap que generan el mapa del sitio consultando toda la base de datos de golpe en vez de ir por partes.
Plugins que hacen SELECT * sin LIMIT:
Hay plugins que necesitan procesar datos en lote (exportaciones, generación de informes, envío masivo de correos) y que no implementan paginación. Hacen un SELECT * sobre la tabla completa, cargan todo en la memoria PHP y luego lo procesan. Si la tabla tiene 100.000 filas, meten 100.000 filas en la RAM de golpe.
Importaciones y exportaciones sin procesamiento por lotes:
Importar miles de productos en WooCommerce o miles de entradas con un plugin que no procesa por lotes puede colapsar la base de datos. Cada inserción o actualización dispara sus propias consultas, y si no se hacen por lotes, el servidor no da abasto.
Consultas excesivas o frecuentes
Consultas N+1 en bucle:
Este es uno de los problemas más comunes y más difíciles de detectar si no sabes qué buscar. Ocurre cuando un tema o plugin ejecuta una consulta por cada elemento de un listado, en vez de traer todos los datos de una sola vez.
El ejemplo típico: un tema que muestra 20 entradas en una página de archivo y, dentro del bucle, hace get_post_meta() para cada entrada individualmente. Resultado: 1 consulta para traer las 20 entradas + 20 consultas adicionales para los meta de cada una. Si además pide datos de taxonomías, autor y miniatura, fácilmente llegas a 100+ consultas solo para un listado.
WordPress tiene mecanismos para precargar los meta y las taxonomías de golpe (update_post_meta_cache y update_post_term_cache), pero muchos temas y plugins los desactivan o no los aprovechan.
Heartbeat API haciendo polling continuo:
La Heartbeat API de WordPress envía una petición AJAX al servidor cada 15-60 segundos (dependiendo del contexto) para comprobar cosas como el autoguardado, las notificaciones y si alguien más está editando el mismo contenido. Cada una de esas peticiones ejecuta varias consultas a la base de datos.
Si tienes varios usuarios trabajando en el admin a la vez, cada uno está generando estas peticiones periódicas, y el efecto acumulado puede ser considerable. En un WordPress con 10 usuarios conectados al admin simultáneamente, el Heartbeat puede estar generando cientos de consultas por minuto sin que nadie sea consciente.
Consultas duplicadas:
Es más común de lo que parece. Un tema carga ciertas opciones con get_option(), y un plugin pide exactamente las mismas opciones por su cuenta. O dos plugins distintos ejecutan la misma consulta para obtener la misma información. WordPress tiene su propia caché de objetos en memoria para una sola petición, pero no siempre funciona si los plugins hacen consultas directas con $wpdb en vez de usar las funciones estándar de WordPress.
WP-Cron con tareas acumuladas:
WordPress simula un cron mediante una comprobación en cada carga de página. Si hay plugins mal diseñados que programan tareas y nunca las limpian, o que programan la misma tarea varias veces, puedes acabar con cientos de tareas programadas que se ejecutan todas de golpe cuando les toca, saturando la base de datos en ese momento.
Esto es especialmente problemático en webs con poco tráfico, porque las tareas se acumulan y cuando finalmente llega una visita, se disparan todas a la vez.
Tabla resumen
| Problema | Tipo | Cómo se detecta | Impacto principal |
|---|---|---|---|
Autoload descontrolado en wp_options |
Lenta | Consulta de tamaño autoload > 1 MB |
Cada carga de página más lenta |
meta_query sin índices en wp_postmeta |
Lenta | EXPLAIN muestra type: ALL |
Filtrados y búsquedas lentos |
Búsqueda interna con LIKE %…% |
Lenta | Query Monitor, consultas con LIKE |
Búsquedas muy lentas en webs grandes |
Múltiples JOIN pesados |
Lenta | EXPLAIN muestra filas enormes |
Páginas concretas muy lentas |
| Precarga masiva de caché | Agresiva | SHOW PROCESSLIST, picos de carga | Saturación puntual del servidor |
SELECT * sin LIMIT |
Agresiva | Query Monitor, filas devueltas | Alto consumo de RAM |
| Importaciones sin lotes | Agresiva | SHOW PROCESSLIST durante imports | Base de datos bloqueada durante el proceso |
Consultas N+1 en bucle |
Excesiva | Query Monitor > 100 consultas/página | Tiempo acumulado, TTFB alto |
| Heartbeat API descontrolada | Excesiva | Query Monitor en admin, peticiones AJAX | Carga constante en el admin |
| Consultas duplicadas | Excesiva | Query Monitor > Duplicate Queries | Trabajo innecesario, TTFB alto |
| WP-Cron con tareas acumuladas | Excesiva | WP Crontrol o wp cron event list |
Picos de carga impredecibles |
Consultas de diagnóstico
Antes de pasar a las soluciones, necesitas datos concretos de tu instalación. Para cada consulta de diagnóstico te doy tres formas de ejecutarla:
- Directamente como SQL (en phpMyAdmin o cualquier cliente de base de datos).
- Como código PHP (un mu-plugin temporal que puedes subir si no tienes otro acceso).
- Por WP-CLI si tienes SSH. Usa la que te venga mejor según tu situación.
Tamaño total de datos con autoload
SQL:
SELECT SUM(LENGTH(option_value)) AS autoload_size_bytes, ROUND(SUM(LENGTH(option_value)) / 1024 / 1024, 2) AS autoload_size_mb FROM wp_options WHERE autoload = 'yes';
PHP (mu-plugin temporal en /wp-content/mu-plugins/ayudawp-diagnostico.php):
<?php
/**
* AyudaWP - Diagnóstico: tamaño de autoload.
* Muestra el resultado como aviso en el admin.
* Eliminar después de usar.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function ayudawp_diag_autoload_size() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
$result = $wpdb->get_row(
"SELECT SUM(LENGTH(option_value)) AS total_bytes
FROM {$wpdb->options}
WHERE autoload = 'yes'"
);
$mb = round( $result->total_bytes / 1024 / 1024, 2 );
$class = $result->total_bytes > 1000000 ? 'notice-error' : 'notice-info';
echo '<div class="notice ' . esc_attr( $class ) . '"><p>';
echo 'AyudaWP Diagnostico: Autoload total = '
. esc_html( number_format( $result->total_bytes, 0, ',', '.' ) )
. ' bytes (' . esc_html( $mb ) . ' MB).';
if ( $result->total_bytes > 1000000 ) {
echo ' <strong>Supera 1 MB, conviene revisarlo.</strong>';
}
echo '</p></div>';
}
add_action( 'admin_notices', 'ayudawp_diag_autoload_size' );
WP-CLI:
wp db query "SELECT SUM(LENGTH(option_value)) AS autoload_bytes, ROUND(SUM(LENGTH(option_value)) / 1024 / 1024, 2) AS autoload_mb FROM $(wp db prefix)options WHERE autoload = 'yes';"
Las 30 opciones con autoload más pesadas
SQL:
SELECT option_name, LENGTH(option_value) AS size_bytes, ROUND(LENGTH(option_value) / 1024, 2) AS size_kb FROM wp_options WHERE autoload = 'yes' ORDER BY size_bytes DESC LIMIT 30;
PHP (añadir al mismo mu-plugin de diagnóstico):
function ayudawp_diag_top_autoload() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
$results = $wpdb->get_results(
"SELECT option_name, LENGTH(option_value) AS size_bytes
FROM {$wpdb->options}
WHERE autoload = 'yes'
ORDER BY size_bytes DESC
LIMIT 30"
);
echo '<div class="notice notice-info"><p><strong>AyudaWP - Top 30 autoload:</strong></p><ol>';
foreach ( $results as $row ) {
$kb = round( $row->size_bytes / 1024, 2 );
echo '<li>' . esc_html( $row->option_name )
. ' - ' . esc_html( $kb ) . ' KB</li>';
}
echo '</ol></div>';
}
add_action( 'admin_notices', 'ayudawp_diag_top_autoload' );
WP-CLI:
wp db query "SELECT option_name, LENGTH(option_value) AS size_bytes, ROUND(LENGTH(option_value) / 1024, 2) AS size_kb FROM $(wp db prefix)options WHERE autoload = 'yes' ORDER BY size_bytes DESC LIMIT 30;"
Tablas con más filas y mayor tamaño
SQL:
SELECT TABLE_NAME, TABLE_ROWS, ROUND(DATA_LENGTH / 1024 / 1024, 2) AS data_mb, ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS index_mb, ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS total_mb, ROUND(DATA_FREE / 1024 / 1024, 2) AS fragmented_mb, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'nombre_de_tu_base_de_datos' ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC;
PHP:
function ayudawp_diag_table_sizes() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
$db_name = DB_NAME;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT TABLE_NAME, TABLE_ROWS,
ROUND(DATA_LENGTH / 1024 / 1024, 2) AS data_mb,
ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS index_mb,
ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS total_mb,
ENGINE
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = %s
ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC",
$db_name
)
);
echo '<div class="notice notice-info">';
echo '<p><strong>AyudaWP - Tablas por tamaño:</strong></p>';
echo '<table style="border-collapse:collapse;width:100%">';
echo '<tr><th style="text-align:left;padding:4px;border-bottom:1px solid #ccc">Tabla</th>';
echo '<th style="text-align:right;padding:4px;border-bottom:1px solid #ccc">Filas</th>';
echo '<th style="text-align:right;padding:4px;border-bottom:1px solid #ccc">Total MB</th>';
echo '<th style="text-align:left;padding:4px;border-bottom:1px solid #ccc">Motor</th></tr>';
foreach ( $results as $row ) {
echo '<tr><td style="padding:4px">' . esc_html( $row->TABLE_NAME ) . '</td>';
echo '<td style="text-align:right;padding:4px">' . esc_html( number_format( $row->TABLE_ROWS, 0, ',', '.' ) ) . '</td>';
echo '<td style="text-align:right;padding:4px">' . esc_html( $row->total_mb ) . '</td>';
echo '<td style="padding:4px">' . esc_html( $row->ENGINE ) . '</td></tr>';
}
echo '</table></div>';
}
add_action( 'admin_notices', 'ayudawp_diag_table_sizes' );
WP-CLI:
wp db query "SELECT TABLE_NAME, TABLE_ROWS, ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS total_mb, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC;" --table
postmeta huérfano (sin ninguna entrada asociada)
SQL:
SELECT COUNT(*) AS orphaned_postmeta FROM wp_postmeta WHERE post_id NOT IN (SELECT ID FROM wp_posts);
PHP:
function ayudawp_diag_orphaned_postmeta() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
$count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->postmeta}
WHERE post_id NOT IN (SELECT ID FROM {$wpdb->posts})"
);
$class = $count > 1000 ? 'notice-warning' : 'notice-info';
echo '<div class="notice ' . esc_attr( $class ) . '"><p>';
echo 'AyudaWP: Postmeta huerfano = '
. esc_html( number_format( $count, 0, ',', '.' ) ) . ' registros.';
if ( $count > 1000 ) {
echo ' <strong>Conviene limpiarlo.</strong>';
}
echo '</p></div>';
}
add_action( 'admin_notices', 'ayudawp_diag_orphaned_postmeta' );
WP-CLI:
wp db query "SELECT COUNT(*) AS orphaned FROM $(wp db prefix)postmeta WHERE post_id NOT IN (SELECT ID FROM $(wp db prefix)posts);"
commentmeta huérfano (sin ningún comentario asociado)
SQL:
SELECT COUNT(*) AS orphaned_commentmeta FROM wp_commentmeta WHERE comment_id NOT IN (SELECT comment_ID FROM wp_comments);
PHP:
function ayudawp_diag_orphaned_commentmeta() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
$count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->commentmeta}
WHERE comment_id NOT IN (SELECT comment_ID FROM {$wpdb->comments})"
);
echo '<div class="notice notice-info"><p>';
echo 'AyudaWP: Commentmeta huerfano = '
. esc_html( number_format( $count, 0, ',', '.' ) ) . ' registros.';
echo '</p></div>';
}
add_action( 'admin_notices', 'ayudawp_diag_orphaned_commentmeta' );
WP-CLI:
wp db query "SELECT COUNT(*) AS orphaned FROM $(wp db prefix)commentmeta WHERE comment_id NOT IN (SELECT comment_ID FROM $(wp db prefix)comments);"
Transients caducados pendientes de limpiar
SQL:
SELECT COUNT(*) AS expired_transients FROM wp_options WHERE option_name LIKE '%_transient_timeout_%' AND option_value < UNIX_TIMESTAMP();
PHP:
function ayudawp_diag_expired_transients() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
$count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->options}
WHERE option_name LIKE '%_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP()"
);
echo '<div class="notice notice-info"><p>';
echo 'AyudaWP: Transients caducados = '
. esc_html( number_format( $count, 0, ',', '.' ) ) . '.';
echo '</p></div>';
}
add_action( 'admin_notices', 'ayudawp_diag_expired_transients' );
WP-CLI:
wp transient delete --expired
En WP-CLI el comando directamente los borra. Si solo quieres ver cuántos hay sin eliminarlos, usa la consulta SQL con wp db query.
Las 20 entradas con más revisiones
SQL:
SELECT p.post_title,
p.ID AS post_id,
COUNT(r.ID) AS revision_count
FROM wp_posts p
INNER JOIN wp_posts r ON r.post_parent = p.ID AND r.post_type = 'revision'
WHERE p.post_type NOT IN ('revision', 'auto-draft')
GROUP BY p.ID
ORDER BY revision_count DESC
LIMIT 20;
PHP:
function ayudawp_diag_top_revisions() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
$results = $wpdb->get_results(
"SELECT p.post_title, p.ID, COUNT(r.ID) AS rev_count
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->posts} r ON r.post_parent = p.ID AND r.post_type = 'revision'
WHERE p.post_type NOT IN ('revision', 'auto-draft')
GROUP BY p.ID
ORDER BY rev_count DESC
LIMIT 20"
);
echo '<div class="notice notice-info">';
echo '<p><strong>AyudaWP - Top 20 entradas con mas revisiones:</strong></p><ol>';
foreach ( $results as $row ) {
echo '<li>' . esc_html( $row->post_title )
. ' (ID ' . intval( $row->ID ) . ') - '
. intval( $row->rev_count ) . ' revisiones</li>';
}
echo '</ol></div>';
}
add_action( 'admin_notices', 'ayudawp_diag_top_revisions' );
WP-CLI:
wp db query "SELECT p.post_title, p.ID, COUNT(r.ID) AS rev_count FROM $(wp db prefix)posts p INNER JOIN $(wp db prefix)posts r ON r.post_parent = p.ID AND r.post_type = 'revision' WHERE p.post_type NOT IN ('revision', 'auto-draft') GROUP BY p.ID ORDER BY rev_count DESC LIMIT 20;"
Tablas que no son del core de WordPress
Útil para detectar tablas huérfanas de plugins que ya no tienes instalados.
SQL:
SELECT TABLE_NAME,
TABLE_ROWS,
ROUND(((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024), 2) AS size_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'nombre_de_tu_base_de_datos'
AND TABLE_NAME NOT IN (
'wp_commentmeta', 'wp_comments', 'wp_links',
'wp_options', 'wp_postmeta', 'wp_posts',
'wp_termmeta', 'wp_terms', 'wp_term_relationships',
'wp_term_taxonomy', 'wp_usermeta', 'wp_users'
)
ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC;
PHP:
function ayudawp_diag_non_core_tables() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
$prefix = $wpdb->prefix;
$core = array(
$prefix . 'commentmeta', $prefix . 'comments', $prefix . 'links',
$prefix . 'options', $prefix . 'postmeta', $prefix . 'posts',
$prefix . 'termmeta', $prefix . 'terms', $prefix . 'term_relationships',
$prefix . 'term_taxonomy', $prefix . 'usermeta', $prefix . 'users',
);
$placeholders = implode( ',', array_fill( 0, count( $core ), '%s' ) );
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT TABLE_NAME, TABLE_ROWS,
ROUND(((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024), 2) AS size_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = %s
AND TABLE_NAME NOT IN ($placeholders)
ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC",
array_merge( array( DB_NAME ), $core )
)
);
if ( empty( $results ) ) {
echo '<div class="notice notice-success"><p>AyudaWP: No hay tablas fuera del core.</p></div>';
return;
}
echo '<div class="notice notice-warning">';
echo '<p><strong>AyudaWP - Tablas fuera del core de WordPress:</strong></p><ol>';
foreach ( $results as $row ) {
echo '<li>' . esc_html( $row->TABLE_NAME )
. ' - ' . esc_html( number_format( $row->TABLE_ROWS, 0, ',', '.' ) )
. ' filas - ' . esc_html( $row->size_mb ) . ' MB</li>';
}
echo '</ol></div>';
}
add_action( 'admin_notices', 'ayudawp_diag_non_core_tables' );
WP-CLI:
wp db query "SELECT TABLE_NAME, TABLE_ROWS, ROUND(((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024), 2) AS size_mb FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME NOT LIKE '$(wp db prefix)comment%' ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC;" --table
Nota: En WP-CLI este es un caso donde la consulta SQL completa con todas las exclusiones es más precisa. El comando de arriba es una aproximación rápida; para el listado exacto de tablas core, usa la consulta SQL completa con
wp db query.
Índices existentes en una tabla
Antes de crear un índice, comprueba si ya existe.
SQL:
SHOW INDEX FROM wp_postmeta;
PHP:
function ayudawp_diag_show_indexes() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wpdb;
// Cambia la tabla a revisar.
$table = $wpdb->postmeta;
$results = $wpdb->get_results( "SHOW INDEX FROM {$table}" );
echo '<div class="notice notice-info">';
echo '<p><strong>AyudaWP - Indices de ' . esc_html( $table ) . ':</strong></p><ul>';
foreach ( $results as $row ) {
echo '<li>' . esc_html( $row->Key_name )
. ' (' . esc_html( $row->Column_name ) . ')</li>';
}
echo '</ul></div>';
}
add_action( 'admin_notices', 'ayudawp_diag_show_indexes' );
WP-CLI:
wp db query "SHOW INDEX FROM $(wp db prefix)postmeta;"
Motor de almacenamiento de cada tabla (detectar MyISAM)
SQL:
SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'nombre_de_tu_base_de_datos' ORDER BY TABLE_NAME;
PHP: esta información ya la muestra la consulta de tamaño de tablas que vimos antes, que incluye la columna ENGINE.
WP-CLI:
wp db query "SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME;"
Soluciones según el tipo de problema
Ahora que tienes diagnosticado qué pasa y los datos concretos de tu instalación, vamos con las soluciones. De nuevo, para cada una te doy las opciones según tu nivel de acceso.
Soluciones para consultas lentas
Crear índices donde faltan:
Los índices son la solución más directa para consultas lentas. Un índice permite a MySQL encontrar las filas que necesita sin tener que recorrer toda la tabla.
El caso más típico es el índice de autoload en wp_options:
SQL:
CREATE INDEX idx_autoload ON wp_options(autoload);
PHP:
function ayudawp_create_autoload_index() {
global $wpdb;
// Comprobar si el índice ya existe.
$indexes = $wpdb->get_results( "SHOW INDEX FROM {$wpdb->options} WHERE Key_name = 'idx_autoload'" );
if ( ! empty( $indexes ) ) {
return; // Ya existe.
}
$wpdb->query( "CREATE INDEX idx_autoload ON {$wpdb->options}(autoload)" );
}
// Ejecutar una sola vez.
add_action( 'admin_init', 'ayudawp_create_autoload_index' );
WP-CLI:
wp db query "CREATE INDEX idx_autoload ON $(wp db prefix)options(autoload);"
Para wp_postmeta, si haces muchas búsquedas por meta_key y meta_value (típico de WooCommerce):
SQL:
CREATE INDEX idx_meta_key_value ON wp_postmeta(meta_key(191), meta_value(100));
PHP:
function ayudawp_create_postmeta_index() {
global $wpdb;
$indexes = $wpdb->get_results(
"SHOW INDEX FROM {$wpdb->postmeta} WHERE Key_name = 'idx_meta_key_value'"
);
if ( ! empty( $indexes ) ) {
return;
}
$wpdb->query(
"CREATE INDEX idx_meta_key_value ON {$wpdb->postmeta}(meta_key(191), meta_value(100))"
);
}
add_action( 'admin_init', 'ayudawp_create_postmeta_index' );
WP-CLI:
wp db query "CREATE INDEX idx_meta_key_value ON $(wp db prefix)postmeta(meta_key(191), meta_value(100));"
Cambia wp_options y wp_postmeta por el nombre real de tus tablas si usas un prefijo distinto a wp_. En las versiones PHP, al usar $wpdb->options y $wpdb->postmeta, WordPress ya aplica el prefijo correcto automáticamente.
- Precauciones con los índices: los índices aceleran las lecturas pero ralentizan ligeramente las escrituras (
INSERT,UPDATE,DELETE) porque MySQL tiene que actualizar el índice cada vez. En la mayoría de los WordPress, las lecturas superan ampliamente a las escrituras, así que el beneficio compensa. Pero no te dediques a crear índices a lo loco sin antes confirmar conEXPLAINque la consulta realmente los necesita y los va a usar. - Limpia autoloads innecesarios: cuando hayas identificado las consultas de diagnóstico qué opciones ocupan más y no necesitan cargarse en cada petición cámbialas a
no:
SQL:
UPDATE wp_options SET autoload = 'no' WHERE option_name = 'nombre_de_la_opcion';
PHP:
// Cambiar autoload de una opción concreta.
// Sustituye 'nombre_de_la_opcion' por la que quieras cambiar.
function ayudawp_fix_autoload() {
global $wpdb;
$wpdb->update(
$wpdb->options,
array( 'autoload' => 'no' ),
array( 'option_name' => 'nombre_de_la_opcion' ),
array( '%s' ),
array( '%s' )
);
}
add_action( 'admin_init', 'ayudawp_fix_autoload' );
WP-CLI:
wp db query "UPDATE $(wp db prefix)options SET autoload = 'no' WHERE option_name = 'nombre_de_la_opcion';"
Cuidado: no cambies el autoload de opciones del núcleo de WordPress ni de opciones que no sepas para qué sirven. Investiga antes qué hace cada opción. Las más seguras de cambiar suelen ser las de plugins de estadísticas, logs, caché de datos temporales y configuraciones de plugins que solo se usan en el admin.
Sustituir la búsqueda interna:
Si la búsqueda nativa de WordPress está generando consultas lentas con LIKE, la solución más efectiva es usar un plugin de búsqueda que utilice su propio sistema de indexación, como SearchWP o Relevanssi. Estos plugins crean sus propios índices optimizados para búsqueda, evitando las consultas LIKE sobre wp_posts. En este caso no hay alternativa SQL o PHP directa, ya que el problema es estructural: la búsqueda nativa de WordPress funciona así por diseño y no se puede resolver con un simple cambio en la base de datos.
Soluciones para consultas agresivas
Controlar la precarga de caché:
Si usas un plugin de caché con opción de precalentamiento, revisa su configuración. Busca opciones para limitar cuántas páginas precarga a la vez (concurrencia) y para añadir una pausa entre peticiones. Si no ofrece estas opciones, valora si realmente necesitas el precalentamiento o si es mejor dejar que la caché se genere bajo demanda conforme los visitantes van entrando. Esto es cuestión de configuración del plugin, no hay consulta SQL ni snippet PHP que lo resuelva.
Limitar resultados en consultas personalizadas:
Si desarrollas o mantienes código personalizado en tu web, asegúrate de que todas las consultas a tablas grandes incluyan LIMIT. Nunca hagas SELECT * FROM tabla_enorme sin limitar los resultados. Si necesitas procesar muchos registros, hazlo por lotes:
// Mal: traer todo de golpe.
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->postmeta}" );
// Bien: procesar por lotes de 500.
$offset = 0;
$batch = 500;
do {
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->postmeta} LIMIT %d OFFSET %d",
$batch,
$offset
)
);
// Procesar $results aquí.
$offset += $batch;
} while ( ! empty( $results ) );
Soluciones para consultas excesivas o frecuentes
Usar transients para consultas costosas que se repiten:
Si tienes una consulta que se ejecuta en cada carga de página y los datos no cambian constantemente (por ejemplo, un listado de los productos más vendidos, los artículos más populares o un menú generado dinámicamente), almacena el resultado con la API de transients de WordPress:
function ayudawp_get_popular_posts() {
// Intentar obtener el resultado cacheado.
$cached = get_transient( 'ayudawp_popular_posts' );
if ( false !== $cached ) {
return $cached;
}
// Si no hay cache, ejecutar la consulta.
$posts = new WP_Query( array(
'posts_per_page' => 10,
'orderby' => 'comment_count',
'order' => 'DESC',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
) );
// Guardar en cache durante 12 horas.
set_transient( 'ayudawp_popular_posts', $posts->posts, 12 * HOUR_IN_SECONDS );
return $posts->posts;
}
La próxima vez que se cargue la página, en vez de ejecutar la consulta pesada, WordPress simplemente lee el resultado desde wp_options, que es una lectura mucho más rápida. El transient se renueva automáticamente cuando caduca.
Optimizar WP_Query para evitar consultas innecesarias:
Cada vez que haces una WP_Query, WordPress ejecuta internamente varias consultas adicionales (paginación, caché de meta, caché de taxonomías). Si no las necesitas, desactívalas:
$query = new WP_Query( array(
'posts_per_page' => 10,
'post_type' => 'post',
// No calcular el total para paginación si no la necesitas.
'no_found_rows' => true,
// No precargar meta si no los vas a usar.
'update_post_meta_cache' => false,
// No precargar taxonomías si no las vas a usar.
'update_post_term_cache' => false,
// Traer solo los campos que necesites.
'fields' => 'ids',
) );
Cada uno de estos parámetros te ahorra una o más consultas. En un listado donde solo necesitas los IDs para luego procesarlos tú, puedes pasar de 5 consultas por WP_Query a solo 1.
Controlar la Heartbeat API:
Puedes reducir la frecuencia del Heartbeat o desactivarlo donde no lo necesites añadiendo un mu-plugin:
<?php
/**
* AyudaWP - Controlar la frecuencia del Heartbeat API.
*
* Reduce la frecuencia en el admin y lo desactiva
* en el front-end donde no es necesario.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Ajustar la frecuencia del Heartbeat.
*
* @param array $settings Configuración del Heartbeat.
* @return array
*/
function ayudawp_heartbeat_settings( $settings ) {
// Aumentar el intervalo a 60 segundos (por defecto son 15-30).
$settings['interval'] = 60;
return $settings;
}
add_filter( 'heartbeat_settings', 'ayudawp_heartbeat_settings' );
/**
* Desactivar Heartbeat en el front-end.
*/
function ayudawp_disable_heartbeat_frontend() {
if ( ! is_admin() ) {
wp_deregister_script( 'heartbeat' );
}
}
add_action( 'init', 'ayudawp_disable_heartbeat_frontend', 1 );
Esto es solo PHP/mu-plugin. No hay equivalente SQL porque el Heartbeat es un proceso de WordPress, no de la base de datos. En WP-CLI no tiene sentido porque el Heartbeat solo funciona cuando hay usuarios conectados al navegador.
Limpiar tareas de WP-Cron duplicadas o huérfanas:
WP-CLI (el método más directo):
# Ver todas las tareas programadas. wp cron event list # Borrar tareas de un hook concreto. wp cron event delete nombre_del_hook
Plugin: si no tienes SSH, instala temporalmente WP Crontrol para ver y gestionar las tareas desde el admin. Busca tareas duplicadas (la misma tarea programada varias veces a la misma hora) y elimina las sobrantes.
SQL: Las tareas de cron se guardan en la opción cron de wp_options como un array serializado. No se recomienda editarlas directamente con SQL porque corromper un array serializado dejaría el cron roto. Usa WP-CLI o WP Crontrol para este caso.
Otras optimizaciones que ayudan SIEMPRE
Limpiar postmeta huérfano:
SQL:
DELETE FROM wp_postmeta WHERE post_id NOT IN (SELECT ID FROM wp_posts);
PHP:
function ayudawp_clean_orphaned_postmeta() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE FROM {$wpdb->postmeta}
WHERE post_id NOT IN (SELECT ID FROM {$wpdb->posts})"
);
return $deleted;
}
// Llamar manualmente o desde un hook puntual.
WP-CLI:
wp db query "DELETE FROM $(wp db prefix)postmeta WHERE post_id NOT IN (SELECT ID FROM $(wp db prefix)posts);"
Lo mismo para commentmeta y term_relationships huérfanas:
SQL:
DELETE FROM wp_commentmeta WHERE comment_id NOT IN (SELECT comment_ID FROM wp_comments); DELETE FROM wp_term_relationships WHERE object_id NOT IN (SELECT ID FROM wp_posts);
PHP:
function ayudawp_clean_orphaned_commentmeta() {
global $wpdb;
$wpdb->query(
"DELETE FROM {$wpdb->commentmeta}
WHERE comment_id NOT IN (SELECT comment_ID FROM {$wpdb->comments})"
);
}
function ayudawp_clean_orphaned_term_relationships() {
global $wpdb;
$wpdb->query(
"DELETE FROM {$wpdb->term_relationships}
WHERE object_id NOT IN (SELECT ID FROM {$wpdb->posts})"
);
}
WP-CLI:
wp db query "DELETE FROM $(wp db prefix)commentmeta WHERE comment_id NOT IN (SELECT comment_ID FROM $(wp db prefix)comments);" wp db query "DELETE FROM $(wp db prefix)term_relationships WHERE object_id NOT IN (SELECT ID FROM $(wp db prefix)posts);"
Limitar revisiones:
Añade esto a tu wp-config.php:
define( 'WP_POST_REVISIONS', 5 );
Esto mantiene un máximo de 5 revisiones por entrada. Es una constante de WordPress que solo funciona desde wp-config.php, no se puede configurar con SQL ni con un código PHP normal.
Si ya tienes miles de revisiones acumuladas, elimínalas:
SQL:
DELETE p, pm, tr FROM wp_posts p LEFT JOIN wp_postmeta pm ON pm.post_id = p.ID LEFT JOIN wp_term_relationships tr ON tr.object_id = p.ID WHERE p.post_type = 'revision';
PHP:
function ayudawp_delete_all_revisions() {
global $wpdb;
$wpdb->query(
"DELETE p, pm, tr
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
LEFT JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
WHERE p.post_type = 'revision'"
);
}
WP-CLI:
# Listar cuántas revisiones hay. wp post list --post_type=revision --format=count # Borrar todas las revisiones. wp post delete $(wp post list --post_type=revision --format=ids) --force
Eliminar transients caducados:
SQL:
DELETE FROM wp_options
WHERE option_name LIKE '%_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP();
DELETE FROM wp_options
WHERE option_name LIKE '%_transient_%'
AND option_name NOT LIKE '%_transient_timeout_%'
AND option_name NOT IN (
SELECT REPLACE(option_name, '_timeout', '')
FROM (
SELECT option_name FROM wp_options
WHERE option_name LIKE '%_transient_timeout_%'
) AS t
);
PHP:
function ayudawp_delete_expired_transients() {
global $wpdb;
// Borrar timeouts caducados.
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '%_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP()"
);
// Borrar transients huerfanos (sin timeout asociado).
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '%_transient_%'
AND option_name NOT LIKE '%_transient_timeout_%'
AND option_name NOT IN (
SELECT REPLACE(option_name, '_timeout', '')
FROM (
SELECT option_name FROM {$wpdb->options}
WHERE option_name LIKE '%_transient_timeout_%'
) AS t
)"
);
}
WP-CLI:
wp transient delete --expired
WP-CLI tiene este comando nativo que lo hace directamente, sin necesidad de consultas SQL.
Convertir tablas MyISAM a InnoDB:
Si tu WordPress lleva muchos años funcionando o se migró desde una instalación antigua, es posible que algunas tablas todavía usen el motor MyISAM en vez de InnoDB. MyISAM bloquea la tabla completa cuando se escribe en ella, lo que en una web con tráfico significa que las lecturas se quedan esperando mientras se procesa cualquier escritura.
SQL (tabla por tabla):
ALTER TABLE wp_nombre_tabla ENGINE = InnoDB;
PHP:
function ayudawp_convert_myisam_to_innodb() {
global $wpdb;
$tables = $wpdb->get_results(
$wpdb->prepare(
"SELECT TABLE_NAME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = %s
AND ENGINE = 'MyISAM'",
DB_NAME
)
);
foreach ( $tables as $table ) {
// Usar esc_sql para el nombre de tabla en consulta directa.
$table_name = esc_sql( $table->TABLE_NAME );
$wpdb->query( "ALTER TABLE `{$table_name}` ENGINE = InnoDB" );
}
}
WP-CLI:
# Primero ver qué tablas usan MyISAM. wp db query "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND ENGINE = 'MyISAM';" # Convertir una tabla concreta. wp db query "ALTER TABLE $(wp db prefix)nombre_tabla ENGINE = InnoDB;"
Haz esto tabla por tabla y comprueba después que todo funciona correctamente.
OPTIMIZE TABLE:
Este comando reorganiza el almacenamiento físico de una tabla y recupera el espacio desperdiciado por fragmentación. Es útil después de haber hecho borrados masivos (revisiones, transients, postmeta huérfano):
SQL:
OPTIMIZE TABLE wp_options, wp_postmeta, wp_posts, wp_comments, wp_commentmeta;
PHP:
function ayudawp_optimize_core_tables() {
global $wpdb;
$tables = array(
$wpdb->options,
$wpdb->postmeta,
$wpdb->posts,
$wpdb->comments,
$wpdb->commentmeta,
);
$table_list = implode( ', ', $tables );
$wpdb->query( "OPTIMIZE TABLE {$table_list}" );
}
WP-CLI:
wp db optimize
El comando wp db optimize optimiza todas las tablas de la base de datos de una vez.
Conviene saber que OPTIMIZE TABLE no mejora la velocidad de las consultas por sí solo. Si una consulta es lenta porque le falta un índice, optimizar la tabla no va a cambiar nada. Es una operación de mantenimiento para después de limpiar datos, no una solución milagrosa.
Prevención y vigilancia
Arreglar las consultas problemáticas es solo la mitad del trabajo, la otra mitad es evitar que el problema vuelva a aparecer, así que toma nota, por favor:
- Limitar revisiones desde el principio: Configura
WP_POST_REVISIONSen elwp-config.phpde todas tus instalaciones. Es una de esas cosas que deberían venir por defecto. - Evaluar plugins antes de instalarlos: Antes de instalar un plugin nuevo, activa Query Monitor y compara cuántas consultas hace tu web antes y después de activarlo. Si un plugin añade 30 consultas extra por carga de página, piénsatelo dos veces.
- Revisar la base de datos periódicamente: No hace falta hacerlo cada semana, pero una vez al mes o cada dos meses, echa un vistazo al tamaño de las tablas, al autoload de
wp_options, a los transients acumulados y a las tareas de cron. Cinco minutos de comprobación te pueden ahorrar horas de diagnóstico cuando algo va mal.
Qué pedirle a tu hosting cuando ya has hecho todo lo que podías
Si has optimizado consultas, creado índices, limpiado tablas y tu web sigue lenta, el siguiente paso es hablar con tu hosting. Pídeles que revisen los parámetros de configuración de MySQL/MariaDB, especialmente los buffers de caché de consultas, el innodb_buffer_pool_size y los límites de conexiones simultáneas. También pídeles acceso al slow query log si no lo tienes ya. Un buen hosting colaborará contigo en esto sin problemas.
Para terminar
La base de datos es una de esas partes de WordPress que mucha gente ignora hasta que da problemas. Cuando los da, suele ser porque lleva meses o años acumulando consultas innecesarias, datos huérfanos y configuraciones que nadie revisó.
Lo bueno es que, como has visto, no necesitas ser un experto en MySQL ni tener acceso de administrador al servidor para diagnosticar y solucionar la mayoría de estos problemas. Con un plugin como Query Monitor, un par de snippets PHP y las consultas de diagnóstico que hemos visto, puedes hacer un análisis bastante completo de tu base de datos sin depender de nadie.
Un par de consejos finales basados en la experiencia:
- No optimices a ciegas: Diagnostica primero, arregla después. Crear índices, borrar datos o cambiar configuraciones sin saber qué consultas están fallando es como tomar medicinas sin saber qué enfermedad tienes. Puede funcionar por suerte, pero probablemente no.
- Haz copia de seguridad antes de cada cambio: Sé que ya lo he dicho, pero no me canso de repetirlo. Un DELETE mal ejecutado en la base de datos no tiene botón de deshacer.
- Empieza por lo que más impacto tenga. Si tu
autoloadpesa 5 MB y tienes 200.000 registros de postmeta huérfano, arreglar esas dos cosas probablemente mejore más el rendimiento que todas las demás optimizaciones juntas. - No dejes herramientas de diagnóstico en producción: Ni
SAVEQUERIES, ni el mu-plugin de registro, ni Query Monitor activo permanentemente. Son herramientas temporales para diagnosticar, no para dejar instaladas. Cada una consume recursos adicionales que no necesitas en el día a día. - Si un plugin es el culpable, empieza a pensar en cambiarlo: A veces la solución no es optimizar la consulta del plugin, sino buscar una alternativa que haga lo mismo sin machacar la base de datos. No tiene sentido parchear las consecuencias si puedes eliminar la causa.
¿Te gustó este artículo? ¡Ni te imaginas lo que te estás perdiendo en YouTube!






