Initial commit
This commit is contained in:
328
skills/wp-performance-review/references/wp-query-guide.md
Normal file
328
skills/wp-performance-review/references/wp-query-guide.md
Normal 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;
|
||||
```
|
||||
Reference in New Issue
Block a user