WP_Query er klassen WordPress bruker til å hente innlegg fra databasen. Hver arkivside, søkeresultat og enkeltside starter med én hovedspørring. Sekundære loops er ekstra WP_Query-instanser du legger til i maler, widgets eller shortcodes. Feilen er nesten alltid den samme: for mange spørringer, for brede argumenter, eller feil pagineringsvariabel.
På norske hostinger med PHP-FPM (typisk Domene.no, Domeneshop, eller egen VPS med nginx + php-fpm) merker du dette først som økt pm.max_children-trykk og trege TTFB-tall, ikke som «langsom SQL» i admin. Denne guiden går gjennom hovedspørring versus sekundær WP_Query, pre_get_posts, ytelsesflagg, tax_query-feller, paginering, wp_reset_postdata(), Query Monitor, objekt-cache og når get_posts() er nok.
Offisiell referanse: WP_Query, pre_get_posts og ytelsesoptimalisering.
Hovedspørring eller sekundær WP_Query i WordPress
Hovedspørringen ($wp_the_query / den globale $wp_query) bygges før malen lastes. Den bestemmer hvilken mal WordPress velger (is_home(), is_archive(), is_singular() osv.) og hvilke innlegg have_posts() / the_post() viser i standardloopen.
if ( have_posts() ) :
while ( have_posts() ) :
the_post();
get_template_part( 'template-parts/content', get_post_type() );
endwhile;
endif;En sekundær loop er en ny instans. Den endrer ikke hovedspørringen og skal ikke brukes til å «fikse» arkivfiltre.
$related = new WP_Query(
[
'post_type' => 'post',
'posts_per_page' => 4,
'post__not_in' => [ get_the_ID() ],
'ignore_sticky_posts' => true,
'no_found_rows' => true,
]
);
if ( $related->have_posts() ) {
while ( $related->have_posts() ) {
$related->the_post();
the_title( '<h3>', '</h3>' );
}
wp_reset_postdata();
}Bruk aldri query_posts(). Den overskriver hovedspørringen midt i forespørselen, ødelegger paginering og betingelser, og tvinger en ekstra databasehit. Hvis du må endre hva arkivet viser, bruk pre_get_posts. Hvis du trenger ekstra innhold ved siden av hovedlisten, bruk new WP_Query().
Slik bruker du pre_get_posts i WordPress
pre_get_posts kjører før SQL bygges. Du får ett treff i databasen i stedet for «hent alt, deretter filtrer i PHP» eller «kjør en ny WP_Query ovenpå».
add_action(
'pre_get_posts',
static function ( WP_Query $query ): void {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_home() ) {
$query->set( 'posts_per_page', 12 );
$query->set( 'category__not_in', [ 17 ] ); // f.eks. «Nyhetsbrev».
}
if ( $query->is_post_type_archive( 'case' ) ) {
$query->set( 'orderby', 'menu_order title' );
$query->set( 'order', 'ASC' );
}
}
);Tre regler som sparer timer med feilsøking:
- Guard med
is_admin()og$query->is_main_query(). Uten dem treffer du REST, feeds, adminlister og hver sekundære loop. - Sett argumenter med
$query->set(), ikke ved å erstatte hele objektet. - Ikke kall
get_posts()ellernew WP_Query()inne i callbacken uten streng begrunnelse. Det lager nye spørringer under byggingen av den du prøver å justere.
På WooCommerce-arkiv er hovedspørringen produktlisten. Filtrer der, ikke med en ekstra loop i archive-product.php, hvis målet er samme URL og samme paginering.
Feeds (is_feed()) og søk (is_search()) er også hovedspørringer. Hvis du begrenser post_type på forsiden, sjekk at du ikke utilsiktet tømmer RSS eller søkeresultater. Guard med eksplisitte betingelser per kontekst.
Raskere WP_Query med no_found_rows og fields ids
Standard WP_Query kjører en spørring som også beregner totalt antall treff for paginering (FOUND_ROWS() / historisk SQL_CALC_FOUND_ROWS). Trenger du ikke sidetall, skru det av:
$ids = new WP_Query(
[
'post_type' => 'post',
'posts_per_page' => 20,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]
);Når du bare trenger ID-er (relaterte innlegg, eksklusjonslister, transient-nøkler), er fields => 'ids' det billigste alternativet: mindre data over wire, mindre hydrering av WP_Post-objekter, og du kan fortsatt bruke ID-ene i post__in.
update_post_meta_cache og update_post_term_cache er nyttige når loopen aldri kaller get_post_meta() eller taksonomi-tagger. Hvis malen henter featured image, kategorier eller ACF-felt, la dem stå på true (standard). Å skru dem av og deretter hente meta per innlegg gir N+1-spørringer som er verre enn én cache-warm.
posts_per_page => -1 er nesten alltid feil i produksjon. Sett et tall. Hvis du virkelig trenger alle ID-er (migrering, CLI), gjør det i WP-CLI eller en bakgrunnsjobb, ikke i en HTTP-forespørsel bak PHP-FPM.
Vanlige feil med tax_query i WP_Query
tax_query er kraftig og lett å gjøre dyr. Vanlige feil:
OR mellom taksonomier uten å forstå join-kostnad. Flere relation => 'OR'-klausuler på store taksonomier gir brede joins. Preferer én taksonomi med flere termer (operator => 'IN') når det er mulig.
Slug versus term_id. field => 'slug' er lesbart, men term_id er stabilt ved omskriving av slug. I kode som lever lenge, lagre term-ID i config eller options.
Tomme arrays. En tom terms-liste kan gi «ingen treff» eller uventet SQL avhengig av versjon og operator. Valider input før du bygger spørringen.
meta_query for filtre som burde vært taksonomi. Meta er lagret som EAV (wp_postmeta). Indeksene er svakere enn term-relasjoner. Hvis feltet er et filter (bransje, by, produktlinje), registrer en custom taxonomy i stedet for å søke på meta_key.
$args = [
'post_type' => 'post',
'posts_per_page' => 10,
'no_found_rows' => true,
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [ 4, 9 ],
'operator' => 'IN',
],
[
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => [ 'wordpress', 'ytelse' ],
'operator' => 'AND',
],
],
];include_children er true som standard for hierarkiske taksonomier. På flate lister der du vil treffe nøyaktig én term, sett include_children => false eksplisitt.
Paginering: paged versus page
WordPress bruker to query vars:
pagedfor arkiver, bloggindekser og de fleste tilpassede lister.pagefor paginering inne i et statisk forsideinnlegg / sideinnhold (<!--nextpage-->).
I en sekundær loop på et arkiv:
$paged = max( 1, (int) get_query_var( 'paged' ) );
$list = new WP_Query(
[
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => $paged,
]
);På en side satt som statisk forside (is_front_page() && is_page()) leser hovedspørringen ofte page, ikke paged. Mange themes feiler her: lenkene ser riktige ut, men loop nummer to starter alltid på side 1. Les begge når du er usikker:
$paged = (int) get_query_var( 'paged' );
if ( $paged < 1 ) {
$paged = (int) get_query_var( 'page' );
}
$paged = max( 1, $paged );For pagineringslenker i sekundære loops: send total og current til paginate_links() basert på $list->max_num_pages og $paged, ikke den globale $wp_query. Ellers peker «neste side» på hovedarkivets sidetall.
Når loopen ikke skal pagineres, sett no_found_rows => true og hopp over paginate_links(). Du sparer tellespørringen helt.
wp_reset_postdata og globale $post
$related->the_post() setter den globale $post. Etter loopen må du kalle wp_reset_postdata() slik at resten av malen (sidebar, footer, related widgets) fortsatt ser hovedinnlegget.
$q = new WP_Query( $args );
while ( $q->have_posts() ) {
$q->the_post();
// mal
}
wp_reset_postdata();wp_reset_query() er for query_posts()-æraen. Bruk den ikke i ny kode. Hvis du itererer over et array av WP_Post uten the_post(), bruk setup_postdata( $post ) inne i loopen og wp_reset_postdata() etterpå, eller unngå globale template-tagger og les $post->post_title direkte.
Glemmer du reset, feiler get_the_ID() i footer, schema-blokker og «les også»-moduler som forventer hovedinnlegget.
Finn trege spørringer med Query Monitor
Query Monitor viser SQL, kallstack og hvilken komponent som eier spørringen. Arbeidsflyt som fungerer på staging:
- Åpne siden som føles treg (arkiv, produktliste, forside med tre «featured»-blokker).
- Se fanen Queries: sorter på tid. Finn
WP_Query-rader med høy varighet eller mange like spørringer. - Sjekk Duplicate Queries: ofte er det samme
tax_queryi header, hero og hovedliste. - Se Hooks: bekreft at
pre_get_posts-callbacken din faktisk kjører på hovedspørringen.
På delt hosting i Norge har du ofte begrenset memory_limit og få PHP-FPM-workere. Ti ekstra WP_Query på forsiden multipliseres under kampanjetrafikk. Query Monitor på lokal eller staging gir tallene; i produksjon holder det med New Relic / Blackfire hvis dere har det, eller midlertidig QM bak IP-restriksjon.
Objekt-cache og WP_Query
WP_Query cacher ikke hele resultatmengden automatisk på tvers av forespørsler. Den varmer post-, meta- og term-cacher for ID-ene den henter. Med Redis eller Memcached som objekt-cache (vanlig på managed WordPress og egen VPS) blir gjentatte get_post() / meta-oppslag billigere, men den første SQL-en for en unik argumentkombinasjon går fortsatt til MySQL.
Praktiske mønstre:
- Transient rundt ID-lister. Cache arrayet av ID-er fra en dyr
tax_query, TTL 5-15 minutter, invalidér vedsave_postfor den posttypen. - Ikke cache hele HTML-en i samme transient som ID-listen hvis innholdet er personalisert eller har nonce-avhengige deler.
- Sticky posts og
post__in-rekkefølge. Når du cacher ID-er, huskorderby => 'post__in'ved rehydrering.
Uten objekt-cache lagres transients i wp_options (autoload kan bli et problem). På norske budsjethostinger uten Redis: korte TTL-er, få nøkler, og rydd døde transients. Med Redis: bruk en nøkkelprefiks per miljø (prod:, staging:) slik at cache ikke lekker mellom deploy-slots.
Full page cache (nginx FastCGI cache, Cloudflare, eller plugin-basert HTML-cache) sitter foran PHP-FPM. En billig WP_Query hjelper fortsatt ved cache miss, preview, innloggede brukere og WooCommerce-handlekurv-sider der HTML ikke caches. Optimaliser spørringen uansett; stol ikke på edge-cache alene.
Når bruke get_posts i stedet for WP_Query
get_posts() er en tynn wrapper rundt WP_Query med noen standardverdier (suppress_filters historisk, no_found_rows-lignende oppførsel for enkle kall). Bruk den når:
- du trenger et array av innlegg eller ID-er,
- du ikke trenger
have_posts()-loopen, - du ikke paginerer.
$ids = get_posts(
[
'post_type' => 'post',
'posts_per_page' => 5,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]
);Velg new WP_Query() når du trenger found_posts, max_num_pages, egen paginering, eller eksplisitt kontroll over filtre. Velg pre_get_posts når du endrer det arkivet brukeren allerede er på. Velg get_posts() for små, ikke-paginerte hjelpelister i PHP.
REST API (/wp/v2/posts) og blokkeditorens Query Loop er egne stier. De ender også i WP_Query, men filtrene dine på pre_get_posts må respektere is_admin(), REST-kontekst og WP_Query::is_main_query() slik at du ikke endrer API-svar utilsiktet.
WP_Query er synkron i forespørselen. Unngå å kjøre tunge sekundære loops i wp_footer «fordi det er praktisk». Flytt dem til stedet i malen der HTML faktisk trengs, eller last via REST/Ajax hvis blokken er under folden.
Sjekkliste for WP_Query-kode før merge
- Er dette hovedspørringen? Bruk
pre_get_posts, ikke en ekstra loop. - Trenger du paginering? Hvis nei:
no_found_rows => true. Hvis ja: korrektpaged/page. - Trenger du bare ID-er?
fields => 'ids'. - Er filteret en taksonomi, ikke meta?
- Kall
wp_reset_postdata()etter hver sekundær loop som brukerthe_post(). - Verifiser med Query Monitor at antall SQL ikke dobles på forsiden.
- Med objekt-cache: cache ID-lister, ikke antagelser om «WordPress cacher spørringen for meg».
Videre lesning internt: hvordan gjøre en WordPress-basert nettside raskere. Eksternt: den offisielle WP_Query-referansen, pre_get_posts og advanced administration: performance.







