Todo o programador WordPress sabe executar um loop simples. O problema aparece quando o pedido mistura taxonomias, metadados e exclusões ao mesmo tempo: produtos na coleção de verão, com a etiqueta vermelho, sem o estado esgotado. Nesse ponto, cat=5 e query_posts() deixam de ser ferramentas - passam a ser dívida técnica.
Este guia trata WP_Query como API de engenharia: tax_query, meta_query com EXISTS, relações AND/OR aninhadas, no_found_rows, fields => ids, leitura de Query Monitor, object cache e armadilhas de indexação MySQL/MariaDB. A referência canónica continua a ser a classe WP_Query no Developer Handbook.
Quando WP_Query deixa de ser um loop simples
Um loop em arquivo ou single template raramente precisa de flags de performance. O custo aparece em:
- widgets e blocos que correm em cada página
- shortcodes dentro de conteúdo
- endpoints REST ou AJAX que devolvem listas filtradas
- sliders e carrosséis na homepage
- “posts relacionados” no rodapé de cada artigo
Nesses contextos, cada new WP_Query() pode gerar SQL_CALC_FOUND_ROWS, carregar meta e termos em massa e competir com a query principal pelo object cache. O objetivo não é “escrever consultas bonitas”. É reduzir JOINs, evitar contagens desnecessárias e só hidratar o que o template usa.
Antes de otimizar, confirme se precisa mesmo de um segundo WP_Query. Muitas vezes get_posts(), get_terms() ou uma query já existente no request resolvem o mesmo problema com menos superfície.
Anatomia da tax_query
cat, tag_id e category__and ainda funcionam, mas são atalhos. A forma estável e documentada é tax_query, descrita nos parâmetros de taxonomia e na classe WP_Tax_Query.
Cada cláusula tipicamente inclui:
taxonomy- slug registado (category,post_tag, ou CPT taxonomy)field-term_id,name,slugouterm_taxonomy_idterms- valor único ou arrayoperator-IN(omissão),NOT IN,AND,EXISTS,NOT EXISTSinclude_children- relevante em taxonomias hierárquicas
Relação AND entre taxonomias
Quer filmes do género ação e do ano 2026. Ambas as condições têm de ser verdadeiras:
$args = [
'post_type' => 'movie',
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'genre',
'field' => 'slug',
'terms' => 'action',
],
[
'taxonomy' => 'year',
'field' => 'slug',
'terms' => '2026',
],
],
];
$query = new WP_Query( $args );O WordPress traduz isto em JOINs sobre wp_term_relationships e wp_term_taxonomy. Duas cláusulas AND sobre taxonomias distintas significam, na prática, interseção de conjuntos de posts.
Relação OR e exclusões
Quer produtos com etiqueta em promoção ou com etiqueta liquidação:
$args = [
'post_type' => 'product',
'tax_query' => [
'relation' => 'OR',
[
'taxonomy' => 'product_label',
'field' => 'slug',
'terms' => 'on-sale',
],
[
'taxonomy' => 'product_label',
'field' => 'slug',
'terms' => 'clearance',
],
],
];Para excluir um termo, use operator => 'NOT IN'. Misturar OR de inclusão com NOT IN no mesmo nível sem aninhamento é a forma mais rápida de obter SQL errado. Quando a lógica for “(A ou B) e não C”, aninhe arrays:
'tax_query' => [
'relation' => 'AND',
[
'relation' => 'OR',
[
'taxonomy' => 'product_label',
'field' => 'slug',
'terms' => [ 'on-sale', 'clearance' ],
],
],
[
'taxonomy' => 'product_status',
'field' => 'slug',
'terms' => 'sold-out',
'operator' => 'NOT IN',
],
],NOT IN, EXISTS e filhos de categorias
Em taxonomias hierárquicas, include_children (true por omissão em category) pode expandir a consulta a todos os termos descendentes. Em catálogos grandes isso multiplica JOINs. Se só quiser o termo exacto, force 'include_children' => false.
EXISTS / NOT EXISTS em tax_query respondem a “tem qualquer termo nesta taxonomia?”, sem listar IDs. Útil para limpar CPT sem classificação ou para auditorias de conteúdo.
Prefira field => 'term_id' quando os IDs já estão em memória (por exemplo após get_the_terms()). Slugs forçam lookups extra e quebram se o slug mudar.
Meta_query com EXISTS e tipos de comparação
A filtragem por post meta está documentada nos parâmetros de custom field e na classe WP_Meta_Query. Cada cláusula junta wp_postmeta à query principal.
Comparações típicas
'meta_query' => [
[
'key' => '_stock_status',
'value' => 'instock',
'compare' => '=',
],
[
'key' => '_weight',
'value' => 5,
'compare' => '>=',
'type' => 'NUMERIC',
],
],Sem type => 'NUMERIC', o MySQL compara strings. "10" fica antes de "2" em ordem lexicográfica. O mesmo problema aparece com datas: use DATE, DATETIME ou CHAR de forma consciente, alinhada ao formato guardado.
Operadores úteis: =, !=, >, >=, <, <=, LIKE, NOT LIKE, IN, NOT IN, BETWEEN, NOT BETWEEN, EXISTS, NOT EXISTS.
EXISTS sem valor
Muitos campos ACF ou meta de plugins existem só quando o editor os preenche. Para listar posts que têm a chave, independentemente do valor:
'meta_query' => [
[
'key' => '_featured_until',
'compare' => 'EXISTS',
],
],NOT EXISTS é o inverso. Não passe value nestes casos - a documentação de WP_Meta_Query deixa claro que o valor é ignorado com EXISTS/NOT EXISTS.
Relação AND/OR em meta_query
Tal como em tax_query, o default é AND. Pode aninhar grupos:
'meta_query' => [
'relation' => 'AND',
[
'key' => '_sku',
'compare' => 'EXISTS',
],
[
'relation' => 'OR',
[
'key' => '_backorders',
'value' => 'yes',
'compare' => '=',
],
[
'key' => '_stock',
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
],
],
],Cada cláusula adicional é tipicamente mais um JOIN a wp_postmeta. Três cláusulas AND sobre chaves diferentes são três JOINs. Em tabelas com milhões de linhas de meta, isso dói mais do que a tax_query equivalente.
Misturar tax_query e meta_query na mesma consulta
Cenários reais combinam classificação e estado. Exemplo: produtos numa categoria e com stock meta definido:
$args = [
'post_type' => 'product',
'tax_query' => [
[
'taxonomy' => 'product_cat',
'field' => 'slug',
'terms' => 't-shirts',
'include_children' => false,
],
],
'meta_query' => [
[
'key' => '_stock_status',
'value' => 'instock',
'compare' => '=',
],
[
'key' => '_manage_stock',
'compare' => 'EXISTS',
],
],
];Não existe um relation global entre tax_query e meta_query: o WordPress aplica ambas (AND implícito ao nível da query). Se precisar de “(tax A) OU (meta B)”, terá de fazer duas queries e unir IDs em PHP, ou escrever SQL customizado com posts_clauses. Para a maioria dos filtros de catálogo, o AND entre os dois blocos é o comportamento desejado.
Ordem de avaliação mental: primeiro reduza o conjunto com a taxonomia mais selectiva, depois aplique meta. Em SQL o otimizador decide, mas em desenho de API ajuda a não empilhar cláusulas meta redundantes.
Posts relacionados sem plugins
Um padrão frequente: três artigos da mesma categoria, excluindo o atual, sem paginação.
function wppoland_get_related_posts( int $current_id = 0 ): WP_Query {
$current_id = $current_id ?: (int) get_the_ID();
$terms = get_the_terms( $current_id, 'category' );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return new WP_Query( [ 'post__in' => [ 0 ] ] );
}
$term_ids = wp_list_pluck( $terms, 'term_id' );
return new WP_Query(
[
'category__in' => $term_ids,
'post__not_in' => [ $current_id ],
'posts_per_page' => 3,
'orderby' => 'date',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'ignore_sticky_posts' => true,
]
);
}Evite orderby => rand em tráfego alto: força scans caros e impede caches de query estáveis. Prefira data, menu_order ou um meta de score pré-calculado. Se precisar de aleatoriedade, faça-o sobre um conjunto pequeno de IDs já filtrados.
Performance: no_found_rows, fields ids e caches
A query padrão conta linhas para paginação (found_posts, max_num_pages) via mecanismo equivalente a SQL_CALC_FOUND_ROWS / contagem separada, consoante a versão. Se o template não mostra paginação, desligue a contagem.
Checklist de argumentos leves
$args = [
'posts_per_page' => 5,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'ignore_sticky_posts' => true,
];no_found_rows => true- não calcule o total de matchesupdate_post_meta_cache => false- não pré-carregue meta para os posts do result setupdate_post_term_cache => false- não pré-carregue termosfields => 'ids'- devolva só IDs; o loop não hidrata objetosWP_Postcompletos
Quando usar fields => ids
Use fields => 'ids' quando só precisa de:
- verificar se existe pelo menos um resultado (
! empty( $query->posts )) - passar IDs a outra API (
update_post_meta_cache()seletivo, REST, transient) - construir um segundo passo mais barato
$ids = get_posts(
[
'post_type' => 'product',
'fields' => 'ids',
'posts_per_page' => 50,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'tax_query' => [ /* ... */ ],
]
);get_posts() já define no_found_rows como true por omissão. WP_Query directo não. Essa diferença explica muitos “porquê é que o meu loop na sidebar é lento?”.
Não desligue update_post_meta_cache se o template chama get_post_meta() por post no loop - aí o WordPress fará N queries em vez de uma. O mesmo para get_the_terms() e update_post_term_cache.
Query Monitor na prática
Query Monitor mostra o SQL real, o tempo, o caller e o número de queries duplicadas. Fluxo útil:
- Abra a página problematica com QM activo e perfil de administrador.
- No painel Queries, filtre por
WP_Query/get_posts. - Procure
SQL_CALC_FOUND_ROWSouSELECT FOUND_ROWS()quando não há paginação. - Conte JOINs a
wp_postmetaewp_term_relationships. - Verifique se a mesma query corre várias vezes no mesmo request (shortcode sem static cache).
Se o SQL parecer correcto mas lento, olhe para EXPLAIN (QM ou cliente MySQL): type=ALL em wp_postmeta com filtro por meta_key + meta_value e ORDER BY meta_value é o padrão clássico de catálogo mal indexado.
Guarde um transient com o SQL e o tempo antes/depois de no_found_rows e fields => ids. Sem medição, “otimização” vira opinião.
Object cache e o custo de hidratar posts
Com Redis ou Memcached atrás de um drop-in de object cache, o WordPress guarda objectos de post, meta e termos. Isso ajuda pages quentes - e mascara queries más em staging sem cache.
Implicações práticas:
- Um
WP_Queryque devolve 100 posts ainda pode ser barato na segunda visita se tudo estiver no object cache, e caro na primeira (cache miss em massa). fields => idsreduz o trabalho de hidratação e o tamanho das entradas de cache de post.- Transients de listas (IDs filtrados) são mais estáveis do que cachear HTML completo de widgets quando o filtro depende de query vars.
- Invalide com cuidado: ao gravar um produto, limpe apenas as chaves do filtro afectado, não todo o grupo
query.
Object cache não substitui índices. Só reduz a frequência com que o SQL mau corre.
Armadilhas de indexação em MySQL e MariaDB
A tabela wp_postmeta tem índice em (meta_key(191), post_id) (dimensão exacta depende da versão e do schema). Filtrar por meta_value sozinho raramente usa índice de forma útil. Ordenar por meta numérico (ORDER BY meta_value+0) impede uso eficiente de índice.
Padrões que escalam mal:
- muitas cláusulas
meta_queryAND com chaves diferentes meta_query+tax_query+orderby => meta_value_numna mesma queryposts_per_page => -1em arquivos filtráveisLIKE '%termo%'emmeta_value
Mitigações de engenharia (não de “plugin milagroso”):
- Materializar filtros quentes em taxonomias ou colunas próprias (Custom Table, feature flag em
post_statuscustom, tabela de índice). - Pré-calcular “featured” / “in stock” como termo ou post type separado.
- Paginar sempre; nunca
-1em admin-ajax público. - Para ordenação por meta, considere um índice composto customizado só se medir o ganho - e documente a migração.
Em WooCommerce e catálogos grandes, a equipa Core e o ecossistema moveram parte desta lógica para tabelas de lookup precisamente porque meta_query não escala como motor de facetas.
Erros frequentes que ainda vemos em 2026
- Usar
query_posts()e destruir a query principal do template. - Esquecer
wp_reset_postdata()após um loop secundário que chamathe_post(). - Confiar em
orderby => randem home pages. - Passar slugs hard-coded sem validar se o termo existe (
term_exists()). - Misturar
relation => ORno topo com cláusulas que deveriam ser AND, sem aninhamento. - Desligar caches de meta/termos e depois chamá-los no loop.
- Contar
found_postscomno_found_rows => truee ficar surpreendido com zero.
Checklist rápido antes de merge:
- Esta query precisa de paginação? Se não,
no_found_rows. - O template usa meta/termos? Se não, desligue os update caches.
- Bastam IDs?
fields => ids. - Quantos JOINs o QM mostra? Se forem mais de três a
postmeta, redesenhe. - Há object cache em produção? Meça cold e warm cache.
Resumo operacional
- Prefira
tax_queryemeta_querydocumentados em vez de argumentos legados de string. - Aninhe
relationAND/OR quando a lógica for composta; não invente um único nível “mágico”. - Use
EXISTSpara presença de meta ou termos sem comparar valores fantasma. - Em loops secundários, combine
no_found_rows, flags de cache e, quando fizer sentido,fields => ids. - Confirme com Query Monitor e
EXPLAIN; object cache e índices são camadas diferentes. - Se a faceta de catálogo crescer, saia de
postmetapuro - a APIWP_Querynão deixa de ser a ferramenta certa para muitos casos, mas deixa de ser a única.
A base de dados e o TTFB agradecem consultas que pedem só o necessário. O Developer Handbook de WP_Query, WP_Tax_Query e WP_Meta_Query continua a ser a fonte de verdade para parâmetros e operadores.







