En enkel WP_Query med category_name fungerer til demoen er over. Når katalogen skal vise produkttyper, regioner og lagerstatus samtidig, eller når en nyhetsportal i Oslo skal krysse emne, forfatterrolle og publiseringsstatus, trenger du tax_query, meta_query og eksplisitte ytelsesflagg. Denne guiden er skrevet for PHP-utviklere som eier SQL-en bak WordPress 6.x, ikke for klikk-og-dra-bygging.
Samme historie går igjen: filteret fungerte i staging med tusen poster, men ble tregt i produksjon med ti ganger så mange rader i wp_postmeta. Forskjellen er sjelden «treg hosting». Det er nestede JOINs, unødvendig FOUND_ROWS, og meta-nøkler som aldri ble designet for hot path.
Målet er konkret: bygge nestede taksonomi-klausuler, bruke EXISTS i meta-laget, velge relation AND/OR bevisst, slå av unødvendig FOUND_ROWS, hente kun ID-er når det holder, måle med Query Monitor, forstå object cache, og unngå indekseringsskandaler i wp_postmeta.
Hvordan er tax_query bygget opp i WP_Query
Parametere som cat, tag_id og category__and finnes fortsatt. De er snarveier. De skalerer dårlig når logikken blir nestet. tax_query er det stabile API-et: hver klausul er en array med taxonomy, field, terms, og valgfritt operator og include_children.
field bør være term_id når du allerede har ID-er fra get_the_terms(). Bruk slug når filteret kommer fra URL eller et skjema. name er tregere og mer skjørt ved oversettelser.
$args = [
'post_type' => 'arrangement',
'posts_per_page' => 12,
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'by',
'field' => 'slug',
'terms' => [ 'oslo', 'bergen' ],
'operator' => 'IN',
'include_children' => false,
],
[
'taxonomy' => 'tema',
'field' => 'slug',
'terms' => 'wordpress',
],
],
];
$query = new WP_Query( $args );include_children => false er viktig på hierarkiske taksonomier. Standardverdien er true, og da kan en forespørsel mot «Norge» plutselig trekke inn alle undertermene uten at du ser det i koden.
Nestet relation AND/OR
Et typisk katalogfilter ser slik ut: (kategori A ELLER kategori B) OG (farge C) OG IKKE status trukket tilbake. I tax_query løses det med en nestet array som har sin egen relation:
$args = [
'post_type' => 'produkt',
'tax_query' => [
'relation' => 'AND',
[
'relation' => 'OR',
[
'taxonomy' => 'produktkategori',
'field' => 'slug',
'terms' => 'jakker',
],
[
'taxonomy' => 'produktkategori',
'field' => 'slug',
'terms' => 'vester',
],
],
[
'taxonomy' => 'farge',
'field' => 'slug',
'terms' => 'navy',
],
[
'taxonomy' => 'lagerstatus',
'field' => 'slug',
'terms' => 'utgatt',
'operator' => 'NOT IN',
],
],
];Hold nestingen grund. Tre nivåer er lesbart. Fem nivåer blir ubugbar SQL. Når filteret vokser forbi det, er det ofte et tegn på at domenemodellen bør flyttes ut av rene termer og inn i egne tabeller eller et søkeindeks-lag.
meta_query: EXISTS, type og sammenligninger
meta_query filtrerer mot wp_postmeta. Hver klausul har key, valgfritt value, compare og type. type (NUMERIC, DECIMAL, DATE, CHAR, BINARY) styrer CAST i SQL. Uten NUMERIC blir sammenligning av «10» og «2» leksikografisk: '2' > '10' som CHAR.
EXISTS og NOT EXISTS
Du trenger ofte ikke verdien. Du trenger å vite om nøkkelen finnes. compare => 'EXISTS' (og NOT EXISTS) er laget for det. Det er vanlig på flagg som _featured, _sync_locked eller _legacy_import der verdien er irrelevant.
$args = [
'post_type' => 'post',
'meta_query' => [
'relation' => 'AND',
[
'key' => '_featured',
'compare' => 'EXISTS',
],
[
'key' => '_draft_lock',
'compare' => 'NOT EXISTS',
],
],
];Ikke bland EXISTS med en value. I kildekoden til WP_Meta_Query tolkes EXISTS med en verdi som =, mens verdien ignoreres for NOT EXISTS. En value ved siden av EXISTS gjør altså klausulen til en vanlig likhetssjekk, ikke en eksistenssjekk.
Named clauses og orderby
Klausuler kan ha string-nøkler i stedet for numeriske indekser. Da kan orderby peke på klausulnavnet. Det er det offisielle mønsteret i WP_Query-dokumentasjonen, ikke et hack.
$args = [
'post_type' => 'event',
'meta_query' => [
'relation' => 'AND',
'event_start' => [
'key' => 'start_date',
'value' => gmdate( 'Y-m-d' ),
'compare' => '>=',
'type' => 'DATE',
],
'capacity_left' => [
'key' => 'seats_left',
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
],
],
'orderby' => [
'event_start' => 'ASC',
],
];Relation AND/OR i meta_query
Syntaksen speiler tax_query. Du kan neste grupper. Standard relation er AND. Sett OR eksplisitt når én av flere nøkler skal treffe. Blanding av mange OR-klausuler mot ulike meta-nøkler er en klassisk fullskann-felle på store installasjoner.
Hvordan sette sammen tax_query og meta_query
Begge arrayene kan leve i samme $args. WordPress AND-er dem på toppnivå: resultatet må tilfredsstille taksonomi-delen og meta-delen. Det er nesten alltid det du vil i et filterpanel.
$args = [
'post_type' => 'produkt',
'posts_per_page' => 24,
'tax_query' => [
[
'taxonomy' => 'produktkategori',
'field' => 'slug',
'terms' => 'skiutstyr',
],
],
'meta_query' => [
'relation' => 'AND',
[
'key' => 'lager_antall',
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
],
[
'key' => 'sesong',
'value' => 'vinter',
'compare' => '=',
],
],
];I praksis ser vi ofte at laget over WooCommerce eller et eget CPT-lag bygger disse arrayene dynamisk fra $_GET. Valider da slugger og cast tall eksplisitt før de lander i meta_query. Rå brukerinput rett inn i value er både en sikkerhets- og ytelsesrisiko.
Hvordan bruke no_found_rows og fields ids i WP_Query
Standard WP_Query teller alle treff for paginering. Når spørringen har LIMIT, legger core til SQL_CALC_FOUND_ROWS og kjører deretter SELECT FOUND_ROWS(). På store tabeller koster det. Når widgeten, related-blokken eller API-endepunktet ikke trenger totaltall, sett:
$args = [
'posts_per_page' => 5,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'fields' => 'ids',
];no_found_rows => truehopper over kostbar telling. MedSQL_CALC_FOUND_ROWSmå MySQL gå gjennom alle radene som treffer, selv når den bare returnerer fem; uten kan den stoppe vedLIMITnår rekkefølgen kommer fra en indeks. MySQL har merketSQL_CALC_FOUND_ROWSogFOUND_ROWS()som foreldet siden 8.0.17 (MySQL-manualen).fields => 'ids'returnerer bare post-ID-er. Bruk det når du skal matepost__in, bygge transient-nøkler, eller sjekke eksistens.update_post_meta_cache/update_post_term_cachesomfalseunngår ekstra bulk-SELECTs når malen ikke leser termer eller meta i løkken.
orderby => rand frister i «relaterte innlegg». På store tabeller blir det ORDER BY RAND() som sorterer hele resultatet i minnet. Bedre mønster: hent en pool med fields => ids og no_found_rows, trekk deretter med array_rand i PHP, eller cache det trekte settet noen minutter i object cache / transient.
Hvordan slå av filtre med suppress_filters i WP_Query
Når tredjeparts-plugins hekter seg på posts_where, posts_join eller posts_clauses, arver sekundære spørringer betingelser som var ment for hovedløkken: språkfiltre, tilgangssjekker eller ekstra JOINs.
I bakgrunnsjobber, CLI-skript og små isolerte spørringer kan du skjerme WP_Query med 'suppress_filters' => true:
$rene_args = [
'post_type' => 'product',
'posts_per_page' => 10,
'no_found_rows' => true,
'suppress_filters' => true,
'fields' => 'ids',
];
$ren_query = new WP_Query( $rene_args );get_posts() setter 'suppress_filters' => true som standard, mens new WP_Query() bruker false. Det forklarer hvorfor like argumenter kan gi ulike resultater avhengig av hvem som kaller. suppress_filters slår av posts_*-filtrene (posts_where, posts_join, posts_clauses og resten), men ikke handlingen pre_get_posts, som core alltid kjører. Hvis et flerspråklig plugin avgrenser språket gjennom de filtrene, returnerer spørringen innlegg på alle språk. Sjekk hvordan pluginet ditt filtrerer før du slår det på.
Hvordan bruke Query Monitor på WP_Query-spørringer
Gjetning om ytelse er bortkastet tid. Query Monitor viser SQL, duplikate spørringer, komponenten som trigget dem, og tidsbruk. En typisk arbeidsflyt:
- Aktiver QM på staging med samme datavolum som produksjon (eller en anonymisert dump).
- Last siden som bruker filteret.
- Finn
WP_Query-raden: se JOIN-antall motwp_term_relationshipsogwp_postmeta. - Sammenlign med variante
no_found_rows+fields => ids. - Se etter N+1: løkker som kaller
get_post_meta()per rad uten at meta-cache ble varmet.
Hvis samme meta_query dukker opp tre ganger fra tre widgets, er problemet arkitektur, ikke MySQL-versjonen. Samle resultatet én gang i requesten (wp_cache_get / wp_cache_set med en stabil nøkkel) og del det.
Lag også en vane av å eksportere den genererte SQL-en fra Query Monitor og kjøre EXPLAIN i klienten din. Se etter Using temporary og Using filesort på filtre som kjører på hver listevisning. Det er der du avgjør om løsningen er et bedre type-cast, færre OR-klausuler, eller en egen indeksert tabell.
Hva gjør object cache for WP_Query
Object cache (Redis, Memcached via drop-in) cacher objekter WordPress allerede har hentet: poster, termer, meta-grupper. Den gjør en god spørring billigere neste gang. Den gjør ikke en dårlig spørring billig første gang.
Praktiske regler:
- Cache ID-lister for stabile filtre (f.eks. «forside-utvalgte») med kort TTL, og invalider ved
save_post/edited_term. - Ikke cache hele HTML for personlige eller handlekurv-avhengige blokker.
- Når
fields => idsreturnerer et sett, kan du hydrate medget_posts( [ 'post__in' => $ids, 'orderby' => 'post__in' ] )i en separat, enklere query som treffer object cache hardere.
Uten object cache på et høyt trafikkert filterpanel vil hver unik kombinasjon av tax/meta-parametre treffe databasen. Med cache lander de vanligste kombinasjonene i minnet. Filteret må fortsatt være billig å beregne første gang.
Hvordan cacher WP_Query resultater siden WordPress 6.1
Siden WordPress 6.1 har WP_Query sin egen resultatcache. Etter SELECT lagrer core listen over ID-er sammen med found_posts og max_num_pages i object cache-gruppen post-queries. Nøkkelen er en hash av argumentene og den genererte SQL-en (WP_Query::generate_cache_key(), merket @since 6.1.0 i class-wp-query.php). Argumentet cache_results slår det av og på; standard er true.
Tre detaljer fra kildekoden avgjør om denne cachen hjelper deg:
- Invalideringen er global. Nøkkelen saltes med
last_changedfor gruppenposts, og ogsåtermsnår spørringen har entax_query. Å lagre et hvilket som helst innlegg, eller legge til, endre eller slette en hvilken som helst post meta-verdi, flytterlast_changed(core koblerwp_cache_set_posts_last_changed()tilupdated_post_metaog de andre meta-handlingene idefault-filters.php). I en nettbutikk der en import oppdaterer lager med få minutters mellomrom, blir treffraten lav. - Noen spørringer caches aldri. En spørring der
orderbyinneholderRAND(hopper over cachen, og det samme gjør en spørring der filtreneposts_fieldsellerposts_clauseshar endret feltene som velges. - Uten persistent object cache (Redis, Memcached) holder standard
WP_Object_Cachedata bare for gjeldende request. Resultatcachen hjelper da bare når samme spørring kjøres to ganger i én sidelasting.
Det er enda en grunn til å hente en pool med ID-er og stokke den i PHP i stedet for orderby => rand: pool-spørringen kan serveres fra post-queries, den tilfeldige kan ikke. Slår du av cache_results i en eksport eller et CLI-skript, fylles heller ikke post-cachen med de returnerte objektene.
Indekseringsfeller i wp_postmeta
wp_postmeta har separate indekser på post_id og meta_key(191), i tillegg til primærnøkkelen meta_id. Den har ikke en generisk, effektiv indeks for «alle rader der meta_value er mellom X og Y på tvers av nøkler». Konsekvenser:
- LIKE på
meta_value- full tabellskann på store butikker. Unngå i hot path. - Mange OR-klausuler på ulike nøkler - flere JOINs eller unions som MySQL håndterer dårlig.
- Tom
keyimeta_query- historisk mulighet for verdissammenligning uten nøkkel, men det skanner alt. Ikke bruk det. - Lagring av serialiserte arrays som filterverdier - du kan ikke indeksere eller sammenligne dem meningsfullt. Lag atomiske nøkler (
_region,_sku,_available) i stedet.
Hvis et meta-filter er forretningskritisk (tilgjengelighet, region, SKU) og volumet er stort, vurder en egen tabell med riktige indekser og en tynn posts_clauses / posts_join-hook. La core meta_query håndtere sjeldne admin-filtre, ikke butikkens hovedsti.
På nordiske WooCommerce-installasjoner ser vi ofte at lagerstatus og leveringsregion ender som meta fordi det var raskest i ACF dag én. Det fungerer til katalogen passerer noen titusen produkter. Deretter blir Query Monitor rød under kampanjehelger, og flyttingen til egen tabell eller et søkeprodukt (OpenSearch, Algolia, ElasticPress) blir tvungen.
Et praktisk mellomsteg før full migrasjon: speil de mest filtrerte nøklene til en smal custom table med (meta_key-ekvivalent, verdi, post_id)-indekser, og la posts_join koble inn den tabellen kun på butikkens listevisning. Admin-søk og engangs-rapporter kan fortsatt bruke vanlig meta_query. Du deler da belastningen uten å skrive om hele domenemodellen på én sprint.
Hvordan hjelper en sammensatt indeks på wp_postmeta
WordPress-skjemaet (wp-admin/includes/schema.php) lager tre nøkler på wp_postmeta: PRIMARY KEY (meta_id), KEY post_id (post_id) og KEY meta_key (meta_key(191)).
Ta en lagersjekk:
SELECT post_id FROM wp_postmeta WHERE meta_key = '_stock_status' AND meta_value = 'instock';MySQL kan bare bruke indeksen på meta_key. Den finner radene for nøkkelen og må deretter lese hver rad for å sjekke om meta_value stemmer. Med mange rader per nøkkel betyr det mange lesinger fra buffer pool.
En sammensatt indeks på (meta_key(191), meta_value(100)) lar MySQL sjekke nøkkel og verdi inne i indeksen, slik at den bare leser radene som allerede treffer fra tabellen. Det er ikke en dekkende indeks: en sekundærindeks i InnoDB lagrer primærnøkkelen (meta_id), ikke post_id, så raden leses fortsatt for å hente post_id:
ALTER TABLE wp_postmeta ADD INDEX meta_key_val_comp (meta_key(191), meta_value(100));Test slike endringer på staging med EXPLAIN og Query Monitor. Ekstra indekser gjør målrettede lesinger raskere, men legger kostnad på hver update_post_meta() og add_post_meta().
Når bør du bruke wpdb prepare i stedet for WP_Query
WP_Query er riktig verktøy for maler, paginering og integrasjon med object cache. For aggregeringer, koblinger mot egne tabeller eller uthenting av mange ID-er uten core-laget er en direkte SQL-spørring med $wpdb ofte enklere og raskere.
Bruk alltid $wpdb->prepare() for å parametrisere spørringen og hindre SQL-injeksjon:
global $wpdb;
$min_lager = 5;
$landskode = 'NO';
$sql = $wpdb->prepare(
"SELECT p.ID, pm_stock.meta_value AS lager
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm_stock
ON p.ID = pm_stock.post_id AND pm_stock.meta_key = %s
INNER JOIN {$wpdb->postmeta} pm_geo
ON p.ID = pm_geo.post_id AND pm_geo.meta_key = %s
WHERE p.post_type = %s
AND p.post_status = %s
AND CAST(pm_stock.meta_value AS UNSIGNED) >= %d
AND pm_geo.meta_value = %s
ORDER BY CAST(pm_stock.meta_value AS UNSIGNED) DESC
LIMIT %d",
'_stock',
'_target_country',
'product',
'publish',
$min_lager,
$landskode,
20
);
$resultater = $wpdb->get_results( $sql );Valgkriterium:
WP_Queryi temamaler, vanlige paginerte arkiver og løkker som skal fylle WordPress sin object cache.$wpdb->prepare()for eksport, statistikkpaneler, numeriske aggregeringer (SUM(),AVG()) og endepunkter der filterkjeden iWP_Querybare legger til ventetid.
Hvordan vise relaterte innlegg med WP_Query uten plugin
Et vanlig krav er «relaterte innlegg» på single. Hold det kort, bruk term-ID-er, ekskluder gjeldende post, og slå av telling:
function wppoland_get_related_posts( int $current_id, int $limit = 3 ): WP_Query {
$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' => $limit,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'ignore_sticky_posts' => true,
]
);
}Bytt category__in til tax_query når du jobber med CPT-taksonomier. Unngå orderby => rand her med mindre settet er lite og cachet.
Hva sjekke før du merger en WP_Query-endring
- Trengs paginering? Hvis nei:
no_found_rows. - Trengs hele post-objektet? Hvis nei:
fields => ids. - Leser malen termer/meta i løkken? Hvis nei: skru av cache-oppdateringene.
- Har tall- og datofelter korrekt
typeimeta_query? - Er nestede
relation-grupper dokumentert med en kommentar om forretningsregelen? - Har Query Monitor kjørt mot realistisk datavolum?
- Er object cache-nøkler invalidert ved lagring?
Hva er viktigst å huske om avansert WP_Query
tax_query og meta_query følger samme mønster: klausuler, relation, nesting. EXISTS sparer verdiløse sammenligninger. Ytelse kommer fra bevisste flagg (no_found_rows, fields, meta/term-cache), fra å unngå fullskann i wp_postmeta, og fra måling i Query Monitor. Object cache forsterker en god spørring. Den reparerer ikke en dårlig.
Når filteret vokser forbi det WordPress sin meta-modell tåler, er neste steg ikke flere OR-klausuler. Det er en tabell eller et søkeindeks-lag med indekser som matcher hvordan brukerne faktisk filtrerer. Start med måling, deretter én hot path, deretter resten. Det er den billigste veien til en stabil listevisning.







