Avansert wp_Query: Taksonomier, meta-data og ytelse (2026)

Avansert wp_Query: Taksonomier, meta-data og ytelse (2026)

Sist verifisert: 21. september 2026
10 min lesetid
Casestudie
Full-stack-utvikler
500+ WP-prosjekter

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.

På WordCamp Oslo og i lokale meetups går samme samtale 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.

#Anatomi i tax_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. Dokumentasjonen for WP_Meta_Query er tydelig: når compare er EXISTS eller NOT EXISTS, ignoreres verdien. Å sende begge deler skaper falsk trygghet i kodegjennomgang.

#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.

#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.

#Ytelsesflagg: no_found_rows og fields ids

Standard WP_Query teller alle treff for paginering. Det historiske mønsteret brukte SQL_CALC_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 => true hopper over kostbar telling. På 50k+ poster er forskjellen ofte hundrevis av millisekunder per request.
  • fields => 'ids' returnerer bare post-ID-er. Bruk det når du skal mate post__in, bygge transient-nøkler, eller sjekke eksistens.
  • update_post_meta_cache / update_post_term_cache som false unngå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.

#Query Monitor som fasit

Gjetning om ytelse er bortkastet tid. Query Monitor viser SQL, duplikate spørringer, komponenten som trigget dem, og tidsbruk. En typisk arbeidsflyt:

  1. Aktiver QM på staging med samme datavolum som produksjon (eller en anonymisert dump).
  2. Last siden som bruker filteret.
  3. Finn WP_Query-raden: se JOIN-antall mot wp_term_relationships og wp_postmeta.
  4. Sammenlign med variante no_found_rows + fields => ids.
  5. 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.

#Object cache: hva den fikser og hva den ikke fikser

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 => ids returnerer et sett, kan du hydrate med get_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.

#Indekseringsfeller i wp_postmeta

wp_postmeta har indeks på meta_key og på (post_id, meta_key). Den har ikke en generisk, effektiv indeks for «alle rader der meta_value er mellom X og Y på tvers av nøkler». Konsekvenser:

  1. LIKE på meta_value - full tabellskann på store butikker. Unngå i hot path.
  2. Mange OR-klausuler på ulike nøkler - flere JOINs eller unions som MySQL håndterer dårlig.
  3. Tom key i meta_query - historisk mulighet for verdissammenligning uten nøkkel, men det skanner alt. Ikke bruk det.
  4. 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.

#Relaterte innlegg 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.

#Sjekkliste før merge

  1. Trengs paginering? Hvis nei: no_found_rows.
  2. Trengs hele post-objektet? Hvis nei: fields => ids.
  3. Leser malen termer/meta i løkken? Hvis nei: skru av cache-oppdateringene.
  4. Har tall- og datofelter korrekt type i meta_query?
  5. Er nestede relation-grupper dokumentert med en kommentar om forretningsregelen?
  6. Har Query Monitor kjørt mot realistisk datavolum?
  7. Er object cache-nøkler invalidert ved lagring?

#Kort oppsummering

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.

Neste steg

Gjør artikkelen om til faktisk implementering

Denne blokken styrker intern lenking og sender leseren videre til de mest relevante tjenestene og innholdet.

Vil du få dette implementert på nettstedet ditt?

Hvis problemet er Core Web Vitals, treg rendering eller tung WordPress-kjoring, kan jeg definere og gjennomfore optimaliseringen.

Relaterte artikler