WordPress Query API
The Query API is the backbone of WordPress content retrieval, powering The Loop and determining what content displays on every page.
Source: wp-includes/query.php, wp-includes/class-wp-query.php, wp-includes/class-wp-date-query.php
Components
| Component | Description |
|---|---|
| functions.md | Query helper functions and conditional tags |
| class-wp-query.md | Main query class with 50+ query variables |
| class-wp-date-query.md | Date-based query filtering |
| hooks.md | Actions and filters for query modification |
Global Query Objects
WordPress maintains several global query-related variables:
global $wp_query; // The main query object (may be modified)
global $wp_the_query; // Original/backup of the main query (never modified)
global $post; // Current post in The Loop
$wp_query vs $wp_the_query
$wp_the_query— The pristine main query, set duringwp()and never overwritten$wp_query— Points to$wp_the_queryinitially but can be replaced byquery_posts()- Always use
$wp_the_querywhen checkingis_main_query()to avoid false positives
The Loop
The Loop is WordPress’s fundamental content iteration pattern:
if ( have_posts() ) {
while ( have_posts() ) {
the_post();
// Access post data via template tags or $post global
the_title();
the_content();
}
} else {
// No posts found
}
Loop Functions
| Function | Description |
|---|---|
have_posts() |
Check if more posts exist in the loop |
the_post() |
Advance to next post, set up globals |
rewind_posts() |
Reset loop index to beginning |
in_the_loop() |
Check if currently inside a loop |
Custom Loops
For secondary queries, always use WP_Query directly:
$custom_query = new WP_Query( array(
'post_type' => 'product',
'posts_per_page' => 10,
) );
if ( $custom_query->have_posts() ) {
while ( $custom_query->have_posts() ) {
$custom_query->the_post();
// Display content
}
wp_reset_postdata(); // Restore original $post
}
Never use query_posts() for secondary loops. It overwrites the main query and causes pagination/conditional tag issues.
Query Flow
Main Query Lifecycle
URL Request
│
▼
WP::parse_request()
│ Parses URL into query vars
▼
WP::query_posts()
│ Creates WP_Query instance
▼
WP_Query::__construct()
│
├── init() Reset all properties
├── parse_query() Parse query vars, set flags
│ ├── fill_query_vars()
│ ├── parse_tax_query()
│ └── do_action('parse_query')
│
▼
WP_Query::get_posts()
│
├── do_action('pre_get_posts') ◄── Primary hook for modifying queries
│
├── Build SQL components:
│ ├── Parse meta_query → WP_Meta_Query
│ ├── Parse tax_query → WP_Tax_Query
│ ├── Parse date_query → WP_Date_Query
│ ├── Build WHERE, JOIN, ORDER BY
│ └── Apply filters (posts_where, posts_join, etc.)
│
├── apply_filters('posts_request')
│
├── Execute database query
│
├── apply_filters('posts_results')
│
├── apply_filters('the_posts')
│
└── Return posts array
Query Types Determined
During parse_query(), WordPress sets boolean flags indicating query type:
- Singular:
is_single,is_page,is_attachment,is_singular - Archive:
is_archive,is_category,is_tag,is_tax,is_author,is_date - Special:
is_home,is_front_page,is_search,is_feed,is_404
Three Ways to Query Posts
1. Main Query (Automatic)
WordPress creates this automatically based on URL:
// In template files, just use The Loop
if ( have_posts() ) : while ( have_posts() ) : the_post();
// ...
endwhile; endif;
2. WP_Query (Recommended for Custom Queries)
Full control with proper encapsulation:
$query = new WP_Query( $args );
// Use $query->have_posts(), $query->the_post()
wp_reset_postdata();
3. get_posts() (Simple Retrieval)
Returns array of posts without setting up loop globals:
$posts = get_posts( array(
'numberposts' => 5,
'category' => 3,
) );
foreach ( $posts as $post ) {
setup_postdata( $post );
// Use template tags
}
wp_reset_postdata();
Reset Functions
| Function | When to Use |
|---|---|
wp_reset_postdata() |
After custom WP_Query or get_posts() loops |
wp_reset_query() |
After query_posts() (which you shouldn’t use) |
rewind_posts() |
To loop through the same query again |
Why Reset Matters
Custom queries modify the global $post. If you don’t reset, template tags in sidebars/footers will reference the wrong post:
// Without reset, sidebar shows wrong post data
$my_query = new WP_Query( $args );
while ( $my_query->have_posts() ) {
$my_query->the_post();
the_title(); // Shows custom query post
}
// $post still points to last custom post!
wp_reset_postdata(); // Now $post points to main query post again
Common Query Patterns
Category Archive + Custom Ordering
add_action( 'pre_get_posts', function( $query ) {
if ( ! is_admin() && $query->is_main_query() && $query->is_category() ) {
$query->set( 'orderby', 'title' );
$query->set( 'order', 'ASC' );
}
} );
Exclude Posts from Home
add_action( 'pre_get_posts', function( $query ) {
if ( $query->is_home() && $query->is_main_query() ) {
$query->set( 'post__not_in', array( 1, 2, 3 ) );
}
} );
Include Custom Post Types in Search
add_action( 'pre_get_posts', function( $query ) {
if ( $query->is_search() && $query->is_main_query() ) {
$query->set( 'post_type', array( 'post', 'page', 'product' ) );
}
} );
Performance Considerations
Disable Unnecessary Queries
$query = new WP_Query( array(
'post_type' => 'post',
'no_found_rows' => true, // Skip SQL_CALC_FOUND_ROWS (no pagination)
'update_post_meta_cache' => false, // Skip meta cache priming
'update_post_term_cache' => false, // Skip term cache priming
'fields' => 'ids', // Return only IDs
) );
Query Caching
WordPress caches query results by default. The cache key is generated from:
- Query arguments
- SQL statement
postsandtermscache groups’last_changedvalues
Disable with 'cache_results' => false for one-off queries.
Debugging Queries
View Generated SQL
$query = new WP_Query( $args );
echo $query->request; // The SQL query
Query Monitor Plugin
Shows all queries, their execution time, and calling code.
Debug with Hooks
add_filter( 'posts_request', function( $sql, $query ) {
if ( $query->is_main_query() ) {
error_log( $sql );
}
return $sql;
}, 10, 2 );