Logo
WP Fix by Blimx

Optimización de Queries WordPress — Índices, EXPLAIN y SQL Inteligente

Actualizado:
PerformanceDatabase

Por qué WordPress se vuelve lento

El schema de WordPress está diseñado para flexibilidad, no velocidad. El mayor culpable es la tabla estilo EAV wp_postmeta que guarda todo como filas post_id, meta_key, meta_value. Las queries contra esta tabla crecen caras a medida que los datos crecen.

Tras 200,000 filas en wp_postmeta, incluso búsquedas simples de post pueden tomar 5+ segundos sin índices apropiados. Tras 1 millón, el sitio se vuelve inusable.

Este artículo documenta los cuatro patrones de query que afinamos en cada auditoría de rendimiento WordPress, con firmas EXPLAIN y el arreglo para cada uno.

Herramienta: habilita el slow query log

Antes de cualquier optimización, identifica las queries lentas reales:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

Navega el sitio como un usuario típico por 30 minutos. Después:

mysqldumpslow -s t -t 20 /var/log/mysql/slow.log

Top 20 queries por tiempo total. Estos son tus candidatos de optimización.

Patrón 1 — Escaneo wp_postmeta sin índice

La query (búsqueda meta de Yoast SEO):

SELECT meta_value FROM wp_postmeta 
WHERE post_id = 12345 AND meta_key = '_yoast_wpseo_metadesc';

EXPLAIN muestra:

type: ref
key: post_id
rows: 250
Extra: Using where

Rows = 250 significa que MySQL escanea 250 filas postmeta solo para encontrar la una con _yoast_wpseo_metadesc. Con 100 page loads por minuto, eso es 25,000 lecturas de fila innecesarias por minuto.

Arreglo: índice compuesto

ALTER TABLE wp_postmeta 
ADD INDEX idx_post_meta_key (post_id, meta_key);

Después:

type: ref
key: idx_post_meta_key
rows: 1
Extra: Using index condition

Rows = 1. El tiempo de query cae 50-100ms por llamada.

El core de WordPress en realidad envía este índice pero los plugins a veces lo dropean o renombran durante actualizaciones de base de datos. Verifica:

SHOW INDEX FROM wp_postmeta;

Busca un índice conteniendo ambos post_id y meta_key en ese orden.

Patrón 2 — meta_query ineficiente

La query (un filtro típico de producto WooCommerce):

SELECT p.* 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
INNER JOIN wp_postmeta pm3 ON p.ID = pm3.post_id
WHERE p.post_type = 'product' AND p.post_status = 'publish'
  AND pm1.meta_key = '_price' AND pm1.meta_value BETWEEN 10 AND 100
  AND pm2.meta_key = '_stock_status' AND pm2.meta_value = 'instock'
  AND pm3.meta_key = '_visibility' AND pm3.meta_value = 'catalog';

Tres joins en wp_postmeta más un filtro de rango. EXPLAIN a menudo muestra escaneos de múltiples millones de filas.

Arreglo: tablas de lookup dedicadas para campos filtrados

WooCommerce envía wp_wc_product_meta_lookup exactamente para esto — una tabla plana con product_id, sku, stock_status, stock_quantity, regular_price, sale_price, on_sale. Consulta:

SELECT product_id FROM wp_wc_product_meta_lookup
WHERE stock_status = 'instock'
  AND min_price BETWEEN 10 AND 100;

Una tabla, columnas indexadas, regresa en milisegundos.

Para custom post types donde necesitas filtrado similar, crea tu propia tabla de lookup y puébala vía hook save_post.

Patrón 3 — Búsqueda post_meta sin límite

La query (alternativa de full-text que algunos plugins usan):

SELECT post_id FROM wp_postmeta 
WHERE meta_value LIKE '%search term%';

EXPLAIN:

type: ALL
key: NULL
rows: 850000
Extra: Using where

Escaneo de tabla completo. Con 850,000 filas, esto toma 8+ segundos.

Arreglo: no uses LIKE en meta_value

Esto está fundamentalmente mal. MySQL no puede usar índices en LIKE '%X%' porque el wildcard está en ambos extremos.

Mejores aproximaciones: 1. Usa un índice real de búsqueda — Elasticsearch, Algolia, MeiliSearch o el índice posts.post_content FULLTEXT de WordPress 2. Usa `LIKE 'X%'` (wildcard solo al final) — puede usar índice si meta_value está indexado 3. Usa búsquedas `meta_key` en su lugar — guarda el campo buscable como meta_key, no meta_value

Para WordPress full-text en posts:

ALTER TABLE wp_posts ADD FULLTEXT(post_title, post_content);

Después:

SELECT * FROM wp_posts
WHERE MATCH(post_title, post_content) AGAINST('search term');

Sub-100ms incluso en tablas de múltiples millones de filas.

Patrón 4 — Homepage WordPress con demasiados posts y ACF

Query del tema:

SELECT * FROM wp_posts WHERE post_type = 'post' AND post_status = 'publish' 
ORDER BY post_date DESC LIMIT 20;

Eso devuelve 20 filas rápido. El problema viene de lo que corre después: para cada post, el tema llama get_field('hero_image'), get_field('subtitle'), etc. Cada llamada consulta wp_postmeta.

Con 20 posts y 10 campos ACF cada uno, has añadido 200 queries wp_postmeta separadas. Cada una toma 5-50ms. Total: 1-10 segundos añadidos al TTFB del homepage.

Arreglo: consulta meta en bulk

El core de WordPress tiene update_meta_cache():

$post_ids = wp_list_pluck($posts, 'ID');
update_meta_cache('post', $post_ids);

Una query carga todo el meta para todos los posts. Las llamadas subsiguientes get_field() golpean el cache.

Mejor: usa filtro pre_get_posts para configurar update_post_meta_cache = true en tu query principal — que es el default pero plugins a veces lo deshabilitan por "optimización."

Patrón 5 — Página admin lenta

La lentitud admin usualmente rastrea a:

SELECT COUNT(*) FROM wp_posts WHERE post_type = 'attachment' AND post_status = 'inherit';

Este es el conteo de Media Library. Con 100,000+ adjuntos, esta query sola toma 2-5 segundos.

Arreglo: bypasea el conteo

add_filter('manage_upload_columns', function($cols) {
    // tu customización
    return $cols;
});

add_filter('media_views_l10n', function($strings) {
    // strings que incluyen conteos
    return $strings;
});

// Más agresivamente:
add_filter('wp_count_attachments', function() {
    return (object) array('inherit' => 100000);
});

Esto devuelve un conteo estático, eliminando la query lenta al costo de números ligeramente obsoletos.

Cómo añadir índices de forma segura

Siempre prueba índices en staging primero. Añadir un índice en una tabla grande puede tomar 5-30 minutos durante los cuales la tabla está bloqueada.

-- En staging
ALTER TABLE wp_postmeta 
ADD INDEX idx_post_meta_key (post_id, meta_key);

SHOW INDEX FROM wp_postmeta;

-- Verifica que EXPLAIN mejora antes de producción:
EXPLAIN SELECT meta_value FROM wp_postmeta WHERE post_id = 123 AND meta_key = '_yoast_wpseo_metadesc';

Para producción, usa ALGORITHM=INPLACE, LOCK=NONE:

ALTER TABLE wp_postmeta 
ADD INDEX idx_post_meta_key (post_id, meta_key),
ALGORITHM=INPLACE, LOCK=NONE;

Esto añade el índice online sin bloquear lecturas. Las escrituras son brevemente ralentizadas pero no bloqueadas.

Errores comunes durante optimización de queries

  • Añadir muchos índices "por si acaso" — cada índice ralentiza escrituras; solo añade índices que mediste
  • Confiar en EXPLAIN sin considerar volumen de datos — lo que se ve rápido en staging (datos pequeños) gatea en producción
  • Olvidar ANALYZE TABLE — tras añadir índices o cambios bulk de datos, corre ANALYZE TABLE wp_postmeta; para actualizar estadísticas
  • Optimizar la query equivocada — mide primero, optimiza las queries que el slow log realmente marcó

Midiendo el éxito

Repite el análisis de slow query tras cambios:

mysqldumpslow -s t -t 20 /var/log/mysql/slow.log

Si tus top 5 queries lentas ya no aparecen, las arreglaste. Itera en las siguientes 5.

Cuándo llamar a un especialista

Una base de datos que ha estado lenta por años usualmente necesita una auditoría multi-día: identificar queries lentas, diseñar índices, refactorizar código custom que construye queries ineficientes. Hacemos esto rutinariamente.

Auditoría de velocidad de base de datos — típicamente 4-12 horas de trabajo. Para rendimiento más amplio ve recuperación de velocidad WordPress.