🔍 Custom Queries with WP_Query
Master WordPress's powerful query system for custom content retrieval
Learn WP_Query, get_posts, and advanced query techniques
Learning Objectives
- Understand the three main query methods in WordPress
- Master WP_Query class and its parameters
- Create custom loops for specific content
- Implement multiple loops on a single page
- Optimize queries for performance
- Handle query reset and cleanup
- Build complex queries with meta and taxonomy parameters
WordPress Query Methods
WordPress provides three main methods for querying posts. Understanding when to use each is crucial for efficient theme development.
| Method | Use Case | Returns | Affects Main Query | Best For |
|---|---|---|---|---|
| WP_Query | Complex custom queries | WP_Query object | No | Secondary loops, widgets |
| get_posts() | Simple post retrieval | Array of post objects | No | Simple lists, dropdowns |
| query_posts() | ⚠️ Avoid using | Array of posts | Yes (breaks pagination) | Never use in themes |
Never use query_posts() in themes! It can break pagination, affect performance, and cause unexpected behavior. Use WP_Query or get_posts() instead.
Understanding WP_Query
WP_Query is the heart of WordPress's content retrieval system. Every time WordPress displays posts, it's using WP_Query behind the scenes.
Basic WP_Query Structure
<?php
// 1. Define query arguments
$args = array(
'post_type' => 'post',
'posts_per_page' => 10,
'orderby' => 'date',
'order' => 'DESC'
);
// 2. Create new query
$custom_query = new WP_Query( $args );
// 3. The Loop
if ( $custom_query->have_posts() ) :
while ( $custom_query->have_posts() ) : $custom_query->the_post();
// Display post content
the_title();
the_content();
endwhile;
else :
echo 'No posts found';
endif;
// 4. Reset post data
wp_reset_postdata();
?>
Best Practice
Always call wp_reset_postdata() after custom queries to restore the global $post object.
Essential WP_Query Parameters
Post Type Parameters
<?php
$args = array(
// Single post type
'post_type' => 'product',
// Multiple post types
'post_type' => array( 'post', 'page', 'product' ),
// All post types
'post_type' => 'any',
// Include attachments
'post_type' => array( 'post', 'attachment' ),
'post_status' => array( 'publish', 'inherit' )
);
?>
Pagination Parameters
<?php
$args = array(
'posts_per_page' => 10, // Number of posts (use -1 for all)
'nopaging' => false, // Enable/disable pagination
'paged' => get_query_var('paged'), // Current page
'offset' => 5, // Skip first X posts
'page' => 2, // Static page number
'ignore_sticky_posts' => true // Ignore sticky posts
);
?>
Order & Orderby Parameters
<?php
$args = array(
'orderby' => 'date', // date, title, menu_order, rand
'order' => 'DESC', // DESC or ASC
// Multiple orderby
'orderby' => array(
'menu_order' => 'ASC',
'date' => 'DESC'
),
// Order by meta value
'meta_key' => 'price',
'orderby' => 'meta_value_num',
'order' => 'ASC'
);
?>
Author Parameters
<?php
$args = array(
'author' => 1, // Author ID
'author_name' => 'john', // Author nicename
'author__in' => array(1,2,3), // Multiple authors
'author__not_in' => array(4,5) // Exclude authors
);
?>
Category Parameters
<?php
$args = array(
'cat' => 5, // Category ID
'category_name' => 'news', // Category slug
'category__and' => array(2,6), // Posts in ALL these categories
'category__in' => array(2,6), // Posts in ANY of these categories
'category__not_in' => array(3,7) // Exclude categories
);
?>
Tag Parameters
<?php
$args = array(
'tag' => 'cooking', // Tag slug
'tag_id' => 5, // Tag ID
'tag__and' => array(2,3), // Posts with ALL these tags
'tag__in' => array(2,3), // Posts with ANY of these tags
'tag__not_in' => array(4,5), // Exclude tags
'tag_slug__and' => array('news','featured')
);
?>
Custom Taxonomy Parameters
<?php
$args = array(
'tax_query' => array(
array(
'taxonomy' => 'product_category',
'field' => 'slug',
'terms' => 'electronics',
),
// Relationship between multiple taxonomies
'relation' => 'AND',
array(
'taxonomy' => 'product_tag',
'field' => 'term_id',
'terms' => array(10, 20),
'operator' => 'IN'
)
)
);
?>
Meta Query (Custom Fields)
<?php
$args = array(
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'price',
'value' => 100,
'type' => 'NUMERIC',
'compare' => '>='
),
array(
'key' => 'color',
'value' => 'blue',
'compare' => '='
),
array(
'key' => 'featured',
'value' => 'yes',
'compare' => '='
)
)
);
?>
Multiple Loops on One Page
Often you'll need multiple queries on a single page - featured posts, recent posts, related posts, etc.
Example: Homepage with Multiple Sections
<?php
// Featured Posts Loop
$featured_args = array(
'post_type' => 'post',
'posts_per_page' => 3,
'meta_key' => 'featured',
'meta_value' => 'yes'
);
$featured_query = new WP_Query( $featured_args );
if ( $featured_query->have_posts() ) : ?>
<section class="featured-posts">
<h2>Featured Posts</h2>
<?php while ( $featured_query->have_posts() ) : $featured_query->the_post(); ?>
<article>
<h3><?php the_title(); ?></h3>
<?php the_excerpt(); ?>
</article>
<?php endwhile; ?>
</section>
<?php endif;
wp_reset_postdata();
// Recent Posts Loop (excluding featured)
$recent_args = array(
'post_type' => 'post',
'posts_per_page' => 5,
'meta_query' => array(
array(
'key' => 'featured',
'value' => 'yes',
'compare' => '!='
)
)
);
$recent_query = new WP_Query( $recent_args );
if ( $recent_query->have_posts() ) : ?>
<section class="recent-posts">
<h2>Recent Posts</h2>
<?php while ( $recent_query->have_posts() ) : $recent_query->the_post(); ?>
<article>
<h3><?php the_title(); ?></h3>
<?php the_excerpt(); ?>
</article>
<?php endwhile; ?>
</section>
<?php endif;
wp_reset_postdata();
// Popular Posts Loop (by comment count)
$popular_args = array(
'post_type' => 'post',
'posts_per_page' => 5,
'orderby' => 'comment_count',
'order' => 'DESC'
);
$popular_query = new WP_Query( $popular_args );
if ( $popular_query->have_posts() ) : ?>
<section class="popular-posts">
<h2>Popular Posts</h2>
<?php while ( $popular_query->have_posts() ) : $popular_query->the_post(); ?>
<article>
<h3><?php the_title(); ?></h3>
<span><?php comments_number(); ?></span>
</article>
<?php endwhile; ?>
</section>
<?php endif;
wp_reset_postdata();
?>
Using get_posts() for Simple Queries
get_posts() is a simpler alternative to WP_Query for basic post retrieval. It returns an array of post objects.
get_posts() Example
<?php
$posts = get_posts( array(
'numberposts' => 5,
'category' => 3,
'orderby' => 'date',
'order' => 'DESC'
));
foreach ( $posts as $post ) :
setup_postdata( $post ); ?>
<article>
<h2><?php the_title(); ?></h2>
<?php the_excerpt(); ?>
</article>
<?php endforeach;
wp_reset_postdata();
?>
When to Use get_posts()
- Simple lists without pagination
- Dropdown menus or select boxes
- Widget content
- Quick data retrieval for processing
Advanced Query Examples
Posts from Date Range
<?php
$args = array(
'post_type' => 'post',
'date_query' => array(
array(
'after' => 'January 1, 2024',
'before' => 'December 31, 2024',
'inclusive' => true,
),
),
'posts_per_page' => -1,
);
$year_posts = new WP_Query( $args );
?>
Products by Price Range
<?php
$args = array(
'post_type' => 'product',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'price',
'value' => array( 100, 500 ),
'type' => 'NUMERIC',
'compare' => 'BETWEEN'
),
array(
'key' => 'in_stock',
'value' => 'yes',
'compare' => '='
)
),
'orderby' => 'meta_value_num',
'meta_key' => 'price',
'order' => 'ASC'
);
$products = new WP_Query( $args );
?>
Search Query with Filters
<?php
$search_term = get_search_query();
$args = array(
's' => $search_term,
'post_type' => array( 'post', 'page', 'product' ),
'posts_per_page' => 10,
'meta_query' => array(
'relation' => 'OR',
array(
'key' => 'subtitle',
'value' => $search_term,
'compare' => 'LIKE'
),
array(
'key' => 'description',
'value' => $search_term,
'compare' => 'LIKE'
)
)
);
$search_results = new WP_Query( $args );
?>
Query Performance Optimization
Performance Best Practices
- Limit fields returned: Use
'fields' => 'ids'when you only need IDs - Disable unnecessary features:
'no_found_rows' => truewhen not using pagination - Cache queries: Use transients for expensive queries
- Optimize meta queries: Index custom fields used in queries
- Limit posts retrieved: Always set reasonable
posts_per_pagelimits
Optimized Query Example
<?php
// Cache expensive query
$cache_key = 'featured_products_' . get_current_blog_id();
$featured_products = get_transient( $cache_key );
if ( false === $featured_products ) {
$args = array(
'post_type' => 'product',
'posts_per_page' => 10,
'no_found_rows' => true, // Disable pagination info
'fields' => 'ids', // Get only IDs
'meta_query' => array(
array(
'key' => 'featured',
'value' => 'yes'
)
)
);
$query = new WP_Query( $args );
$featured_products = $query->posts;
// Cache for 12 hours
set_transient( $cache_key, $featured_products, 12 * HOUR_IN_SECONDS );
}
// Now use the IDs to get full posts
foreach ( $featured_products as $product_id ) {
$post = get_post( $product_id );
// Display post
}
?>
Common Pitfalls and Solutions
Pitfall 1: Forgetting wp_reset_postdata()
Solution: Always reset after custom queries
Solution: Always reset after custom queries
Pitfall 2: Using query_posts()
Solution: Use WP_Query or get_posts() instead
Solution: Use WP_Query or get_posts() instead
Pitfall 3: Not handling empty results
Solution: Always check have_posts() before looping
Solution: Always check have_posts() before looping
Pitfall 4: Inefficient meta queries
Solution: Combine conditions, use proper compare operators
Solution: Combine conditions, use proper compare operators
Practice Exercise
Custom Query Challenge