How to Debug WP_Query (SQL, Hooks, Query Monitor)

January 18, 2026
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_posts or 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 offset with pagination incorrectly
  • Custom SQL filters altering LIMIT

Meta Query Is Slow

  • Too many meta clauses (multiple JOINs)
  • LIKE comparisons (no index benefit)
  • Repeater sub-field patterns (ACF) that force wildcard meta keys

Wrong Results in Search

  • Custom posts_search or posts_where filters
  • Unexpected post_type restrictions
  • Plugins modifying s behavior

Practical Debug Workflow

  1. Confirm which query instance is wrong (main vs custom)
  2. Log query_vars
  3. Log $query->request
  4. Add posts_clauses logging to see what changed
  5. Use Query Monitor to identify the caller responsible
  6. Fix in pre_get_posts or remove/adjust the problematic filter

Summary

  • $query->request is your primary SQL truth source
  • pre_get_posts shows how query vars were shaped
  • posts_request / posts_clauses reveal late-stage SQL modifications
  • SAVEQUERIES helps 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.

Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.