Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:25:33 +08:00
commit ec3986470d
10 changed files with 2945 additions and 0 deletions

View File

@@ -0,0 +1,328 @@
# 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;
```