Por qué las consultas SQL lentas son el cuello de botella oculto de WordPress
WordPress ejecuta decenas de consultas SQL por carga de página. La mayoría terminan en menos de 1 ms. Pero basta con una consulta lenta—un único JOIN de 800 ms sobre wp_postmeta—para que toda tu página parezca rota.
PageSpeed dice 95. Los usuarios dicen "va lento". El frontend vuela. El servidor tarda más de 2 segundos antes siquiera de empezar a enviar HTML.
Lo peor: WordPress no registra las consultas lentas por defecto. No hay aviso ni alerta en el escritorio. Las consultas se degradan en silencio a medida que crece tu base de datos—y solo te enteras cuando los usuarios empiezan a quejarse.
Qué provoca las consultas lentas en la base de datos de WordPress
1. JOINs sobre postmeta de WooCommerce
WooCommerce guarda los datos de producto en wp_postmeta como pares clave-valor. Filtrar productos por precio, stock o atributos exige varios JOIN sobre una tabla sin índices:
SELECT p.ID FROM wp_posts p
INNER JOIN wp_postmeta pm1 ON p.ID = pm1.post_id
INNER JOIN wp_postmeta pm2 ON p.ID = pm2.post_id
WHERE pm1.meta_key = '_price' AND pm1.meta_value BETWEEN 10 AND 50
AND pm2.meta_key = '_stock_status' AND pm2.meta_value = 'instock'
Con 10.000 productos y 500.000 filas en postmeta, esta consulta puede tardar 2–5 segundos sin la indexación adecuada.
2. Búsquedas por meta_key sin índice
La tabla wp_postmeta por defecto solo tiene un índice sobre post_id. Cualquier consulta que filtre por meta_key + meta_value hace un escaneo completo de la tabla:
SELECT post_id FROM wp_postmeta
WHERE meta_key = '_thumbnail_id'
-- Full table scan on 500K+ rows
3. Consultas LIKE sobre columnas de texto grandes
Los plugins de búsqueda y las búsquedas del admin suelen ejecutar LIKE '%term%' sobre post_content. Eso se salta todos los índices y escanea la columna entera:
SELECT ID FROM wp_posts
WHERE post_content LIKE '%shipping policy%'
-- Scans every row, every time
4. Consultas COUNT con WHERE complejos
Las tablas de listado del admin ejecutan consultas COUNT para mostrar la paginación. Combinadas con filtros de taxonomía y consultas de metadatos, pueden salir sorprendentemente caras en sitios grandes.
Cómo encontrar consultas lentas en WordPress
-
Activa SAVEQUERIES — Añade
define('SAVEQUERIES', true);awp-config.php. WordPress registrará cada consulta con su tiempo y la función que la llamó. Revisa$wpdb->queriesdespués de cargar la página. -
Revisa el slow query log de MySQL — Si tienes acceso al servidor:
SET GLOBAL slow_query_log = 'ON';ySET GLOBAL long_query_time = 0.5;para cazar consultas de más de 500 ms. -
Usa EXPLAIN en las consultas sospechosas — Antepón
EXPLAINa cualquier consulta lenta para ver el plan de ejecución. Buscatype: ALL(escaneo completo de tabla) y valores derows:en cientos de miles. -
Perfila una página concreta — Usa Query Monitor o activa
SAVEQUERIEStemporalmente. Ordena por tiempo de ejecución. Las 3 consultas más lentas suelen explicar el 80% de tu TTFB. - Comprueba en hora punta — Las consultas lentas que van "bien" con 10 usuarios simultáneos se vuelven catastróficas con 100. Prueba bajo carga, no solo en tu entorno de desarrollo.
Cómo leer un plan de EXPLAIN en una consulta real de WordPress
EXPLAIN es lo más parecido a un depurador que tiene MySQL. Se lo antepones a cualquier SELECT y MySQL te cuenta cómo piensa ejecutar la consulta: qué tablas escanea, qué índices usa y cuántas filas espera tocar. Aquí tienes la consulta de filtrado de WooCommerce de antes, con un ORDER BY típico añadido, tal y como la ejecuta una página real de tienda:
EXPLAIN SELECT p.ID FROM wp_posts p
INNER JOIN wp_postmeta pm1 ON p.ID = pm1.post_id
INNER JOIN wp_postmeta pm2 ON p.ID = pm2.post_id
WHERE pm1.meta_key = '_price' AND pm1.meta_value BETWEEN 10 AND 50
AND pm2.meta_key = '_stock_status' AND pm2.meta_value = 'instock'
ORDER BY p.post_date DESC LIMIT 20;
En una tienda con unas 800.000 filas de postmeta, el plan vuelve con esta pinta:
| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | pm1 | ALL | post_id, meta_key | NULL | 812441 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | p | eq_ref | PRIMARY | PRIMARY | 1 | Using where |
| 1 | SIMPLE | pm2 | ref | post_id, meta_key | post_id | 4 | Using where |
La primera fila es todo el problema. Así se lee:
- type: ALL: un escaneo completo de tabla. MySQL lee cada fila de wp_postmeta y comprueba la condición WHERE en cada una. Sobre 812.000 filas, en cada carga de página. Es el peor tipo de acceso que EXPLAIN puede enseñarte.
- key: NULL: no se usó ningún índice. Fíjate en que possible_keys lista meta_key, así que el índice existe: MySQL simplemente lo rechazó. ¿Por qué? Porque '_price' coincide con una parte enorme de la tabla (todos los productos tienen uno), así que el optimizador decidió que un escaneo sale más barato que un índice que casi no filtra nada. El índice no falta, es que no sirve para esta consulta.
- Using temporary: MySQL tiene que construir una tabla temporal interna para guardar resultados intermedios antes de poder ordenarlos. Con conjuntos de resultados grandes, esa tabla temporal se desborda de la memoria al disco, y el tiempo de la consulta pasa de milisegundos a segundos.
- Using filesort: el ORDER BY no lo puede resolver ningún índice, así que MySQL ordena el resultado a mano. El nombre engaña —no siempre toca disco—, pero combinado con 812.000 filas escaneadas significa ordenar un montón enorme de datos en cada petición.
La solución es un índice que el optimizador quiera de verdad: uno que filtre por meta_key Y meta_value juntos, para que '_price' más el rango de valores reduzca a unos pocos miles de filas en vez de coincidir con media tabla.
ALTER TABLE wp_postmeta
ADD INDEX wpmt_key_value (meta_key(191), meta_value(32));
Ejecuta EXPLAIN otra vez y la primera fila cambia por completo:
| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | pm1 | range | post_id, meta_key, wpmt_key_value | wpmt_key_value | 2874 | Using where |
| 1 | SIMPLE | p | eq_ref | PRIMARY | PRIMARY | 1 | Using where |
| 1 | SIMPLE | pm2 | ref | post_id, meta_key, wpmt_key_value | wpmt_key_value | 2 | Using where |
type: range significa que MySQL solo recorre las entradas del índice entre los dos límites de precio: 2.874 filas en vez de 812.441. Eso es 280 veces menos trabajo antes incluso de que la consulta llegue a la ordenación. La búsqueda por igualdad en pm2 obtiene type: ref con el mismo índice, a 2 filas por producto. Puede que siga apareciendo un filesort en Extra, pero ordenar 20 filas candidatas es ruido; ordenar 800.000 era la caída.
Esa es toda la habilidad: ejecuta EXPLAIN, mira type, key y rows en cada tabla, y arregla la fila que hace más trabajo. El Slow Query Analyzer de WP Multitool hace exactamente este análisis por ti —en local, en cada consulta lenta que caza— y te dice qué índice añadir.
Referencias de rendimiento de consultas en WordPress
No todas las consultas lentas son iguales. Esto es a lo que apuntar:
Si una sola consulta pasa de 100 ms, merece la pena investigarla. Si tu tiempo total de base de datos supera los 500 ms, tus usuarios lo notan—incluso con caché de página.
Cómo acelerar las consultas de base de datos de WordPress (arreglos prácticos)
Encontrar la consulta lenta es la mitad del trabajo. Una regla antes de nada: optimiza la consulta, no el síntoma. No añadas índices a ciegas: entiende por qué la consulta es lenta. A veces la solución es reestructurar los datos (tablas propias en vez de postmeta). A veces es evitar la consulta del todo (cachear en transients las agregaciones caras). Esto es lo que de verdad hace las consultas más rápidas, en el orden en que yo lo probaría.
1. Añade los índices que te está pidiendo tu plan de EXPLAIN
El índice al que llega el recorrido de EXPLAIN de arriba es el primero que añado: un índice compuesto sobre wp_postmeta que cubre las dos columnas por las que filtran las meta queries de WordPress:
ALTER TABLE wp_postmeta
ADD INDEX wpmt_key_value (meta_key(191), meta_value(32));
Cubrir meta_key y meta_value juntos permite a MySQL acotar las dos condiciones en un solo recorrido del índice. En el recorrido de antes eso convirtió type: ALL en type: range y bajó las filas examinadas de 812.441 a 2.874. Las longitudes de prefijo (191) y (32) mantienen el índice compacto sin dejar de cubrir búsquedas reales. No añadas índices por fe: ejecuta EXPLAIN primero y luego añade el que le falta al plan.
Un apunte: por internet verás recomendado un índice de una sola columna sobre meta_value. No puede ayudar a una consulta que filtra por meta_key y meta_value a la vez: el índice compuesto de arriba cubre ese caso, así que empieza por ahí.
2. Acelera las consultas de productos y pedidos de WooCommerce
Los JOINs sobre postmeta de WooCommerce que vimos antes son el caso clásico: cada condición de meta_query añade un JOIN sobre una tabla EAV de 4 columnas. Deja de filtrar por postmeta cuando puedas desnormalizar. Para los campos calientes por los que filtras todo el rato —precio, stock, valoración— copia el valor a una tabla de consulta específica con columnas e índices de verdad. WooCommerce trae wc_product_meta_lookup exactamente por eso; úsala, o monta el mismo patrón para tus propios campos.
3. Deja de usar LIKE '%term%' y llamadas a WP_Query sin límite
Un comodín al principio nunca puede usar un índice B-tree: MySQL escanea todas las filas, siempre. Añade un índice FULLTEXT sobre post_content y consulta con MATCH() AGAINST(), o saca la búsqueda de la tabla de posts a un plugin de búsqueda dedicado o a un motor externo. Cualquier cosa es mejor que escanear un longtext.
No consultes nunca con posts_per_page => -1. Las consultas sin límite funcionan bien con 200 entradas y se hunden con 20.000. Pon un límite real y pagina, o procesa por lotes con offsets en tareas cron. La consulta lenta que no puedes reproducir en local suele ser una de estas golpeando datos de tamaño real.
Y evita los bosques de OR en meta_query. Las relaciones OR anidadas entre varias meta keys empujan a MySQL a escaneos amplios y tablas temporales. Mejor una consulta concreta, una columna de bandera desnormalizada, o dos consultas baratas que juntas en PHP, que una meta_query monstruosa.
4. Cachea agregados y mata los bucles N+1 de metadatos
Empieza por la caché de objetos. Redis o Memcached guardan los resultados de las consultas en memoria: las consultas repetidas van a la caché en vez de a MySQL. Es el cambio de mayor impacto para la mayoría de sitios.
Después, cachea tú mismo los agregados caros. COUNT() sobre WHERE complejos, widgets de "entradas de este mes", recuentos de términos... no necesitan estar frescos en cada petición. Calcúlalos una vez, guárdalos en un transient o en la caché de objetos y refréscalos de forma programada o en save_post. Un COUNT de 900 ms que se ejecuta una vez a la hora no cuesta nada.
Luego recorta lo que trae WP_Query. Si solo necesitas IDs, dilo: 'fields' => 'ids' evita cargar los objetos de fila completos. 'no_found_rows' => true elimina la pasada de SQL_CALC_FOUND_ROWS cuando no paginas. 'update_post_meta_cache' => false y 'update_post_term_cache' => false se saltan las consultas de precalentado de caché cuando no vas a tocar metadatos ni términos, pero deja el precalentado de metadatos ACTIVADO si vas a leer metadatos en un bucle, porque esa única consulta de precalentado es la que te salva de una consulta get_post_meta() por entrada.
Por último, recorta la consulta que WordPress ejecuta antes que todas las demás. Cada petición empieza cargando todas las opciones en autoload antes de que se dispare tu primera consulta. Arreglar las opciones infladas en autoload acelera todas las páginas, no solo las lentas.
5. Qué aspecto tiene "más rápido" (mediciones tras cada arreglo)
Aplica un arreglo cada vez y vuelve a medir después: EXPLAIN más una comprobación de tiempos. Los objetivos son las referencias de arriba: menos de 50 ms por consulta y menos de 200 ms de tiempo total de base de datos por página. En el recorrido de EXPLAIN, un solo índice compuesto bajó las filas escaneadas de 812.441 a 2.874; esa es la escala del cambio que produce un índice correcto. Si un arreglo no mueve el plan de EXPLAIN ni los tiempos, deshazlo y pasa al siguiente.
Slow query log frente a Query Monitor y a la detección automática
Hay tres formas de cazar una consulta lenta. Responden a preguntas distintas:
| Enfoque | ¿Hace falta acceso al servidor? | ¿Funciona en continuo? | ¿Captura la traza? | Sugiere arreglos de índices | Seguro en producción |
|---|---|---|---|---|---|
| Manual (SAVEQUERIES / slow log de MySQL) | Sí: permisos de my.cnf o SET GLOBAL | El log de MySQL sí, pero nadie lo lee hasta que algo se rompe | No: obtienes el SQL, no el PHP que lo ejecutó | No: ejecutas EXPLAIN e interpretas tú | SAVEQUERIES no (sobrecarga de memoria en cada petición); el log de MySQL sí, con un umbral razonable |
| Plugin Query Monitor | No | No: muestra la carga de página actual, y solo a administradores identificados | Sí: pila de llamadas completa, su mejor función | No | Pensado para desarrollo. Se puede instalar en producción, pero solo ve las peticiones que haces mientras lo miras: no va a cazar el pico de las 3 de la mañana |
| Slow Query Analyzer de WP Multitool | No: funciona en hosting compartido | Sí: registra cada consulta por encima de tu umbral, con tráfico real, las 24 horas | Sí: el archivo del plugin o del tema que lanzó la consulta | Sí: ejecuta EXPLAIN en local y señala el índice que falta | Sí: diseñado para funcionar en sitios en producción con una sobrecarga mínima |
Query Monitor es realmente bueno en lo suyo; yo también lo uso. Simplemente responde a otra pregunta. Te dice qué hizo esta carga de página mientras miras; no puede decirte qué hizo tu sitio el martes pasado bajo carga. Ese es el hueco que llena la detección continua.
Por qué las consultas lentas vuelven una y otra vez
Encontrar las consultas lentas una vez no basta. Vuelven:
- Las actualizaciones de plugins introducen consultas nuevas o cambian las existentes
- Tu base de datos crece—una consulta rápida con 10.000 filas es lenta con 500.000
- Los nuevos tipos de contenido añaden filas de postmeta y de taxonomías
- Las ventas de WooCommerce acumulan datos de pedidos en las mismas tablas
Las comprobaciones manuales con SAVEQUERIES son tediosas y fáciles de olvidar. Si además las consultas lentas hacen que tu admin de WordPress vaya lento, el problema se acumula. Necesitas monitorización continua que cace las regresiones en cuanto ocurren—no después de que los usuarios se quejen.
Consultas lentas en WordPress: preguntas frecuentes
- ¿Cómo encuentro consultas lentas en WordPress?
- Tres formas: activar el slow query log de MySQL con un long_query_time de unos 0,5 s (hace falta acceso al servidor), instalar Query Monitor e inspeccionar las cargas de página estando identificado, o usar un plugin de monitorización que registre las consultas lentas de forma continua con tráfico real. Las dos primeras cazan las consultas que disparas tú; solo la monitorización continua caza las consultas lentas con las que se topan tus visitantes cuando no estás mirando.
- ¿Cómo acelero las consultas de base de datos de WordPress?
- No hace falta reescribir nada: la mayoría de arreglos son cambios puntuales. Ejecuta primero EXPLAIN sobre la consulta lenta: te dice si MySQL está escaneando la tabla entera (type: ALL, key: NULL). La mayoría de consultas lentas de WordPress se arreglan añadiendo el índice adecuado, normalmente uno compuesto sobre wp_postmeta que cubra meta_key y meta_value. Más allá de los índices: pon una caché de objetos (Redis o Memcached) delante de MySQL, deja de hacer consultas sin límite como posts_per_page => -1, sustituye las búsquedas LIKE '%term%' por FULLTEXT, cachea los recuentos caros y arregla las opciones infladas en autoload, que añade latencia a cada consulta antes incluso de que se ejecute.
- ¿Cuál es un buen tiempo de consulta en WordPress?
- Las consultas individuales deberían quedarse por debajo de 50 ms; la mayoría bien indexadas se ejecutan en menos de 5 ms. Una página típica debería necesitar menos de 30 consultas y pasar menos de 200 ms en la base de datos en total. Si una sola consulta pasa de 500 ms, merece un EXPLAIN; por encima de 1 s ya está dañando activamente tu TTFB en cada carga sin caché.
- ¿La caché de página arregla las consultas lentas?
- No: las esconde. Los visitantes cacheados reciben HTML rápido, pero cada fallo de caché, cada usuario identificado, cada página de carrito y cada petición del admin siguen pagando el coste completo de la consulta. La consulta lenta sigue ahí, sigue quemando CPU, y reaparece en cuanto sube el tráfico o se purga la caché. La caché merece la pena, pero es una capa encima de una base de datos arreglada, no un sustituto de ella.
Deja de adivinar. Empieza a registrar.
El Slow Query Analyzer de WP Multitool registra cada consulta por encima de tu umbral, captura la traza completa y sugiere arreglos de índices concretos. Sin necesidad de acceso al servidor.
Consigue WP Multitool Guía de rendimiento del backend