Files
gh-elvismdev-claude-wordpre…/skills/wp-performance-review/references/wp-query-guide.md
2025-11-29 18:25:33 +08:00

8.0 KiB

WP_Query Optimization Guide

Best practices for efficient database queries in WordPress applications.

WP_Query Review Checklist

When reviewing WP_Query code, verify:

  • posts_per_page is set (not -1, not missing)
  • no_found_rows => true if not paginating
  • fields => 'ids' if only IDs needed
  • update_post_meta_cache => false if meta not used
  • update_post_term_cache => false if terms not used
  • Date limits on archive queries (recent content only)
  • include_children => false if child terms not needed
  • No post__not_in with large arrays
  • No meta_query on meta_value (use taxonomy or key presence)
  • Results cached with wp_cache_set if repeated

Core Principles

  1. Always set limits - Never use posts_per_page => -1
  2. Validate inputs - Check IDs before querying
  3. Limit scope - Use date ranges, taxonomies to narrow results
  4. Skip unnecessary work - Use no_found_rows, fields
  5. Pre-fetch related data - Avoid N+1 queries
  6. Cache results - Use object cache for repeated queries

Essential Query Arguments

Limiting Results

// REQUIRED: Always limit results
'posts_per_page' => 10,

// Skip counting total posts if not paginating
'no_found_rows' => true,  // Skips SQL_CALC_FOUND_ROWS

// Return only IDs when you don't need full post objects
'fields' => 'ids',

// Suppress filters when you need raw results
'suppress_filters' => true,

Date-Based Limiting

For sites with years of content, limit queries to relevant time ranges:

// Limit to recent content
$query = new WP_Query([
    'post_type' => 'post',
    'posts_per_page' => 10,
    'date_query' => [
        [
            'after' => '3 months ago',
            'inclusive' => true
        ]
    ]
]);

// Specific date range
'date_query' => [
    'after' => '2024-01-01',
    'before' => '2024-12-31'
]

// Dynamic date filtering via pre_get_posts
add_action('pre_get_posts', function($query) {
    if (!is_admin() && $query->is_main_query() && $query->is_category('news')) {
        $query->set('date_query', [
            'after' => date('Y-m-d', strtotime('-3 months'))
        ]);
    }
});

Taxonomy Query Optimization

// Exclude child terms to reduce query complexity
'tax_query' => [
    [
        'taxonomy' => 'category',
        'field' => 'term_id',
        'terms' => [6],
        'include_children' => false  // Important for performance
    ]
]

// Use term_id instead of slug (avoids extra lookup)
'field' => 'term_id',  // Faster than 'slug' or 'name'

Meta Query Optimization

Meta queries are expensive - minimize usage:

// ❌ AVOID: Multiple meta conditions
'meta_query' => [
    'relation' => 'AND',
    ['key' => 'color', 'value' => 'red'],
    ['key' => 'size', 'value' => 'large'],
    ['key' => 'price', 'compare' => '>=', 'value' => 100]
]

// ✅ BETTER: Use taxonomies for filterable attributes
// Register 'color' and 'size' as taxonomies instead

// ✅ ALTERNATIVE: Offload to ElasticSearch
// Configure ElasticPress to index post meta

Pre-fetching & Cache Priming

Update Meta Cache in Bulk

// After running WP_Query, prime the meta cache
$query = new WP_Query($args);
$post_ids = wp_list_pluck($query->posts, 'ID');

// Prime meta cache (single query vs N queries)
update_postmeta_cache($post_ids);

// Prime term cache
update_object_term_cache($post_ids, 'post');

// Prime user cache for authors
$author_ids = wp_list_pluck($query->posts, 'post_author');
cache_users($author_ids);

WP_Query Built-in Cache Updates

// These are on by default but be aware:
'update_post_meta_cache' => true,   // Updates meta cache after query
'update_post_term_cache' => true,   // Updates term cache after query

// Disable if you don't need meta/terms (saves queries)
'update_post_meta_cache' => false,
'update_post_term_cache' => false,

Caching Query Results

Object Cache for Repeated Queries

function get_featured_posts() {
    $cache_key = 'featured_posts_v1';
    $posts = wp_cache_get($cache_key, 'my_plugin');
    
    if (false === $posts) {
        $query = new WP_Query([
            'post_type' => 'post',
            'posts_per_page' => 5,
            'meta_key' => 'featured',
            'meta_value' => '1'
        ]);
        
        $posts = $query->posts;
        wp_cache_set($cache_key, $posts, 'my_plugin', HOUR_IN_SECONDS);
    }
    
    return $posts;
}

// Invalidate when posts are updated
add_action('save_post', function($post_id) {
    wp_cache_delete('featured_posts_v1', 'my_plugin');
});

Static Variable Caching (Request-Scoped)

function get_expensive_data($id) {
    static $cache = [];
    
    if (!isset($cache[$id])) {
        $cache[$id] = expensive_computation($id);
    }
    
    return $cache[$id];
}

ElasticSearch Offloading

For complex searches, offload to ElasticSearch via ElasticPress:

When to Offload

  • Full-text search queries
  • Complex meta queries
  • Faceted search / filtering
  • Large result sets with sorting
  • Aggregations / analytics

When NOT to Offload

  • Simple primary key lookups
  • Transactional data requiring ACID
  • Data requiring immediate consistency
  • Small data sets
// ElasticPress automatically intercepts WP_Query
// for search queries when configured

// Force MySQL for specific queries
$query = new WP_Query([
    'ep_integrate' => false,  // Skip ElasticSearch
    // ... args
]);

Analyzing Queries with EXPLAIN

Use EXPLAIN to identify slow queries:

EXPLAIN SELECT * FROM wp_posts 
WHERE post_type = 'post' 
AND post_status = 'publish' 
ORDER BY post_date DESC 
LIMIT 10;

Key EXPLAIN Indicators

Column Good Value Bad Value
type const, eq_ref, ref, range ALL (full table scan)
key Named index NULL (no index used)
rows Small number Large number
Extra Using index Using filesort, Using temporary

Common Optimization Actions

-- Add index for frequently queried meta key
ALTER TABLE wp_postmeta ADD INDEX meta_key_value (meta_key, meta_value(50));

-- Add composite index
ALTER TABLE wp_posts ADD INDEX type_status_date (post_type, post_status, post_date);

Query Monitor Integration

Use Query Monitor plugin to identify:

  1. Slow queries - Queries over threshold
  2. Duplicate queries - Same query run multiple times
  3. Queries by component - Which plugin/theme caused each query
  4. Query count - Total queries per page load

Target Metrics

Metric Target Concern
Total queries < 50 > 100
Duplicate queries 0 > 5
Slowest query < 50ms > 100ms
Total query time < 100ms > 500ms

Common Query Patterns

Get Latest Posts (Optimized)

$query = new WP_Query([
    'post_type' => 'post',
    'post_status' => 'publish',
    'posts_per_page' => 10,
    'no_found_rows' => true,
    'update_post_term_cache' => false,  // If not displaying categories
]);

Get Posts by IDs (Optimized)

$query = new WP_Query([
    'post__in' => $post_ids,
    'posts_per_page' => count($post_ids),
    'orderby' => 'post__in',  // Preserve ID order
    'no_found_rows' => true,
    'ignore_sticky_posts' => true
]);

Check if Posts Exist (Optimized)

// Don't fetch full posts just to check existence
$query = new WP_Query([
    'post_type' => 'product',
    'posts_per_page' => 1,
    'fields' => 'ids',
    'no_found_rows' => true,
    'update_post_meta_cache' => false,
    'update_post_term_cache' => false
]);
$has_products = $query->have_posts();

Count Posts (Optimized)

// Use wp_count_posts() for simple counts
$counts = wp_count_posts('post');
$published = $counts->publish;

// For filtered counts, use found_posts
$query = new WP_Query([
    'post_type' => 'post',
    'posts_per_page' => 1,  // Minimize actual retrieval
    'category_name' => 'news',
    // no_found_rows must be FALSE to get found_posts
]);
$total = $query->found_posts;