WordPress hace muchas más consultas a la base de datos de las que parece, en una página normalita pueden ser 30, 40 o más de 100. Pero el problema no es ese número en sí, sino las que se repiten, como la misma SELECT lanzada cuatro veces, ocho veces o más, por culpa de un tema mal hecho, un plugin que no cachea bien o un bucle que pide los mismos datos una y otra vez.
Cada query duplicada es trabajo que la base de datos hace dos o más veces sin necesidad. Si tu página de inicio dispara la misma consulta a wp_options diez veces y tienes 5.000 visitas al día, son 50.000 queries innecesarias diarias. En hosting compartido se nota, cuando tienes picos de tráfico ya ni te cuento.
Para detectarlas no hace falta ningún plugin, basta con activar SAVEQUERIES y meter un pequeño código (que te voy a regalar) en un mu-plugin y tienes el resumen al pie de cada página. Treinta segundos de trabajo y listo. ¿Te animas?
Activar SAVEQUERIES
SAVEQUERIES es una constante de WordPress que guarda en memoria todas las queries de la petición, con el tiempo que tarda cada una. Por defecto está desactivada porque consume memoria, pero activarla es fácil, solo tiene que añadir esto a tu wp-config.php, justo antes de la línea que dice «Eso es todo, deja de editar»:
define( 'SAVEQUERIES', true );
Advertencia: debes activarla solo en local, en staging o en producción durante el rato justo del diagnóstico. Si la dejas activa en una web con tráfico real te comes la memoria del servidor sin ganar nada a cambio.
Código para mostrar queries duplicadas
Crea un archivo en /wp-content/mu-plugins/ llamado ayudawp-debug-queries.php. Si no tienes esa carpeta, créala tú y pega esto dentro:
<?php
/**
* Plugin Name: Debug de queries duplicadas
* Plugin URI: https://servicios.ayudawp.com
* Description: Muestra un en el pie de página un resumen de queries duplicadas. Solo lo ven los admins y solo cuando está activo SAVEQUERIES.
* Version: 1.0
* Author: Fernando Tellado
* Author URI: https://ayudawp.com
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'wp_footer', 'ayudawp_mostrar_queries_duplicadas', 9999 );
function ayudawp_mostrar_queries_duplicadas() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( ! defined( 'SAVEQUERIES' ) || ! SAVEQUERIES ) {
return;
}
global $wpdb;
if ( empty( $wpdb->queries ) ) {
return;
}
$contador = array();
$tiempos = array();
// $callers = array(); // Descomenta para guardar quién dispara cada query.
foreach ( $wpdb->queries as $query ) {
$sql = $query[0];
$tiempo = $query[1];
// $caller = isset( $query[2] ) ? $query[2] : ''; // Descomenta junto con $callers.
// Normaliza la query para que los duplicados con distintos valores también coincidan
$sql_norm = preg_replace( '/\d+/', 'N', $sql );
$sql_norm = preg_replace( "/'[^']*'/", "'X'", $sql_norm );
$sql_norm = trim( preg_replace( '/\s+/', ' ', $sql_norm ) );
if ( ! isset( $contador[ $sql_norm ] ) ) {
$contador[ $sql_norm ] = 0;
$tiempos[ $sql_norm ] = 0;
// $callers[ $sql_norm ] = $caller;
}
$contador[ $sql_norm ]++;
$tiempos[ $sql_norm ] += $tiempo;
}
// Mantiene solo las queries que aparecen más de una vez
$duplicadas = array_filter(
$contador,
function ( $n ) {
return $n > 1;
}
);
arsort( $duplicadas );
$total_queries = count( $wpdb->queries );
$tiempo_bd = round( $wpdb->timer_stop, 4 );
if ( empty( $duplicadas ) ) {
printf(
'<div style="position:fixed;bottom:0;left:0;right:0;background:#0a4d1f;color:#fff;padding:10px;font:12px monospace;z-index:99999;">Sin queries duplicadas. Total: %d queries en %ss.</div>',
(int) $total_queries,
esc_html( $tiempo_bd )
);
return;
}
echo '<div style="position:fixed;bottom:0;left:0;right:0;max-height:50vh;overflow:auto;background:#1a1a1a;color:#fff;padding:15px;font:12px monospace;z-index:99999;border-top:3px solid #ff6b6b;">';
printf(
'<strong style="color:#ff6b6b;">%d tipos de queries duplicadas</strong> · Total: %d queries · Tiempo BD: %ss<br><br>',
count( $duplicadas ),
(int) $total_queries,
esc_html( $tiempo_bd )
);
foreach ( $duplicadas as $sql => $veces ) {
$tiempo_ms = round( $tiempos[ $sql ] * 1000, 2 );
$sql_corta = strlen( $sql ) > 250 ? substr( $sql, 0, 250 ) . '...' : $sql;
printf(
'<div style="margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #333;"><span style="color:#ff6b6b;font-weight:bold;">×%d</span> <span style="color:#888;">[%sms]</span> %s</div>',
(int) $veces,
esc_html( $tiempo_ms ),
esc_html( $sql_corta )
);
}
echo '</div>';
}
Lo que hace ese código:
- Solo se ejecuta para usuarios con permisos de administrador (
manage_options). - Solo entra si
SAVEQUERIESestá activo. - Normaliza las consultas sustituyendo números y cadenas variables por marcadores, así detecta como duplicadas las que solo cambian en los valores (un
SELECTpor ID 5 y otro por ID 7 cuentan como la misma). - Imprime al pie de página de la parte visible de la web un resumen con cuántas veces se repite cada
queryy cuánto tiempo total se va en cada una.
Cómo interpretar el resultado
Al cargar cualquier página en portada habiéndote conectado como admin te aparece una franja al pie con algo así:
3 tipos de queries duplicadas · Total: 47 queries · Tiempo BD: 0,0234s ×6 [12,4ms] SELECT option_value FROM wp_options WHERE option_name = 'X' LIMIT N ×4 [3,2ms] SELECT * FROM wp_postmeta WHERE post_id = N AND meta_key = 'X' ×3 [1,8ms] SELECT post_status FROM wp_posts WHERE ID = N
Y lo que significa el ejemplo es esto:
- La consulta a
wp_optionsrepetida seis veces es casi seguro unget_option()llamado en varios hooks sin cachear el resultado en una variable estática. Plugin o tema mal hecho. - La de
postmetarepetida cuatro veces es la señal clásica del problema N+1: falta una llamada aupdate_post_meta_cache()antes de un bucle de posts. - La tercera puede ser un
get_post_status()o similar dentro de otro bucle.
Qué hacer cuando encuentras duplicadas
Depende de quién las dispara y de tu nivel de acceso al código:
- Si la
queryse repite por un plugin de terceros busca la opción que la lanza y mira si puedes desactivarla. A veces es un widget, una barra lateral o una sección de la cabecera que tampoco te aporta gran cosa. - Si es de tu propio tema o plugin cachea el resultado en una variable estática dentro de la función, o mejor todavía en
wp_cache_set()si tienes caché de objetos (Redis o Memcached). - Para el patrón
N+1depostmetaañadeupdate_post_meta_cache( $post_ids )antes del bucle. Una solaqueryprecarga toda la meta de los posts de golpe. - Para llamadas repetidas a
get_option()agrupa varias opciones en un únicoarray, o cachéalas en memoria de la petición conwp_cache_set().
Si te aparece una query rara y no sabes de dónde viene, en el código hay dos líneas comentadas con $callers y $caller. Descoméntalas temporalmente y luego cambia el último printf para que también imprima $callers[ $sql ]. Verás el callstack completo, función por función, hasta saber exactamente quién dispara cada consulta.
Y acuérdate de quitarlo cuando termines
Cuando hayas terminado el diagnóstico, dos cosas:
- Borra o renombra el archivo del mu-plugin.
- Quita la línea
define( 'SAVEQUERIES', true );dewp-config.php.
Si dejas SAVEQUERIES activo en producción cada petición guarda el array completo de queries en memoria durante todo el render. En sitios con tráfico real son 200 KB extra por petición por lo menos. No te tira el sitio pero es memoria que consumes sin necesidad.
Para diagnósticos más completos (callstack siempre visible, agrupación por componente, hooks ejecutados, monitorización de AJAX y REST API) la herramienta de referencia sigue siendo Query Monitor. Pero para una revisión rápida y puntual sin instalar nada este snippet va de sobra.
¿Te gustó este artículo? ¡Ni te imaginas lo que te estás perdiendo en YouTube!







