Optimise WP_Query with no_found_rows, update_post_meta_cache, and fields Flags

Every new WP_Query() call generates at least one SQL query. With default settings it generates two — the main SELECT for the requested posts, plus a SELECT FOUND_ROWS() query to count the total matching rows for pagination. After both queries return, WordPress performs two more database round-trips: it bulk-preloads all post meta for the returned post IDs (update_post_meta_cache) and bulk-preloads all taxonomy terms (update_post_term_cache). For the main page query and archive pages this overhead is justified because templates need the meta and terms. But for widgets, shortcodes, card lists, and secondary loops that only need the title and URL of a few posts, this is three or four queries worth of overhead for data that never gets used. WordPress provides explicit flags to disable each of these operations individually — combining them on queries where the extra data isn’t needed typically cuts per-query overhead by 50–80%.

Problem: A sidebar widget that shows the 5 most recent post titles and links is generating 4 database queries per page load because of the default COUNT, meta cache, and term cache operations — all for data that the widget never displays.

Solution: Add 'no_found_rows' => true, 'update_post_meta_cache' => false, and 'update_post_term_cache' => false to skip the unnecessary operations. Use 'fields' => 'ids' if you only need post IDs.

<?php
// ── Default query — generates 4 DB operations ────────────────────────
$slow = new WP_Query( [
    'post_type'      => 'post',
    'posts_per_page' => 5,
] );
// 1. SELECT ... FROM wp_posts WHERE ...
// 2. SELECT FOUND_ROWS()         ← total count for pagination
// 3. SELECT ... FROM wp_postmeta ← bulk meta preload
// 4. SELECT ... FROM wp_term_relationships ← term preload

// ── Optimised query — only 1 DB operation ────────────────────────────
$fast = new WP_Query( [
    'post_type'              => 'post',
    'posts_per_page'         => 5,
    'no_found_rows'          => true,   // skip SELECT FOUND_ROWS()
    'update_post_meta_cache' => false,  // skip meta preload
    'update_post_term_cache' => false,  // skip term preload
] );

// ── Return only IDs (no full WP_Post object population) ──────────────
$ids_only = new WP_Query( [
    'post_type'              => 'post',
    'posts_per_page'         => 10,
    'no_found_rows'          => true,
    'update_post_meta_cache' => false,
    'update_post_term_cache' => false,
    'fields'                 => 'ids',   // returns int[] instead of WP_Post[]
] );

// ── When each flag is appropriate ─────────────────────────────────────
// no_found_rows = true         → safe when you don't need pagination or total count
// update_post_meta_cache = false → safe when template never calls get_post_meta()
// update_post_term_cache = false → safe when template never calls get_the_terms() or wp_get_post_terms()
// fields = 'ids'               → use when you only need IDs to loop over or pass elsewhere

Practical example — a widget that outputs 5 recent posts by title with no meta or taxonomy data:

<?php
function render_recent_posts_widget() {
    $posts = get_posts( [
        'post_type'              => 'post',
        'posts_per_page'         => 5,
        'no_found_rows'          => true,
        'update_post_meta_cache' => false,
        'update_post_term_cache' => false,
    ] );

    echo '<ul class="recent-posts">';
    foreach ( $posts as $post ) {
        printf(
            '<li><a href="%s">%s</a></li>',
            esc_url( get_permalink( $post ) ),
            esc_html( $post->post_title )
        );
    }
    echo '</ul>';
}

NOTE: Do not disable update_post_meta_cache or update_post_term_cache if your template calls get_post_meta(), get_the_terms(), or any function that reads post meta or terms in the loop. Without the bulk preload, each individual get_post_meta() call generates its own database query — turning one preload query into N individual queries, which is much worse than leaving the default on.