The N+1 query problem occurs when code executes one database query to fetch a list of items, then one additional query for each item in the list to fetch related data — producing N+1 total queries for N items. In WordPress this surfaces in patterns like looping through posts and calling get_post_meta() on each one inside the loop, or iterating users and calling get_user_meta() per user. With 50 posts in a loop this produces 51 queries; with 200 posts it produces 201. WordPress’s object cache mitigates this for repeat requests, but on the first (uncached) load — or when object caching is not persistent — each call hits the database. WordPress provides several mechanisms to fetch all related data in a single batch query: cache priming flags on WP_Query, dedicated priming functions, and $wpdb batch queries with WHERE IN (...). Understanding these tools is essential for building performant admin list screens, REST API endpoints, and front-end archive templates.
Problem: A custom admin page displays 50 posts with their post meta (_event_date) and taxonomy terms. The current implementation calls get_post_meta() and get_the_terms() inside a loop, producing 101+ queries per page load.
Solution: Use WP_Query with update_post_meta_cache and update_post_term_cache to batch-prime both caches in two queries, then call get_post_meta() inside the loop — it reads from the primed cache with zero additional queries.
<?php
// ── BAD: N+1 pattern (1 query + 1 per post = 51 queries for 50 posts) ─
$posts = get_posts( [ 'posts_per_page' => 50, 'post_type' => 'event' ] );
foreach ( $posts as $post ) {
$date = get_post_meta( $post->ID, '_event_date', true ); // 1 query each
$terms = get_the_terms( $post->ID, 'event_category' ); // 1 query each
// ...
}
// ── GOOD: batch prime meta and term caches in WP_Query ────────────────
$query = new WP_Query( [
'post_type' => 'event',
'posts_per_page' => 50,
'no_found_rows' => true, // skip COUNT query when pagination not needed
'update_post_meta_cache' => true, // prime meta cache in 1 query (default: true)
'update_post_term_cache' => true, // prime term cache in 1 query (default: true)
] );
while ( $query->have_posts() ) {
$query->the_post();
$date = get_post_meta( get_the_ID(), '_event_date', true ); // reads from cache — 0 queries
$terms = get_the_terms( get_the_ID(), 'event_category' ); // reads from cache — 0 queries
}
wp_reset_postdata();
// Total queries: 3 (main query + meta prime + term prime) regardless of post count
// ── When you don't need meta or terms, disable to save 2 queries ──────
$ids_only_query = new WP_Query( [
'post_type' => 'event',
'posts_per_page' => 50,
'fields' => 'ids', // return only IDs
'no_found_rows' => true,
'update_post_meta_cache' => false, // skip meta prime — not needed
'update_post_term_cache' => false, // skip term prime — not needed
] );
// ── Prime post caches manually for a known set of IDs ─────────────────
// Useful when you have IDs from a custom query or meta value
$post_ids = [ 42, 87, 130, 201 ];
_prime_post_caches( $post_ids, true, true ); // args: ids, update_term_cache, update_meta_cache
// ── Batch user meta with update_meta_cache ─────────────────────────────
$user_ids = [ 1, 3, 7, 12 ];
update_meta_cache( 'user', $user_ids ); // prime all user meta in one query
foreach ( $user_ids as $uid ) {
$company = get_user_meta( $uid, 'company', true ); // reads from cache
}
// ── Batch custom query with WHERE IN ──────────────────────────────────
global $wpdb;
$ids_in = implode( ',', array_map( 'absint', $post_ids ) );
$event_dates = $wpdb->get_results(
"SELECT post_id, meta_value FROM {$wpdb->postmeta}
WHERE meta_key = '_event_date'
AND post_id IN ($ids_in)"
);
// Build a lookup map: post_id => date
$date_map = array_column( $event_dates, 'meta_value', 'post_id' );
NOTE: update_post_meta_cache and update_post_term_cache default to true in WP_Query — so standard WordPress loops already batch-load meta and terms. The N+1 problem appears when developers bypass WP_Query and use get_posts() with a custom SQL query that returns only IDs, then call get_post_meta() in a loop without priming first. In that case, call _prime_post_caches() with the ID array before the loop. For REST API endpoints that process many posts, disabling update_post_meta_cache and fetching only the specific meta you need with a single $wpdb query is often faster than the default prime-all approach.