Skip to content

Conversation

@johnbillion
Copy link
Member

@johnbillion johnbillion commented Jan 18, 2023

Trac ticket: https://core.trac.wordpress.org/ticket/47280

This carries on the work started on #330 and #2119. Lazy loading the found_posts and max_num_pages was an interesting exercise but upon discussion with colleagues it's become clear that it'll result in potentially inaccurate counts if either of those properties are accessed after changes have been made to a post that exists in the result set:

$query = WP_Query($args);
wp_update_post(...);
$query->found_posts;

This PR implements the changes from #2119 minus the lazy loading of those two properties. It's fully compatible with the WP_Query query caching that was introduced in WordPress 6.1.

High level changes:

  • Replaces usage of SQL_CALC_FOUND_ROWS and SELECT FOUND_ROWS() with an unbounded COUNT of the query, which is the method recommended by MySQL and is more efficient
  • Retains usage of SELECT FOUND_ROWS() only as a fallback if the query is filtered and a SQL_CALC_FOUND_ROWS modifier inserted (low likelihood but needs to be handled)

Details and todos:

  • Tests for basic queries
  • Tests for empty queries
  • Tests for queries with no results
  • Tests for queries with and without limits and paging
  • Tests for queries with and without no_found_rows
  • Tests for meta queries
  • Tests for taxonomy queries
  • Investigate uses of the found_posts_query filter: https://wpdirectory.net/search/01G2JTDXB9WQ0H1Y4P0VQSFBE3
    • Very low usage overall, most common usage is to no-op the query
    • If the query gets no-oped there's no change to the current behaviour
    • Zero instances of plugins enforcing SELECT FOUND_ROWS()
  • Decide how to handle a change to the query via the posts_request filter (the only filter that runs between the query being constructed and executed) https://wpdirectory.net/search/01G695ZX9RZ03PCDN3C0HFV6A1 . This may be a blocker.
    • If it's overridden and still contains SQL_CALC_FOUND_ROWS, need to retain the use of an immediate call to SELECT FOUND_ROWS().
      • Tests
      • Call _deprecated_argument()
    • If no_found_rows is true then this is not a concern because the count query doesn't run
    • If any other substring replacement is applied then the result of the count query is likely to be inaccurate. Plugins that use this filter:
      • "NextGEN Gallery" allows complete replacement of the query
      • "bbPress" adjusts the FROM and WHERE clauses
      • "XML Sitemap & Google News" sets the query to boolean false
      • "YARPP" and "Real Media Library" can override the query with a completely different one
      • "Relevanssi" and "Advanced Woo Search" can essentially no-op the query to return no results
      • "Contextual Related Posts" can insert a HAVING clause
  • Determine if the DISTINCT clause can be removed from the count query
    • DISTINCT is not used by core but could be used by plugins via the various clause filters
    • GROUP BY can be triggered by core, eg via a meta query with two or more clauses, an OR relation, and resulting posts that match more than one meta clause -- tests have been added to cover this
    • Therefore, the DISTINCT clause in the COUNT() query should remain
    • Tests have been added for this
  • Re-run and publish performance figures for this change
  • Test compatibility with db.php dropins:
    • W3 Total Cache
    • LudicrousDB
    • HyperDB
    • Query Monitor

Plugin authors:

If you're the maintainer of a plugin which makes changes to the way WordPress queries for posts, counts results, or uses the found_posts_query or posts_request filter, please test your plugin with this change in place.

Other query classes

Once WP_Query is complete, the process can be repeated for all query classes that can perform count queries:

  • WP_Network_Query
  • WP_Site_Query
  • WP_Comment_Query
  • WP_User_Query

@johnbillion johnbillion marked this pull request as ready for review January 18, 2023 20:19
@johnbillion johnbillion changed the title #47280 Remove usage of deprecated MySQL SQL_CALC_FOUND_ROWS #47280 Remove usage of deprecated MySQL SQL_CALC_FOUND_ROWS from WP_Query Jan 31, 2023

$this->request = $old_request;
$this->count_request = "
SELECT COUNT(DISTINCT {$count_field})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't DISTINCT be $distinct. Adding when its not needed results in more work for the DB.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you're counting "found rows" here, you really don't want it to be distinct (as distinct rows <= actual rows). So technically DISTINCT should be omitted.

{$count_field} could be left as a * literal like the MySQL documents suggest, so if a $where clause exist, to choose from whatever available index suites the query best.

Look at the explain plan for when there's a WHERE clause that can use an index. Might not make a difference. Also see explain plain when a JOIN is added.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding DISTINCT, I'll check this again to be 100% sure but I think this needs to remain in place because some queries can result in duplicate rows being returned. These tests cover that scenario:

  • test_found_posts_are_correct_for_OR_meta_queries()
  • test_found_posts_are_correct_for_group_by_queries()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this again and SELECT COUNT(DISTINCT {$count_field}) is indeed needed for the scenarios covered by the two tests above. If there was a way to only add the DISTINCT clause when we knew we needed it that would be great, but I'm not too confident about that.

Copy link

@kkataria3010 kkataria3010 May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnbillion Not testing the code. But it looks like DISTINCT is needed only when groupby field is used.

Copy link
Contributor

@peterwilsoncc peterwilsoncc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a few notes inline but nothing major.

Are you able to set up a PR against trunk with the tests to ensure the suite passes on trunk with the exception of the SQL_CALC_FOUND_ROWS related assertions? This can wait if you've still got some wrapping up of this PR to do.

* @param string $fields Value of the `fields` argument for `WP_Query`.
*/
public function test_found_posts_are_correct_for_basic_query( $fields ) {
self::factory()->post->create_many( 5 );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears multiple times, is it possible to create it as a shared fixture. It might need to be a CPT to avoid messing up the assertions elsewhere in this file.

johnbillion and others added 5 commits February 22, 2023 16:18
Co-authored-by: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com>
Co-authored-by: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com>
@spacedmonkey spacedmonkey self-requested a review March 14, 2023 17:14
* Filters the query to run for retrieving the found posts.
*
* @since 2.1.0
* @since x.x.x This query was changed from `SELECT FOUND_ROWS()` to a more
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought, I wonder if we could make it so we can call set_found_posts by itself with the rest of the call. We have examples in core where we get one post to get the count see.

* @param string $request The complete SQL query.
* @param WP_Query $query The WP_Query instance (passed by reference).
*/
$this->request = apply_filters_ref_array( 'posts_request', array( $this->request, &$this ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnbillion We also need to check the posts_request_ids filter as well.

Comment on lines +3132 to +3145
if ( is_string( $this->request ) && str_contains( $this->request, 'SQL_CALC_FOUND_ROWS' ) ) {
_deprecated_argument(
'The posts_request filter',
'x.x.x',
sprintf(
/* translators: 1: SQL query modifier 2: SQL query */
__( 'The %1$s query modifier should no longer be added to queries because results are no longer counted with %2$s by default.' ),
'<code>SQL_CALC_FOUND_ROWS</code>',
'<code>SELECT FOUND_ROWS()</code>'
)
);

$this->count_request = 'SELECT FOUND_ROWS()';
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we are checking both posts_request_ids and posts_request filters, let's make this generic, reusable and make it into it's own method. This will also make it more testable.

$count_field = "{$wpdb->posts}.ID";

if ( ! empty( $groupby ) ) {
$count_field = $groupby;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love some unit tests for some weird and wonder groupby values.

@spacedmonkey
Copy link
Member

With 10k+ posts

Before
Screenshot 2023-07-18 at 16 48 01

After
Screenshot 2023-07-18 at 16 48 11

@spacedmonkey
Copy link
Member

Maybe related - https://bugs.mysql.com/bug.php?id=21849

@archon810
Copy link

Any updates here please? SQL_CALC_FOUND_ROWS is twice as slow as count+select in our use cases and would mean massive performance improvements. https://core.trac.wordpress.org/ticket/47280 is almost at the finish line.

@mibmo
Copy link

mibmo commented May 31, 2024

what's blocking here? i'd love to help fix this.

@github-actions
Copy link

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @grooverdan, @archon810, @mibmo.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

Core Committers: Use this line as a base for the props when committing in SVN:

Props johnbillion, peterwilsoncc, spacedmonkey.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants