329 lines
8.0 KiB
Markdown
329 lines
8.0 KiB
Markdown
# 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
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```php
|
|
// ❌ 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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
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)
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```sql
|
|
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
|
|
|
|
```sql
|
|
-- 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)
|
|
|
|
```php
|
|
$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)
|
|
|
|
```php
|
|
$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)
|
|
|
|
```php
|
|
// 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)
|
|
|
|
```php
|
|
// 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;
|
|
```
|