Skip to content

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 about how SELECTDELETEINSERTREPLACE, 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:

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:

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 or in Query Monitor if it has contributed to degraded performance.

In WordPress Core SQL_CALC_FOUND_ROWS is enabled by default in WP_Query, 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:

$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:

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, found_posts_query or found_users_query can be used to replace SQL_CALC_FOUND_ROWS with a more efficient COUNT(*) query.

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

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:

$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:

/* ❌ 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:

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

Last updated: November 07, 2024

Relevant to

  • WordPress