Guía de campo de WP_Query: get_posts vs new WP_Query, límites en posts_per_page, meta_query indexado, no_found_rows, Query Monitor y SAVEQUERIES.
ES

Una guía práctica de WP_Query y el Loop (edición de rendimiento 2026)

5.00 /5 - (29 votes )
Última verificación: 1 de mayo de 2026
12min de lectura
Guía
Desarrollador full-stack
Core Web Vitals

`WP_Query` es el constructor SQL detrás de cada petición frontal: archivos, posts individuales, endpoints REST y bucles de tienda WooCommerce. Cuando una página va lenta, la causa suele ser una de tres cosas: un `meta_query` contra una `meta_key` sin índice, un `posts_per_page` sin límite, o `SQL_CALC_FOUND_ROWS` ejecutándose en cada carga cuando la página no muestra ningún total.

Orden de magnitud sobre un caso real de una tienda WooCommerce alojada en Webempresa con 50.000 productos: un meta_query contra _stock_status sin índice sobre (meta_key, post_id) se mantiene entre 1,5 y 3 segundos. Tras añadir el índice compuesto, la misma consulta baja a 50–200 ms. La consulta es idéntica; lo único que cambió fue el índice.

El resto de esta guía es la memoria muscular para evitar esos tres modos de fallo y para recuperar el control cuando heredas un tema que ya los tiene cableados.


#Parte 1: Arquitectura de una solicitud

Para dominar WP_Query, debes entender que sucede cuando lo instancias. WordPress ejecuta los siguientes pasos:

  1. Análisis de argumentos: Convirtiendo tu array de argumentos en un formato estandarizado
  2. Generación SQL: Construyendo una sentencia SQL SELECT compleja
  3. Ejecucion de consulta: Enviando la solicitud a la base de datos
  4. Llenado del objeto: Poblando el objeto WP_Query con objetos de publicación y metadatos

#Dentro del objeto WP_Query

Cuando ejecutas $query = new WP_Query($args), no solo obtienes un array de publicaciónes. Obtienes un objeto masivo con propiedades criticas:

  • $query->posts: Array de objetos WP_Post
  • $query->post_count: Número de publicaciónes en la página actual
  • $query->found_posts: Total de publicaciónes que coinciden con los criterios
  • $query->max_num_pages: Total de páginas de resultados
  • $query->query_vars: Los argumentos que WordPress realmente uso

#1. El Loop principal (contexto global)

Es el loop activado por la solicitud de URL. WordPress maneja la instanciacion; tu solo iteras:

if ( have_posts() ) :
    while ( have_posts() ) : the_post();
        get_template_part( 'template-parts/content', get_post_type() );
    endwhile;
endif;

#2. El Loop secundario (consultas personalizadas)

Se usa para listas tipo “Posts relacionados”, widgets de últimas noticias o cualquier sección que muestre contenido fuera de la consulta principal. En catálogos reales conviven tres constructores que no son intercambiables:

  • new WP_Query( $args ), objeto completo, expone found_posts, max_num_pages y soporta iteración con the_post(). Úsalo cuando necesites paginación o acceso completo a las template tags.
  • get_posts( $args ), envoltorio ligero que devuelve un array de objetos WP_Post. Por defecto fija 'suppress_filters' => true y 'no_found_rows' => true, lo que es más rápido pero salta los filtros posts_*. Idóneo para listas cortas y de tamaño fijo en barras laterales.
  • query_posts(), destructivo: sobrescribe la consulta principal global, rompe los condicionales is_*() del resto de la página y obliga a reconstruir la consulta principal con wp_reset_query(). El core lo desaconseja desde 2010 (sigue presente solo por retrocompatibilidad). No lo uses. Si lo encuentras en un tema heredado, sustitúyelo por pre_get_posts para la consulta principal y new WP_Query para todo lo demás.
$args = [
    'post_type'      => 'post',
    'posts_per_page' => 5,
    'no_found_rows'  => true, // CRITICO PARA EL RENDIMIENTO
];
$query = new WP_Query( $args );

if ( $query->have_posts() ) {
    while ( $query->have_posts() ) {
        $query->the_post();
        // Renderizar título, extracto, etc.
    }
    wp_reset_postdata(); // OBLIGATORIO para restaurar el objeto global $post
}

#Parte 2: Asesinos de rendimiento (los “no hagas” de 2026)

#1. posts_per_page => -1 es un disparo en el pie

-1 indica a WP_Query que cargue cada fila coincidente como objeto WP_Post hidratado y, a continuación, ejecute update_post_meta_cache y update_post_term_cache sobre todas. En un CPT que pasó de 50 entradas a 5.000 en dos años, el código que tardaba 80 ms ahora reserva más de 200 MB de memoria PHP y agota el memory_limit típico de los planes compartidos de Raiola Networks o Webempresa.

La solución es un límite duro alineado con la superficie renderizada:

  • Widget lateral con cinco elementos → 'posts_per_page' => 5.
  • Generador de sitemap que sí necesita todas las filas → procesa por lotes con paged dentro de un bucle, no con -1. 200 IDs por lote es un techo seguro en hosting habitual.
  • Herramientas internas de exportación donde aceptas presión de memoria → aun así, fija 'fields' => 'ids' para no hidratar objetos completos.

#2. SQL_CALC_FOUND_ROWS corre incluso cuando nadie lee el total

Sin no_found_rows, WP_Query añade SQL_CALC_FOUND_ROWS al SELECT y emite un SELECT FOUND_ROWS() adicional. MySQL sigue contando después del LIMIT, y sobre una tabla wp_posts de 200.000 filas con un JOIN de tax_query esto suele añadir 80–250 ms por petición, dependiendo del estado de la caché.

Si la página no muestra “Página 3 de 47” ni un total, fija 'no_found_rows' => true. La pregunta natural (“pero quiero paginación con enlaces siguiente y anterior”) tiene respuesta sin found_posts: pide posts_per_page + 1 y renderiza posts_per_page; la fila extra señala que existe una página siguiente. Es exactamente el patrón de paginación de Twitter y de Hacker News.

Para archivos paginados que sí muestran un total real (“Mostrando 1–10 de 4.200 artículos”), conserva el conteo, pero guárdalo en un transient de cinco minutos con clave compuesta por la firma de filtros. El total raramente cambia en esa ventana; el escaneo del JOIN no necesita repetirse en cada carga.

#3. Ordenar por aleatorio (orderby => rand)

La operación más costosa en MySQL. La base de datos crea una tabla temporal, asigna un número aleatorio a cada fila y luego las ordena.

Solución:

  1. Obtener IDs de las últimás 50 publicaciónes
  2. Seleccionar 5 IDs aleatorios en PHP
  3. Ejecutar segunda consulta con post__in => $random_ids

#4. meta_query contra meta_key sin indexar

Es la causa más frecuente de archivos lentos en WooCommerce heredados, sobre todo en catálogos textiles españoles tipo Adolfo Domínguez o casos similares con miles de SKU. wp_postmeta solo trae un índice sobre meta_key, demasiado poco selectivo cuando WordPress filtra por meta_key = '_stock_status' AND meta_value = 'instock': MySQL sigue recorriendo todas las filas con esa clave.

Caso real: una tienda alojada en Stackscale con 50.000 productos. La página de catálogo enganchaba pre_get_posts para añadir un meta_query por _stock_status y un campo personalizado _marca. El TTFB P95 era de 4,1 segundos. SAVEQUERIES mostraba que el 80% del tiempo se quemaba en un JOIN contra wp_postmeta. La corrección:

ALTER TABLE wp_postmeta
  ADD INDEX idx_meta_key_post_id (meta_key(32), post_id);

Tras el índice, la misma consulta bajó a 180 ms. No tocó ni una línea de PHP. Para valores que sí participan en comparaciones de rango ('compare' => '>', 'compare' => 'BETWEEN'), también necesitas índice sobre meta_value, normalmente un índice de prefijo, porque la columna es LONGTEXT.

Cuando el espacio de valores es fijo (tallas, colores, slugs de marca), no uses meta_query en absoluto. Registra una taxonomía personalizada. wp_term_relationships ya está indexada por object_id y term_taxonomy_id, así que el mismo filtro que tardaba 1,8 s como meta_query corre en 30–60 ms como tax_query.

#5. Los JOIN de tax_query se acumulan a escala

Un tax_query con una sola taxonomía añade un JOIN contra wp_term_relationships y otro contra wp_term_taxonomy. Por encima de 200.000 posts, cada taxonomía adicional en la misma consulta añade 50–200 ms. Las páginas de filtros facetados que combinan seis taxonomías (talla + color + marca + temporada + material + rango de precio) suelen quedarse entre 800 ms y 2 s. Las opciones razonables son: desnormalizar en una única taxonomía de facetas, pasar a FacetWP o SearchWP (que indexan fuera de tabla), o llevar las facetas a un motor de búsqueda, ElasticPress sobre Elasticsearch o Algolia para catálogos de solo lectura como los que usan Mango y otras tiendas de moda en España.


#Parte 3: Lógica de consulta avanzada

#1. Relaciones con tax_query

$args = [
    'post_type' => 'product',
    'tax_query' => [
        'relation' => 'AND',
        [
            'taxonomy' => 'color',
            'field'    => 'slug',
            'terms'    => [ 'rojo', 'azul' ],
            'operator' => 'IN',
        ],
        [
            'taxonomy' => 'talla',
            'field'    => 'slug',
            'terms'    => [ 'grande' ],
        ],
    ],
];

#2. Consultas de fecha (rango dinámico)

$args = [
    'date_query' => [
        [
            'after'     => '1 de enero de 2025',
            'before'    => [
                'year'  => 2026,
                'month' => 12,
                'day'   => 31,
            ],
            'inclusive' => true,
        ],
    ],
];

#Parte 4: Cache - El secreto de la velocidad

En 2026, un sitio de alto tráfico raramente deberia tocar la base de datos para listas estaticas.

#1. Usando la API de Transients

$cache_key = 'home_trending_jobs';
$results = get_transient( $cache_key );

if ( false === $results ) {
    $results = new WP_Query( [ /* Args complejos */ ] );
    set_transient( $cache_key, $results, HOUR_IN_SECONDS );
}

#2. Desactivar el priming de caché en una consulta concreta

Tras obtener los IDs, WP_Query llama a update_post_meta_cache() y update_post_term_cache() para cargar en bloque cada fila de meta y de términos en el object cache. Es la opción correcta para un loop de plantilla normal que va a llamar a get_post_meta() y get_the_terms(). No lo es para un loop que solo renderiza título y permalink.

$args = [
    'update_post_meta_cache' => false,
    'update_post_term_cache' => false,
    'no_found_rows'          => true,
    'fields'                 => 'ids',
];

Para un archivo de 50 posts con 30 filas de meta cada uno, saltarse el priming de meta evita leer y cachear 1.500 filas en una sola petición. Combinado con 'fields' => 'ids', ni siquiera hidratas objetos WP_Post, útil para generadores de sitemap, listas de IDs de relacionados en barra lateral, o widgets del dashboard administrativo. Importante para cumplimiento AEPD: cuando registras campos de auditoría sobre quién consulta qué (logs RGPD por consulta), evita que ese hook engorde con get_post_meta() adicionales por fila renderizada, el coste se multiplica por el tamaño de la página.


#Parte 5: Páginación en loops personalizados

Por que la páginación devuelve 404: WordPress no sabe en que página estas para un WP_Query personalizado.

La solución:

$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
if ( is_front_page() ) {
    $paged = ( get_query_var( 'page' ) ) ? get_query_var( 'page' ) : 1;
}
$args = [
    'paged'          => $paged,
    'posts_per_page' => 10,
];

#Parte 6: WP_Query en stacks modernos (REST y headless)

Si construyes un frontend React usando WordPress headless, no usas WP_Query directamente en JS, pero la REST API lo usa en el backend.

#Personalizando resultados de REST API:

add_filter( 'rest_post_query', function( $args, $request ) {
    $exclude = $request->get_param( 'exclude_ids' );
    if ( ! empty( $exclude ) ) {
        $args['post__not_in'] = explode( ',', $exclude );
    }
    return $args;
}, 10, 2 );

#Parte 7: Depurando consultas lentas

Las cuatro herramientas que de verdad encuentran la respuesta:

  1. Query Monitor (John Blackbourn). Instálalo en staging, nunca en producción. El panel “Queries” agrupa por componente, así que ves enseguida si el TTFB de 2 segundos venía de tu tema, del core de WooCommerce o de un plugin de terceros que enganchó pre_get_posts. La pestaña de “duplicate queries” caza los bucles N+1 donde una template tag dentro de the_post() vuelve a consultar la base de datos por cada fila.
  2. SAVEQUERIES. Define define( 'SAVEQUERIES', true ); en wp-config.php solo en staging, duplica el uso de memoria. WordPress llena entonces $wpdb->queries con tripletas [ sql, duración, callstack ]. error_log( print_r( $wpdb->queries, true ) ) en shutdown produce la traza canónica de “qué corrió y cuánto tardó”.
  3. $query->request con EXPLAIN. Después de new WP_Query(), echo $query->request; imprime el SQL real generado por WordPress. Pasa esa cadena por EXPLAIN en TablePlus o mysql -e. Busca type: ALL (escaneo completo de tabla), Using filesort o Using temporary, son las filas que necesitan índice.
  4. WP-CLI. En entornos de hosting español como Webempresa con LiteSpeed, Raiola Networks con PHP-FPM o Stackscale dedicado, WP-CLI suele estar disponible por SSH: wp db query --skip-column-names "EXPLAIN $SQL" ejecuta EXPLAIN contra la BD viva sin la sobrecarga del navegador. wp post list --post_type=product --fields=ID --posts_per_page=10 reproduce la lógica de consulta desde un entorno determinista, útil cuando la página lenta queda detrás de un paywall o login.

Heurística para priorizar: cualquier consulta por encima de 50 ms es sospechosa con caché caliente, cualquier cosa sobre 200 ms está rota. El presupuesto total para una página WordPress renderizada en servidor dentro del rango “Bueno” de Core Web Vitals es de unos 600 ms de TTFB; si una sola WP_Query se come 250 ms, te has salido del presupuesto antes de que la plantilla empiece a renderizar. La comunidad WordPress Madrid y WordPress Barcelona suele compartir capturas de Query Monitor exactamente con este patrón en sus meetups mensuales.


#Checklist operativa

Cuando una página va lenta, recorre la lista en orden, los tres primeros puntos explican la mayoría de las lentitudes de WP_Query en sitios WordPress en producción.

  • Lanza Query Monitor sobre la URL lenta. Ordena por tiempo. La consulta más larga es casi siempre la que hay que arreglar.
  • Comprueba si los filtros meta_query apuntan a una meta_key sin indexar. Si es así, añade INDEX (meta_key(32), post_id) sobre wp_postmeta.
  • Añade 'no_found_rows' => true a cada loop secundario que no muestre un total real.
  • Sustituye posts_per_page => -1 por un límite duro o procesa por lotes con paged.
  • Reemplaza query_posts() por pre_get_posts (consulta principal) o new WP_Query (loops secundarios).
  • En loops que solo renderizan título y enlace, usa 'fields' => 'ids' y update_post_meta_cache => false.
  • Después de loops personalizados, llama a wp_reset_postdata() para restaurar el $post global.
  • Si el mismo WP_Query corre en cada carga y los datos cambian poco, envuélvelo en un transient con clave por la firma de los args.

Lectura relacionada: optimización de velocidad WordPress.

Por qué importa esto en 2026 no es abstracto. El informe de Cloudflare 2025 sobre peso de página sitúa el TTFB mediano de e-commerce sobre WordPress en hosting compartido en 1,4 s, frente al umbral “Bueno” de Google de 800 ms. Las correcciones a nivel de consulta de este documento suelen ser la vía más barata para recuperar ese presupuesto, más barata que cambiar a un plan dedicado en Stackscale, más barata que añadir CDN, y mucho más barata que rehacer el sitio en headless.

¿Necesitas ayuda con desarrollo WordPress o mantenimiento WordPress? Contáctanos.

Siguiente paso

Transforma el artículo en una implementación real

Este bloque refuerza el enlazado interno y lleva al lector al siguiente paso más útil dentro de la arquitectura del sitio.

FAQ del artículo

Preguntas Frecuentes

Respuestas prácticas para aplicar el tema en la ejecución real.

SEO-ready GEO-ready AEO-ready 3 Q&A
Cual es la diferencia entre WP_Query, get_posts y query_posts()?
new WP_Query devuelve el objeto completo con found_posts, max_num_pages y soporte para iteracion con the_post(). get_posts es un envoltorio que por defecto fija suppress_filters => true y no_found_rows => true; ideal para listas cortas de barra lateral. query_posts() sobrescribe la consulta global y rompe los condicionales is_*() del resto de la pagina; el core lo desaconseja desde 2010, no se debe usar.
Por que mi WP_Query es lenta aunque solo devuelva diez posts?
Sin 'no_found_rows' => true, WP_Query lanza un SELECT FOUND_ROWS() adicional que recorre todo el conjunto de coincidencias detras del LIMIT. En una tabla wp_posts de 200.000 filas con un JOIN de tax_query, ese segundo paso puede sumar 80 a 250 ms aunque solo se rendericen diez filas. Activa 'no_found_rows' => true salvo que la pagina muestre un total real.
Como arreglo un meta_query que tarda segundos en un catalogo grande?
wp_postmeta solo trae un indice sobre meta_key, demasiado poco selectivo para busquedas por valor. Anade un indice compuesto: ALTER TABLE wp_postmeta ADD INDEX idx_meta_key_post_id (meta_key(32), post_id). Para comparaciones de rango sobre meta_value, anade tambien un indice de prefijo. Si los valores son fijos (tallas, marcas), migra a una taxonomia personalizada: wp_term_relationships ya esta indexada para los JOINs que WordPress genera.

¿Necesitas un FAQ adaptado a tu sector y mercado? Preparamos una versión alineada con tus objetivos de negocio.

Hablemos

Artículos Relacionados

Tu sitio WordPress esta lento? El culpable probablemente es tu base de datos. Aprende a optimizar MariaDB 11, limpiar opciones y gestionar postmeta para rendimiento en 2026.
development

Optimización de base de datos WordPress en 2026: Limpiando el bloat

Tu sitio WordPress esta lento? El culpable probablemente es tu base de datos. Aprende a optimizar MariaDB 11, limpiar opciones y gestionar postmeta para rendimiento en 2026.

Como optimizar Interaction to Next Paint (INP) en sitios WordPress. Correcciones prácticas para la metrica Core Web Vital más nueva que impacta directamente los rankings de Google.
wordpress

Core Web Vitals 2026: La Guia Completa de Optimización INP para WordPress

Como optimizar Interaction to Next Paint (INP) en sitios WordPress. Correcciones prácticas para la metrica Core Web Vital más nueva que impacta directamente los rankings de Google.

Compara los mejores plugins de optimización de imágenes para WordPress, configura la entrega de WebP/AVIF, extrae critical CSS y configura LiteSpeed Cache para puntuaciones máximás en PageSpeed.
wordpress

Optimización de imágenes WordPress y critical CSS: Una guía completa de rendimiento

Compara los mejores plugins de optimización de imágenes para WordPress, configura la entrega de WebP/AVIF, extrae critical CSS y configura LiteSpeed Cache para puntuaciones máximás en PageSpeed.