Atualizado em 19/06/2026
Toda página do WordPress — a home, uma categoria, a busca, um post — é, no fundo, o resultado de uma consulta ao banco encapsulada pela classe WP_Query. Entender essa classe e o laço que percorre os resultados (a Loop) é o que separa quem "mexe no tema" de quem realmente programa para WordPress. Neste artigo vamos da Loop básica até meta_query, tax_query, modificação da consulta principal com pre_get_posts e os cuidados de performance que evitam derrubar o site.
O que é a Loop
A Loop (laço) é o trecho de código que percorre os posts retornados por uma consulta e os exibe. Ela se apoia em duas funções:
have_posts()— devolvetrueenquanto ainda houver posts a processar e avança o ponteiro interno.the_post()— define o post atual como o post "global" ($post), de modo que template tags comothe_title(),the_content()ethe_permalink()passem a se referir a ele.
Na consulta principal de um tema, a Loop é simplesmente:
<?php
if ( have_posts() ) :
while ( have_posts() ) :
the_post();
the_title( '<h2>', '</h2>' );
the_excerpt();
endwhile;
else :
echo '<p>Nenhum conteúdo encontrado.</p>';
endif;
Repare que aqui não criamos nenhum objeto: have_posts() e the_post() operam sobre o $wp_query global, a consulta que o WordPress montou para a URL atual.
WP_Query: criando uma consulta secundária
Quando você precisa de uma lista além do conteúdo principal — os 5 posts mais recentes em uma sidebar, produtos em destaque, posts relacionados — instancia a sua própria WP_Query. O fluxo é sempre o mesmo: montar os argumentos, percorrer com a Loop e, ao final, resetar o post global.
<?php
$args = array(
'post_type' => 'post',
'posts_per_page' => 5,
'orderby' => 'date',
'order' => 'DESC',
'no_found_rows' => true, // não há paginação aqui
);
$consulta = new WP_Query( $args );
if ( $consulta->have_posts() ) :
echo '<ul class="recentes">';
while ( $consulta->have_posts() ) :
$consulta->the_post();
printf(
'<li><a href="%s">%s</a></li>',
esc_url( get_permalink() ),
esc_html( get_the_title() )
);
endwhile;
echo '</ul>';
endif;
wp_reset_postdata(); // OBRIGATÓRIO após uma WP_Query secundária
Esqueceu o wp_reset_postdata()? Como the_post() sobrescreve o $post global, todo o resto da página (comentários, template tags, outros blocos) passa a enxergar o último post da sua consulta, e não o post da página. É a causa nº 1 de "o título do post está errado depois da minha lista".
Principais argumentos
A WP_Query aceita dezenas de argumentos. Estes são os que você mais vai usar no dia a dia:
| Argumento | Para que serve |
|---|---|
post_type | Tipo de conteúdo: 'post', 'page', um CPT, ou um array de vários. |
posts_per_page | Quantos posts por página. Use um teto; evite -1 (todos). |
paged | Página atual da paginação. Normalmente get_query_var( 'paged' ). |
orderby | Critério de ordenação: date, title, menu_order, meta_value_num, rand... |
order | Direção: ASC ou DESC. |
post_status | Status: publish (padrão no front), draft, pending... |
meta_query | Filtra por campos personalizados (post meta). Veja abaixo. |
tax_query | Filtra por termos de taxonomia (categorias, tags, taxonomias próprias). |
no_found_rows | true pula a contagem total de posts (mais rápido sem paginação). |
fields | 'ids' retorna só os IDs — útil e leve quando você não precisa do objeto inteiro. |
meta_query e tax_query
Os dois argumentos mais poderosos da WP_Query permitem filtrar por campos personalizados (meta_query) e por termos de taxonomia (tax_query). Ambos aceitam uma chave relation (AND ou OR) e podem ser combinados. O exemplo abaixo busca os imovel à venda, com preço de até 500 mil, na categoria/taxonomia "apartamento":
<?php
$args = array(
'post_type' => 'imovel',
'posts_per_page' => 12,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'situacao',
'value' => 'venda',
),
array(
'key' => 'preco',
'value' => 500000,
'type' => 'NUMERIC',
'compare' => '<=',
),
),
'tax_query' => array(
array(
'taxonomy' => 'tipo_imovel',
'field' => 'slug',
'terms' => 'apartamento',
),
),
// ordena pelo valor numérico do meta 'preco'
'meta_key' => 'preco',
'orderby' => 'meta_value_num',
'order' => 'ASC',
);
$imoveis = new WP_Query( $args );
// ... Loop com $imoveis->have_posts() ...
wp_reset_postdata();
Em meta_query, sempre informe 'type' => 'NUMERIC' ao comparar números: por padrão o valor é tratado como string e '9' > '50' sai verdadeiro. E lembre-se de que meta_query gera JOINs na tabela postmeta — muitas condições deixam a consulta cara. Para campos que você filtra com frequência, considere uma taxonomia (indexada) em vez de post meta.
Modificando a consulta PRINCIPAL: pre_get_posts
E quando você quer mudar a própria consulta da página — por exemplo, exibir 12 posts por página no arquivo, ou incluir um CPT na home? A resposta correta é o hook pre_get_posts, que recebe o objeto WP_Query antes de a consulta ir ao banco. Você ajusta os argumentos via $query->set() — sem refazer nada, sem quebrar a paginação:
<?php
add_action( 'pre_get_posts', 'meu_ajuste_consulta_principal' );
function meu_ajuste_consulta_principal( $query ) {
// Nunca toque nas consultas do admin nem nas secundárias
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
// Só no arquivo de categoria
if ( $query->is_category() ) {
$query->set( 'posts_per_page', 12 );
$query->set( 'orderby', 'title' );
$query->set( 'order', 'ASC' );
}
// Inclui o CPT 'livro' nos resultados da home, junto com posts
if ( $query->is_home() ) {
$query->set( 'post_type', array( 'post', 'livro' ) );
}
}
Nunca use query_posts(). Ela substitui o $wp_query global, refaz a consulta do zero (duas queries em vez de uma) e quebra a paginação e o objeto da página. Não há como revertê-la com segurança. Para alterar a consulta principal use pre_get_posts; para listas adicionais, new WP_Query() + wp_reset_postdata().
As duas verificações — is_admin() e is_main_query() — são essenciais. Sem elas, o hook também alteraria as consultas do painel administrativo e qualquer WP_Query secundária do site, gerando bugs difíceis de rastrear.
Paginação correta em consultas secundárias
Se a sua WP_Query precisa paginar (uma página de template com listagem própria), passe o paged explicitamente e use paginate_links():
<?php
$paged = max( 1, get_query_var( 'paged' ), get_query_var( 'page' ) );
$q = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => $paged,
) );
// ... Loop ...
echo paginate_links( array(
'total' => $q->max_num_pages,
'current' => $paged,
) );
wp_reset_postdata();
Performance: peça só o que vai usar
Consultas mal calibradas são uma das maiores causas de lentidão. Alguns princípios:
- Evite
posts_per_page => -1. Em sites grandes ele carrega tudo na memória e pode estourar o limite do PHP. Defina um teto realista. 'no_found_rows' => truequando não há paginação: o WordPress deixa de rodar oSQL_CALC_FOUND_ROWS, que é caro.'update_post_meta_cache' => falsee'update_post_term_cache' => falsequando você não vai ler post meta nem termos dentro do loop — evita consultas extras de pré-carregamento.'fields' => 'ids'quando precisa só dos IDs.- Para consultas realmente caras e repetidas, guarde o resultado em transients ou no cache de objeto.
<?php
// Consulta enxuta: só IDs, sem contagem total, sem caches desnecessários
$ids = get_posts( array(
'post_type' => 'produto',
'posts_per_page' => 50,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
) );
// $ids agora é um array de IDs, pronto para usar
get_posts() é um wrapper sobre a WP_Query que já retorna o array de posts e ativa no_found_rows por padrão (pula a contagem total) — ótimo para listas simples. Por baixo, é a mesma classe. E como ele não usa the_post(), não exige wp_reset_postdata() (a menos que você chame setup_postdata() manualmente).
Consultas bem feitas, hooks no lugar certo e cache são o tripé da performance no WordPress. Antes de subir qualquer consulta para produção, valide também a segurança: dados vindos da URL (como $_GET) precisam ser sanitizados antes de entrar nos argumentos da WP_Query.
Perguntas frequentes
Qual a diferença entre a Loop principal e uma WP_Query secundária?
A Loop principal é a consulta que o WordPress executa automaticamente para cada URL, com base no objeto global $wp_query — é ela que decide quais posts a página exibe. Uma WP_Query secundária é uma instância criada por você (new WP_Query()) para buscar conteúdo adicional, como uma lista de "posts relacionados". Para alterar a principal, use o hook pre_get_posts; para listas extras, crie uma WP_Query e sempre chame wp_reset_postdata() depois.
Por que não devo usar query_posts()?
Porque query_posts() substitui a consulta principal e refaz a query do zero, quebrando a paginação, o objeto da página e a performance. Não dá para reverter de forma confiável. Para modificar a consulta principal, o caminho correto é o hook pre_get_posts. Para consultas adicionais, use new WP_Query() seguido de wp_reset_postdata(). Considere query_posts() proibido em código de produção.
Quando preciso chamar wp_reset_postdata()?
Sempre que você roda uma WP_Query secundária e usa the_post() (ou template tags como the_title()) dentro dela. O the_post() sobrescreve o post global $post; sem o reset, o restante da página passa a enxergar o último post da sua consulta, não o post correto. Chame wp_reset_postdata() logo após o endwhile.
posts_per_page => -1 é perigoso?
Sim, em escala. -1 traz todos os posts que casam com a query, sem limite. Em um site com milhares de registros isso consome memória, sobrecarrega o banco e pode estourar o limite de memória do PHP. Defina sempre um teto realista (posts_per_page => 50, por exemplo) e pagine quando precisar de mais.
Como deixar uma WP_Query mais rápida?
Peça só o que vai usar. Se não há paginação, defina 'no_found_rows' => true para o WordPress pular a contagem total (o SQL_CALC_FOUND_ROWS). Se não vai ler meta nem termos no loop, use 'update_post_meta_cache' => false e 'update_post_term_cache' => false. Evite posts_per_page => -1 e combine com cache de objeto/transients para consultas caras.
Posso usar WP_Query no front-end e no admin?
Sim. A classe funciona em qualquer contexto. No hook pre_get_posts, porém, tenha cuidado: ele dispara para todas as consultas, inclusive as do painel. Sempre proteja a alteração com if ( is_admin() ) return; e $query->is_main_query() para não bagunçar listas do wp-admin nem consultas secundárias.