As WordPress developers, we frequently need to retrieve posts, pages, or other content that matches certain criteria from the WordPress database. Fortunately, we usually don’t need to write raw SQL queries ourselves—in fact, it’s best that we don’t. WordPress provides the WP_Query class, which gives us a safe, efficient way to fetch data. All we need is to define an array of arguments, and the $query object constructs the appropriate SQL for us.

This article assumes you’re already familiar with the basics of the WP_Query class, its methods and properties, and the available parameters you can use.

We’ll look at the optimization-focused parameters of WP_Query—tools specifically designed to reduce SQL query execution times and lower server resource demands.

When your site is small or traffic is light, you might not worry much about query efficiency; WordPress is already pretty well optimized and provides built-in caching. However, as your site—and its traffic—grow, efficiency becomes critical, especially if your database contains thousands of posts.

Our Toolbox

The tips and code examples below have been assessed using Query Monitor, a free plugin that provides valuable insights into query performance, hooks, HTTP requests, rewrite rules, and more.

Alternatively, you can have WordPress save query data by adding the following constant to your wp-config.php file:

define( 'SAVEQUERIES', true );

Setting SAVEQUERIES to true instructs WordPress to keep track of each query and related information in the $wpdb->queries array. You can display this data, including which function made the call and how long each query took, by adding the following code in a template file such as footer.php:

if ( current_user_can( 'administrator' ) ) {
	global $wpdb;
	echo '<pre>';
	print_r( $wpdb->queries );
	echo '</pre>';
}

The output will look something like this:

[4] => Array
(
	[0] => SELECT SQL_CALC_FOUND_ROWS  wp_posts.ID FROM wp_posts  WHERE 1=1  AND wp_posts.post_type = 'post' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'private')  ORDER BY wp_posts.post_date DESC LIMIT 0, 10
	[1] => 0.0163011550903
	[2] => require('wp-blog-header.php'), wp, WP->main, WP->query_posts, WP_Query->query, WP_Query->get_posts, QM_DB->query
	[trace] => QM_Backtrace Object
		( ... )
	[result] => 10
)

For a more in-depth exploration, check out our tutorial on Editing wp-config.php.
Please note: both Query Monitor and SAVEQUERIES are intended solely for development and should never be left active in production environments as they can impact performance and expose sensitive data.

Now, let’s explore some practical ways to make WordPress queries faster.

WP_Query – Why We’d Not Count Rows

To fetch posts, you can use either the get_posts function, which returns an array, or create a new WP_Query instance. The number and type of posts returned depend entirely on the arguments you pass in.

Here’s a typical example of a Loop as you might see it in a template:

// The Query
$the_query = new WP_Query( $args );
// The Loop
if ( $the_query->have_posts() ) {
	while ( $the_query->have_posts() ) : $the_query->the_post(); 
		// Your code here
	endwhile;
} else {
		// no posts found
}
/* Restore original Post Data */
wp_reset_postdata();

The $args array holds key-value pairs (known as query vars) that influence the resulting SQL.
If you’re running custom queries inside a plugin, you might prefer the pre_get_posts filter. Here’s an example:

function myplugin_pre_get_posts( $query ) {
  if ( is_admin() || ! $query->is_main_query() ){
	return;
  }
  $query->set( 'category_name', 'webdev' );
}
add_action( 'pre_get_posts', 'myplugin_pre_get_posts', 1 );

It’s important to point out that $query is passed by reference—any changes directly affect the existing $query instance.

The set method here adds a query var, prompting WordPress to only retrieve posts from the webdev category. Here’s what the resulting SQL query might look like:

SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts 
INNER JOIN wp_term_relationships
ON (wp_posts.ID = wp_term_relationships.object_id)
WHERE 1=1 
AND ( wp_term_relationships.term_taxonomy_id IN (12) )
AND wp_posts.post_type = 'post'
AND (wp_posts.post_status = 'publish'
OR wp_posts.post_status = 'private')
GROUP BY wp_posts.ID
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10

In this example, the LIMIT value is set by the admin user’s Reading options, as depicted in the following screenshot.

Reading Screen

For custom queries, you can set the number of rows to retrieve by using the pagination parameter posts_per_page.

By default, the SQL_CALC_FOUND_ROWS option tells MySQL to count the total number of matched rows, which you can access in WordPress with FOUND_ROWS() as shown below:

SELECT SQL_CALC_FOUND_ROWS * FROM tbl_name
WHERE id > 100 LIMIT 10;

SELECT FOUND_ROWS();

However, using SQL_CALC_FOUND_ROWS can slow down query execution, especially on large datasets.
Luckily, WordPress allows you to skip this functionality with the underused (and undocumented) no_found_rows parameter.

When SQL_CALC_FOUND_ROWS is left out, FOUND_ROWS() only returns the count up to your LIMIT value. For more information, refer to the MySQL documentation.

For example, on an installation with a few hundred posts, running the following meta query took 0.0107 seconds:

SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts 
INNER JOIN wp_postmeta
ON ( wp_posts.ID = wp_postmeta.post_id )
WHERE 1=1 
AND ( ( wp_postmeta.meta_key = 'book_author'
AND CAST(wp_postmeta.meta_value AS CHAR) LIKE '%Isaac Asimov%' ) )
AND wp_posts.post_type = 'book'
AND (wp_posts.post_status = 'publish'
OR wp_posts.post_status = 'private')
GROUP BY wp_posts.ID
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10

When SQL_CALC_FOUND_ROWS was disabled by setting no_found_rows to true, the same query ran in just 0.0006 seconds.

Thanks to Query Monitor plugin, we can easily compare two queries enabling and disabling SQL_CALC_FOUND_ROWS option
With Query Monitor, you can easily compare queries with and without SQL_CALC_FOUND_ROWS enabled.

If your wp_post table contains thousands of entries, execution time can jump to several seconds.
If you do not need pagination, always set no_found_rows to true—this makes queries complete much faster.

To Cache or Not to Cache

WordPress ships with a built-in caching system. While caching usually improves load times, it sometimes triggers additional queries or fetches excess data that may not be necessary for your request.

WordPress allows fine control over query caching with three specific parameters:

  • cache_results: Set to true by default. Determines whether post information gets cached.
  • update_post_meta_cache: Defaults to true. Controls whether to update the cache for post meta.
  • update_post_term_cache: Also defaults to true. Dictates whether to update the post term cache.

If you’re running a persistent object caching solution like Memcached, WordPress will automatically handle these parameters for you in many cases, so manual tweaks are usually unnecessary.

In other scenarios, especially when handling queries that return just a small number of posts, you can improve performance by disabling caching as shown here:

function myplugin_pre_get_posts( $query ) {
  if ( is_admin() || ! $query->is_main_query() ){
	return;
  }
  $query->set( 'category_name', 'webdev' );

  $query->set( 'no_found_rows', true );
  $query->set( 'update_post_meta_cache', false );
  $query->set( 'update_post_term_cache', false );
}
add_action( 'pre_get_posts', 'myplugin_pre_get_posts', 1 );

If persistent caching isn’t set up, you may want to avoid caching for lightweight queries that return only a few rows.

Returned Fields

As a best practice, avoid extracting unnecessary fields from the database. The WP_Query class supports the fields argument, letting you restrict results to just IDs or 'id=>parent' pairs. According to the documentation for fields:

Which fields to return. Single field or all fields (string), or array of fields. ‘id=>parent’ uses ‘id’ and ‘post_parent’. Default all fields. Accepts ‘ids’, ‘id=>parent’.

You can use either 'ids' or 'id=>parent', with the default behavior returning all fields. By default, WordPress sometimes uses ids in its own queries for efficiency.
Here’s how you might optimize your initial query to return only post IDs:

<?php
$args = array( 
	'no_found_rows' => true, 
	'update_post_meta_cache' => false, 
	'update_post_term_cache' => false, 
	'category_name' => 'cms', 
	'fields' => 'ids'
);
// The Query
$the_query = new WP_Query( $args );
$my_posts = $the_query->get_posts();

if( ! empty( $my_posts ) ){
    foreach ( $my_posts as $p ){
        // Your code
    }
}
/* Restore original Post Data */
wp_reset_postdata();
?>

When you don’t need the full post data, set fields to ids to reduce unnecessary data transfer.

Other Essential Optimization Parameters

In addition to the previously discussed parameters, there are several other arguments and practices that can contribute substantially to more efficient and faster WordPress queries:

  • Suppress Filters: The suppress_filters parameter, when set to true, ensures no additional filters (such as pre_get_posts) are applied to your query, preventing unintended modifications. This is useful in critical queries where predictable, unaltered results are required.
  • Specific Column Selection: For highly specialized queries, consider using the fields parameter in combination with custom SQL via get_col or get_var on the $wpdb object. While using WP_Query is preferred for most use-cases, knowing when and how to drop down to the database abstraction layer can sometimes yield further efficiency.
  • Avoid Complex Meta Queries: Complex arguments within meta_query and tax_query can be very slow, especially if not properly indexed. When possible, leverage registered custom fields with indexes or shift complex logic to transient caching, background processes, or external APIs.
  • Join-less Filtering: Avoid unnecessary joins in your SQL by filtering against indexed columns such as post_type, post_status, or post_date before applying more resource-intensive meta_query filters.
$query = new WP_Query( array(
  'posts_per_page'   => 10,
  'suppress_filters' => true,
) );

Examples of Query Optimization in Common Scenarios

Fetching Recent Posts for a Homepage

If your homepage needs only the five newest published posts, keep the query lean:

$args = array(
  'posts_per_page'   => 5,
  'post_status'      => 'publish',
  'no_found_rows'    => true,
  'fields'           => 'ids',
  'cache_results'    => false
);
$query = new WP_Query( $args );

This minimizes overhead and returns only what’s required for further processing or display.

Custom AJAX Search Requests

When powering features like live search/autocomplete via AJAX, it’s critical queries are as fast as possible:

$args = array(
  'posts_per_page'   => 10,
  'post_status'      => 'publish',
  's'                => sanitize_text_field( $_GET['q'] ),
  'fields'           => 'ids',
  'no_found_rows'    => true,
  'cache_results'    => false,
  'update_post_term_cache' => false,
  'update_post_meta_cache' => false
);
$query = new WP_Query( $args );

This approach ensures snappy responses under high traffic, reducing memory and network use.

Measuring and Monitoring Performance

Beyond Query Monitor, consider these strategies for ongoing optimization:

  • Slow Query Logging: Take advantage of MySQL’s slow query log to identify problematic queries in production.
  • Index Analysis: Regularly check that your database tables—especially custom tables and post meta—are appropriately indexed for your most common queries.
  • Real User Monitoring (RUM): Tools like New Relic or built-in application performance monitoring (APM) in hosts can help reveal live query impact on user experience.

Scaling Considerations

As your WordPress site scales, you may need to:

  • Introduce custom database indexes for frequent meta or taxonomy queries.
  • Use background processing (e.g., WP Cron or Action Scheduler) for heavy or non-critical operations.
  • Implement full-page caching (with plugins or server-level solutions) to minimize live queries under load.
  • Periodically clean or archive unused post types, meta fields, and old content to keep database size in check.

Key Takeaways

  • Always tailor WP_Query arguments to return the minimal necessary data.
  • Measure and test queries early—issues grow with your site’s content and audience.
  • Keep development and production configurations separate; profilers are invaluable tools but should not run live.
  • Documentation is your best friend—familiarize yourself with hidden or under-documented parameters for advanced performance gains.

By weaving these best practices into your development workflow, you’ll not only create faster, more resilient WordPress sites but also ensure a better user experience and easier scaling as your audience grows.

Summary

Optimizing query speed may not produce dramatic benefits on small sites, but it’s a smart long-term habit—especially if you plan to grow or manage large, busy WordPress installations. Expensive, poorly optimized queries can slow down your site, but employing just a handful of best practices can provide a major performance boost.

  • Unregistering style variations in a WordPress block theme

    Unregistering style variations in a WordPress block theme

    When you’re creating a custom theme or working with a child theme, there are times when you’ll want to remove or hide particular styling features—whether it’s a single core block or an entire theme style variation. This isn’t always just about personal preference. Removing unused elements can deliver real benefits, including faster performance, more cohesive…

  • Inside a modern WordPress agency tech stack

    Inside a modern WordPress agency tech stack

    Today’s WordPress agencies do so much more than set up plugins or customize themes. These teams are agile and focused, delivering fast, reliable websites, handling complex client demands, and shipping scalable solutions—all while sticking to tight timelines. What enables this efficiency? A carefully chosen, well-integrated tech stack is the critical first step. In this article,…