Make WordPress Core

Changeset 58122


Ignore:
Timestamp:
05/08/2024 10:49:46 PM (3 weeks ago)
Author:
spacedmonkey
Message:

Query: Improve cache key generation.

Query caching in WP_Query was added in [53941]. When this functionality was added to the WP_Query class, a number of edge cases were missed that would result in redundant duplicate queries. It was possible to pass parameters to WP_Query that would be different but would result in the same database query being generated. As the cache key is generated from a mixture of query arguments and the SQL query, this resulted in different cache keys for the same database query, resulting in unnecessary duplicate queries. In this change, the logic in the generate_cache_key method has been improved to ensure reuse of existing caches. The following edge cases have been considered:

  • Passing post_type as an empty string.
  • Passing post_type as the string any.
  • Passing post_type as array vs. string.
  • Passing post_type as an array in a different order.
  • Not passing orderby.
  • Passing post_status as an array vs. string.
  • Passing post_status as an array in a different order.

This change also fixes an issue where the old SQL query would not match, as the queries had different whitespaces.

Props spacedmonkey, joemcgill, pbearne, peterwilsoncc, rajinsharwar, mukesh27, thekt12, huzaifaalmesbah, rodionov201.
Fixes #59442.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-query.php

    r57761 r58122  
    22652265                } elseif ( count( $post_type ) === 1 ) {
    22662266                    $post_type = $post_type[0];
     2267                } else {
     2268                    // Sort post types to ensure same cache key generation.
     2269                    sort( $post_type );
    22672270                }
    22682271
     
    25432546            }
    25442547        } elseif ( ! empty( $post_type ) && is_array( $post_type ) ) {
     2548            // Sort post types to ensure same cache key generation.
     2549            sort( $post_type );
    25452550            $post_type_where = " AND {$wpdb->posts}.post_type IN ('" . implode( "', '", esc_sql( $post_type ) ) . "')";
    25462551        } elseif ( ! empty( $post_type ) ) {
     
    26482653
    26492654            if ( ! empty( $queried_post_types ) ) {
    2650 
     2655                sort( $queried_post_types );
    26512656                $status_type_clauses = array();
    26522657
     
    31053110        }
    31063111
    3107         // Beginning of the string is on a new line to prevent leading whitespace. See https://core.trac.wordpress.org/ticket/56841.
     3112        /*
     3113         * Beginning of the string is on a new line to prevent leading whitespace.
     3114         *
     3115         * The additional indentation of subsequent lines is to ensure the SQL
     3116         * queries are identical to those generated when splitting queries. This
     3117         * improves caching of the query by ensuring the same cache key is
     3118         * generated for the same database queries functionally.
     3119         *
     3120         * See https://core.trac.wordpress.org/ticket/56841.
     3121         * See https://github.com/WordPress/wordpress-develop/pull/6393#issuecomment-2088217429
     3122         */
    31083123        $old_request =
    31093124            "SELECT $found_rows $distinct $fields
    3110             FROM {$wpdb->posts} $join
    3111             WHERE 1=1 $where
    3112             $groupby
    3113             $orderby
    3114             $limits";
     3125                    FROM {$wpdb->posts} $join
     3126                    WHERE 1=1 $where
     3127                    $groupby
     3128                    $orderby
     3129                    $limits";
    31153130
    31163131        $this->request = $old_request;
     
    48574872        );
    48584873
     4874        if ( empty( $args['post_type'] ) ) {
     4875            if ( $this->is_attachment ) {
     4876                $args['post_type'] = 'attachment';
     4877            } elseif ( $this->is_page ) {
     4878                $args['post_type'] = 'page';
     4879            } else {
     4880                $args['post_type'] = 'post';
     4881            }
     4882        } elseif ( 'any' === $args['post_type'] ) {
     4883            $args['post_type'] = array_values( get_post_types( array( 'exclude_from_search' => false ) ) );
     4884        }
     4885        $args['post_type'] = (array) $args['post_type'];
     4886        // Sort post types to ensure same cache key generation.
     4887        sort( $args['post_type'] );
     4888
     4889        if ( isset( $args['post_status'] ) ) {
     4890            $args['post_status'] = (array) $args['post_status'];
     4891            // Sort post status to ensure same cache key generation.
     4892            sort( $args['post_status'] );
     4893        }
     4894
     4895        // Add a default orderby value of date to ensure same cache key generation.
     4896        if ( ! isset( $q['orderby'] ) ) {
     4897            $args['orderby'] = 'date';
     4898        }
     4899
    48594900        $placeholder = $wpdb->placeholder_escape();
    48604901        array_walk_recursive(
     
    48754916        );
    48764917
     4918        ksort( $args );
     4919
    48774920        // Replace wpdb placeholder in the SQL statement used by the cache key.
    48784921        $sql = $wpdb->remove_placeholder_escape( $sql );
  • trunk/tests/phpunit/tests/query/cacheResults.php

    r56925 r58122  
    170170
    171171        $this->assertSame( $cache_key_1, $cache_key_2, 'Cache key differs when using wpdb placeholder.' );
     172    }
     173
     174    /**
     175     * @covers WP_Query::generate_cache_key
     176     * @ticket 59442
     177     */
     178    public function test_generate_cache_key_unregister_post_type() {
     179        global $wpdb;
     180        register_post_type(
     181            'wptests_pt',
     182            array(
     183                'exclude_from_search' => false,
     184            )
     185        );
     186        $query_vars = array(
     187            'post_type' => 'any',
     188        );
     189        $fields     = "{$wpdb->posts}.ID";
     190        $query1     = new WP_Query( $query_vars );
     191        $request1   = str_replace( $fields, "{$wpdb->posts}.*", $query1->request );
     192
     193        $reflection = new ReflectionMethod( $query1, 'generate_cache_key' );
     194        $reflection->setAccessible( true );
     195
     196        $cache_key_1 = $reflection->invoke( $query1, $query_vars, $request1 );
     197        unregister_post_type( 'wptests_pt' );
     198        $cache_key_2 = $reflection->invoke( $query1, $query_vars, $request1 );
     199
     200        $this->assertNotSame( $cache_key_1, $cache_key_2, 'Cache key should differ after unregistering post type.' );
     201    }
     202
     203    /**
     204     * @ticket 59442
     205     *
     206     * @covers WP_Query::generate_cache_key
     207     *
     208     * @dataProvider data_query_cache_duplicate
     209     */
     210    public function test_generate_cache_key_normalize( $query_vars1, $query_vars2 ) {
     211        global $wpdb;
     212
     213        $fields   = "{$wpdb->posts}.ID";
     214        $query1   = new WP_Query( $query_vars1 );
     215        $request1 = str_replace( $fields, "{$wpdb->posts}.*", $query1->request );
     216
     217        $query2   = new WP_Query( $query_vars2 );
     218        $request2 = str_replace( $fields, "{$wpdb->posts}.*", $query2->request );
     219
     220        $reflection = new ReflectionMethod( $query1, 'generate_cache_key' );
     221        $reflection->setAccessible( true );
     222
     223        $this->assertSame( $request1, $request2, 'Queries should match' );
     224
     225        $cache_key_1 = $reflection->invoke( $query1, $query_vars1, $request1 );
     226        $cache_key_2 = $reflection->invoke( $query1, $query_vars2, $request2 );
     227
     228        $this->assertSame( $cache_key_1, $cache_key_2, 'Cache key differs the same paramters.' );
    172229    }
    173230
     
    218275
    219276    /**
     277     * Data provider for test_generate_cache_key_normalize().
     278     *
     279     * @return array[]
     280     */
     281    public function data_query_cache_duplicate() {
     282        return array(
     283            'post type empty'           => array(
     284                'query_vars1' => array( 'post_type' => '' ),
     285                'query_vars2' => array( 'post_type' => 'post' ),
     286            ),
     287            'post type array'           => array(
     288                'query_vars1' => array( 'post_type' => array( 'page' ) ),
     289                'query_vars2' => array( 'post_type' => 'page' ),
     290            ),
     291            'orderby empty'             => array(
     292                'query_vars1' => array( 'orderby' => null ),
     293                'query_vars2' => array( 'orderby' => 'date' ),
     294            ),
     295            'different order parameter' => array(
     296                'query_vars1' => array(
     297                    'post_type'      => 'post',
     298                    'posts_per_page' => 15,
     299                ),
     300                'query_vars2' => array(
     301                    'posts_per_page' => 15,
     302                    'post_type'      => 'post',
     303                ),
     304            ),
     305            'same args'                 => array(
     306                'query_vars1' => array( 'post_type' => 'post' ),
     307                'query_vars2' => array( 'post_type' => 'post' ),
     308            ),
     309            'same args any'             => array(
     310                'query_vars1' => array( 'post_type' => 'any' ),
     311                'query_vars2' => array( 'post_type' => 'any' ),
     312            ),
     313            'any and post types'        => array(
     314                'query_vars1' => array( 'post_type' => 'any' ),
     315                'query_vars2' => array( 'post_type' => array( 'post', 'page', 'attachment' ) ),
     316            ),
     317            'different order post type' => array(
     318                'query_vars1' => array( 'post_type' => array( 'post', 'page' ) ),
     319                'query_vars2' => array( 'post_type' => array( 'page', 'post' ) ),
     320            ),
     321            'post status array'         => array(
     322                'query_vars1' => array( 'post_status' => 'publish' ),
     323                'query_vars2' => array( 'post_status' => array( 'publish' ) ),
     324            ),
     325            'post status order'         => array(
     326                'query_vars1' => array( 'post_status' => array( 'draft', 'publish' ) ),
     327                'query_vars2' => array( 'post_status' => array( 'publish', 'draft' ) ),
     328            ),
     329            'cache parameters'          => array(
     330                'query_vars1' => array(
     331                    'update_post_meta_cache' => true,
     332                    'update_post_term_cache' => true,
     333                    'update_menu_item_cache' => true,
     334                ),
     335                'query_vars2' => array(
     336                    'update_post_meta_cache' => false,
     337                    'update_post_term_cache' => false,
     338                    'update_menu_item_cache' => false,
     339                ),
     340            ),
     341        );
     342    }
     343
     344    /**
    220345     * Data provider.
    221346     *
     
    262387                    'cache_results' => true,
    263388                    'post_type'     => 'page',
     389                ),
     390            ),
     391            'cache true and empty post type'              => array(
     392                'args' => array(
     393                    'cache_results' => true,
     394                    'post_type'     => '',
     395                ),
     396            ),
     397            'cache true and orderby null'                 => array(
     398                'args' => array(
     399                    'cache_results' => true,
     400                    'orderby'       => null,
    264401                ),
    265402            ),
Note: See TracChangeset for help on using the changeset viewer.