Title: Optimize core queries at scale
Author: WordPress VIP Documentation
Published: March 23, 2021
Last modified: December 29, 2025

---

 1. [Databases](https://docs.wpvip.com/databases/)
 2. [Optimize database queries](https://docs.wpvip.com/databases/optimize-queries/)
 3. Optimize core queries at scale

#  Optimize core queries at scale

Performance issues can be caused by core WordPress queries that perform well when
a site has a small amount of data, but as the volume of site content increases the
queries require a longer amount of time to execute.

The increased amount of time required by the queries can go unnoticed during normal
amounts of site traffic, but when the amount of traffic increases—or a single bot
crawls many uncached pages—the inefficiency of the queries can cause the site to
experience performance issues.

For example, if a query targets a database table large enough to contain 700,000
published posts, sorting that volume of posts by date or category cannot be performed
in memory. Instead, the database must use a filesort. A filesort copies the list
of posts to a temporary table in a physical file on disk and then a sort is run 
on the file. This process can be very slow, and it can slow down other database 
operations that also need to use a filesort.

Sorting operations are often the most expensive part of a query. To make a query
more efficient, the amount of data that is retrieved before the sort operation should
be reduced as much as possible.

For example, if a query is intended to retrieve only the five most recent posts 
in the “News” category, sorting through all 700,000 published posts in the database
is unnecessary. Instead, the query should be limited to a reasonable range of time
that might include the five most recent posts from that category, and the results
within that range can then be sorted (e.g. newest to oldest).

## Analyze the efficiency of a query with EXPLAIN

[The MySQL `EXPLAIN` statement provides more detailed information](https://dev.mysql.com/doc/refman/8.0/en/using-explain.html)
about how `SELECT`, `DELETE`, `INSERT`, `REPLACE`, and `UPDATE` statements are executed
by MySQL. The information returned by `EXPLAIN` can help to reveal the inefficient
aspects of a database query that can and should be optimized.

As an example, using an `EXPLAIN` statement to analyze an example query that is 
intended to retrieve the five most recent posts in the “News” category reveals that
the operation is sorting through 677,527 rows:

![](https://docs.wpvip.com/wp-content/uploads/sites/2/2021/03/image-6.png?w=1024)

In this example, the `EXPLAIN` statement also reveals that there is an index named`
type_status_date` on the posts table (for type + status + date), but that index 
was not used to narrow down the recent posts. In its current state, the query is
using the `term_taxonomy_id` index in `wp_term_relationships` to retrieve all posts
in the “News” category, sifting them (by `type` and `status`), then sorting them
by `date`. This order of operations is expensive and slow to perform on such an 
enormous amount of data.

### Optimize the query

If a date constraint is added to the query in code—for example by limiting the query
to only posts published in the last week—the SQL query will use the post index `
type_status_date` first. Running the operation against the `type_status_date` index
first for a specific range of time will greatly reduce the number of rows to sort
through and retrieve posts that match the `post` type and the `publish` status. 
The `term_taxonomy_id` index can then be used to identify the posts in the “News”
category and select the most recent five.

An example of the `EXPLAIN` statement for the improved version of the query reveals
that now only 5,825 rows are being sorted:

![](https://docs.wpvip.com/wp-content/uploads/sites/2/2021/03/image-7.png?w=1024)

This improved version of the query, even if a temporary table is used, requires 
much less disk space and time to sort a much smaller number of items.

## Pagination queries and `SQL_CALC_FOUND_ROWS` performance

`SQL_CALC_FOUND_ROWS` is a MySQL query modifier that calculates the total number
of rows that will be returned by a query without the `LIMIT` clause. In WordPress,
the total number of found rows is then retrieved by `FOUND_ROWS()` to provide the
necessary data for pagination calculations.

For large datasets, `SQL_CALC_FOUND_ROWS` can result in slower queries and degraded
site performance, particularly during high traffic events. References to `SQL_CALC_FOUND_ROWS`
will appear in [an environment’s Slow Query Logs](https://docs.wpvip.com/logs/slow-query/)
or in [Query Monitor](https://docs.wpvip.com/performance/query-monitor/) if it has
contributed to degraded performance.

In WordPress Core `SQL_CALC_FOUND_ROWS` is [enabled by default in `WP_Query`](https://github.com/WordPress/WordPress/blob/a88967db6eb6a95da6d320d8511f78d00b1ffafd/wp-includes/class-wp-query.php#L3108),
but can be disabled in code if pagination is not needed.

### Disable pagination

If pagination is not required for a WordPress site, `SQL_CALC_FOUND_ROWS` can be
disabled with the `no_found_rows` argument.

In this code example, `no_found_rows` argument is set to `true` in a custom new `
WP_Query`:

    ```lang-php
    $args = new WP_Query( array(
        'post_type'           => 'post',
        'posts_per_page'      => 10,
        'no_found_rows'       => true, // Disable SQL_CALC_FOUND_ROWS
    	'post_status'         => 'publish',
    	'ignore_sticky_posts' => true,
    ) );

    $posts = new WP_Query( $args );
    ```

### Disable pagination for core queries

The `no_found_rows` argument can also be used to selectively code-disable `SQL_CALC_FOUND_ROWS`
for core queries.

In this code example, pagination queries are disabled on core queries on the single
post page:

    ```lang-php
    function disable_pagination_on_single_pages( $query ) {
        // Check if it's the main query, on the front end, and a single post/page
        if ( $query->is_main_query() && !is_admin() && ( $query->is_singular() ) ) {
            // Disable the SQL_CALC_FOUND_ROWS query
            $query->set( 'no_found_rows', true );
        }
    }
    add_action( 'pre_get_posts', 'disable_pagination_on_single_pages' );
    ```

**Note**

Do not attempt to disable pagination queries by manipulating the query string and
stripping `SQL_CALC_FOUND_ROWS` with `str_replace`. This approach will not work 
as expected and will trigger the following warning when `FOUND_ROWS()` runs:

`WordPress database error Attempted SELECT FOUND_ROWS() without prior SQL_CALC_FOUND_ROWS
for query SELECT FOUND_ROWS()`

### Alternate pagination queries

If pagination is required—but `SQL_CALC_FOUND_ROWS` needs to be avoided to improve
site performance—alternative methods can be used to perform the calculations for
pagination. For example, the `found_posts_query` or `found_users_query` filters 
can be used to replace `SQL_CALC_FOUND_ROWS` with a more efficient `COUNT(*)` query.

However, it is not necessarily true that `COUNT(*)` will outperform `SQL_CALC_FOUND_ROWS`.
It is advisable to [test the query with EXPLAIN](https://docs.wpvip.com/databases/optimize-queries/optimize-core-queries-at-scale/#0-analyze-the-efficiency-of-a-query-with-explain)
to make sure that making this change will improve the performance of the query.

In this code example `SQL_CALC_FOUND_ROWS` is replaced by `found_users_query` to
perform a user count query:

    ```lang-php
    function alternative_user_found_rows( $value, $user_query ){
      $sql = "SELECT COUNT(*) AS TOTAL $user_query->query_from $user_query->query_where";
      return $sql;
    }

    add_filter( 'found_users_query', 'alternative_user_found_rows', 10, 2 );
    ```

## Avoid duplicate queries

Duplicate queries should be avoided whenever possible. A common mistake that causes
a duplicate query is using the `get_posts()` method to retrieve the `posts` property
from a `WP_Query` object.

In this code example, a new `WP_Query` object is instantiated:

    ```lang-php
    $args = [
      'posts_per_page'      => '10',
      'post_type'           => 'post',
      'post_status'         => 'publish',
      'no_found_rows'       => true,
      'ignore_sticky_posts' => true,
      'cat'                 => 123
    ];
    $my_query = new WP_Query( $args );
    ```

If `get_posts()` is called to retrieve posts outside of the constructor, it will
result in a second, duplicate query and negatively affect performance:

    ```lang-php
    /* ❌ This approach is incorrect. */
    $my_posts = $my_query->get_posts();
    ```

Instead, the posts should be retrieved by accessing the `posts` property of the `
WP_Query` object:

    ```lang-php
    /* ✅ This approach is correct. */
    $my_posts = $my_query->posts;
    ```

Last updated: December 29, 2025