How to Debug WP_Query (SQL, Hooks, Query Monitor)
When a WP_Query doesn’t return what you expect—or it’s slow—the fastest path to the truth is:
- Inspect the generated SQL
- Trace which hooks modified the query
- Confirm the actual executed queries (and timing)
This guide shows reliable, production-safe techniques to debug WP_Query using SQL inspection,
core hooks, and Query Monitor.
Start With the Query Context (What Are You Actually Debugging?)
Before touching SQL, confirm what query you are dealing with:
- Main query vs secondary query
- Front-end vs admin
- Archive/search/single/template-specific
- Modified by
pre_get_postsor other filters
A huge percentage of “WP_Query bugs” are actually “wrong query instance” bugs.
Step 1: Print the Query Vars (Safely)
If you can reproduce the issue locally or in staging, dump query vars from the specific query object.
<?php
// Example: debugging a custom WP_Query instance.
$q = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => 10,
) );
error_log( 'WP_Query vars: ' . print_r( $q->query_vars, true ) );
?>
For the main query, use:
<?php
global $wp_query;
error_log( 'Main query vars: ' . print_r( $wp_query->query_vars, true ) );
?>
Step 2: Get the Generated SQL from WP_Query
After the query runs, you can inspect the SQL via $query->request.
This is the single most useful debugging property.
<?php
$q = new WP_Query( array(
'post_type' => array( 'post', 'page' ),
'posts_per_page' => 5,
'orderby' => 'date',
'order' => 'DESC',
) );
error_log( 'WP_Query SQL: ' . $q->request );
?>
If you’re debugging the main query:
<?php
global $wp_query;
error_log( 'Main SQL: ' . $wp_query->request );
?>
Step 3: Log the SQL Only for Specific Requests
Dumping SQL on every request is noisy. Scope your logging.
A clean pattern is to enable logging only when a debug query param exists.
<?php
function wpct_debug_enabled(): bool {
if ( ! isset( $_GET['debug_query'] ) ) return false;
return current_user_can( 'manage_options' );
}
add_action( 'wp', function () {
if ( ! wpct_debug_enabled() ) return;
global $wp_query;
error_log( '--- WP_Query Debug (main) ---' );
error_log( 'Vars: ' . print_r( $wp_query->query_vars, true ) );
error_log( 'SQL: ' . $wp_query->request );
} );
?>
Now you can open a URL like:
?debug_query=1
…and only admins will trigger logging.
Step 4: Use Hooks to See How the SQL Was Built
Sometimes you need to know what modified the query:
- Another plugin added a meta query
- A theme filter changed ordering
- A search customization altered WHERE clauses
These filters let you intercept SQL parts.
Inspect the Full SQL Right Before Execution (posts_request)
<?php
add_filter( 'posts_request', function ( $sql, $query ) {
if ( ! wpct_debug_enabled() ) return $sql;
if ( is_admin() ) return $sql;
if ( ! $query->is_main_query() ) return $sql;
error_log( 'posts_request SQL: ' . $sql );
return $sql;
}, 9999, 2 );
?>
Inspect Individual SQL Clauses (posts_clauses)
This is useful when the final SQL is too dense and you want to see WHERE/JOIN/ORDERBY separately.
<?php
add_filter( 'posts_clauses', function ( $clauses, $query ) {
if ( ! wpct_debug_enabled() ) return $clauses;
if ( is_admin() ) return $clauses;
if ( ! $query->is_main_query() ) return $clauses;
error_log( 'JOIN: ' . ( $clauses['join'] ?? '' ) );
error_log( 'WHERE: ' . ( $clauses['where'] ?? '' ) );
error_log( 'GROUPBY: ' . ( $clauses['groupby'] ?? '' ) );
error_log( 'ORDERBY: ' . ( $clauses['orderby'] ?? '' ) );
error_log( 'LIMIT: ' . ( $clauses['limits'] ?? '' ) );
return $clauses;
}, 9999, 2 );
?>
Catch ORDER BY Problems (posts_orderby)
If sorting is wrong, this filter tells you what orderby string WordPress ended up using.
<?php
add_filter( 'posts_orderby', function ( $orderby, $query ) {
if ( ! wpct_debug_enabled() ) return $orderby;
if ( is_admin() ) return $orderby;
if ( ! $query->is_main_query() ) return $orderby;
error_log( 'posts_orderby: ' . $orderby );
return $orderby;
}, 9999, 2 );
?>
Step 5: Debug What Changed the Query (pre_get_posts)
If you suspect pre_get_posts logic (yours or a plugin’s), log the query vars right after modifications.
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( ! wpct_debug_enabled() ) return;
if ( is_admin() ) return;
if ( ! $query->is_main_query() ) return;
error_log( 'pre_get_posts vars: ' . print_r( $query->query_vars, true ) );
}, 9999 );
?>
If the vars are correct here, but the SQL is wrong later, another filter is modifying clauses downstream.
That’s when posts_clauses becomes your best friend.
Step 6: Track Query Timing with SAVEQUERIES
If performance is the problem, you want query timing.
WordPress can record all DB queries when SAVEQUERIES is enabled.
In wp-config.php (staging/local only):
<?php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'SAVEQUERIES', true );
?>
Then you can inspect:
<?php
global $wpdb;
if ( wpct_debug_enabled() && ! empty( $wpdb->queries ) ) {
// Each item: [ SQL, time, caller ]
error_log( 'Total queries: ' . count( $wpdb->queries ) );
error_log( 'Slowest query sample: ' . print_r( $wpdb->queries[0], true ) );
}
?>
Do not enable SAVEQUERIES on production unless you fully understand the memory impact.
Step 7: Use Query Monitor (Fastest Visual Debugging)
For real-world debugging, Query Monitor is the quickest way to see:
- Queries executed on the request
- Slow queries and duplicates
- Caller information (which theme/plugin triggered it)
- Main query vars and conditionals
Even if you prefer code-based solutions, Query Monitor is the best “X-ray tool” for diagnosis.
Use it to identify the problem, then fix the cause in code.
Common WP_Query Bugs and What to Look For
Pagination Returns Empty Pages
- Missing
paged - Using
offsetwith pagination incorrectly - Custom SQL filters altering
LIMIT
Meta Query Is Slow
- Too many meta clauses (multiple JOINs)
LIKEcomparisons (no index benefit)- Repeater sub-field patterns (ACF) that force wildcard meta keys
Wrong Results in Search
- Custom
posts_searchorposts_wherefilters - Unexpected
post_typerestrictions - Plugins modifying
sbehavior
Practical Debug Workflow
- Confirm which query instance is wrong (main vs custom)
- Log
query_vars - Log
$query->request - Add
posts_clauseslogging to see what changed - Use Query Monitor to identify the caller responsible
- Fix in
pre_get_postsor remove/adjust the problematic filter
Summary
$query->requestis your primary SQL truth sourcepre_get_postsshows how query vars were shapedposts_request/posts_clausesreveal late-stage SQL modificationsSAVEQUERIEShelps find slow queries (staging/local)- Query Monitor is the quickest way to see queries + callers in one place
Once you can see the SQL and identify the hook that changed it, WP_Query debugging becomes mechanical:
observe → isolate → remove or rewrite the filter.
🎨 Want to learn more? Visit our WordPress Customization Hub for tips and advanced techniques.