Make WordPress Core


Ignore:
Timestamp:
02/06/2025 05:02:17 AM (3 months ago)
Author:
peterwilsoncc
Message:

Query: Increase WP_Query cache hits for equivalent arguments.

Introduces normalization a number of arguments passed to WP_Query to increase cache hits for equivalent requests. For example author__in => [ 1, 2 ] and author__in => [ 2, 1 ] will now hit the same cache.

Prior to generating the SQL request and cache key, the following are sorted, made unique and type cast as appropriate.

  • post_type when passed as an array
  • post_status when passed as an array
  • term_querys containing terms
  • cat
  • category__in
  • category__not_in
  • category__and
  • tag_slug__in
  • tag__in
  • tag__not_in
  • tag__and
  • tag_slug__in
  • tag_slug__and
  • post_parent__not_in
  • author
  • author__not_in
  • author__in

The following are sorted for the purposes of generating the cache key and SQL WHERE clause but unmodified for use in the ORDER BY SQL clause:

  • post_name__in
  • post__in
  • post_parent__in

This commit includes changes to unrelated tests, assertions in Tests_Query_ParseQuery::test_parse_query_cat_array_mixed() and WP_Test_REST_Posts_Controller::test_get_items_not_sticky_with_exclude() have been modified to account for the sorting of the items above.

Props thekt12, peterwilsoncc, spacedmonkey, joemcgill, flixos90, mukesh27, pbearne, swissspidy.
Fixes #59516.

File:
1 edited

Legend:

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

    r59495 r59766  
    474474
    475475    private $compat_methods = array( 'init_query_flags', 'parse_tax_query' );
     476
     477    /**
     478     * The cache key generated by the query.
     479     *
     480     * The cache key is generated by the method ::generate_cache_key() after the
     481     * query has been normalized.
     482     *
     483     * @var string
     484     */
     485    private $query_cache_key = '';
    476486
    477487    /**
     
    11021112        if ( ! empty( $qv['post_type'] ) ) {
    11031113            if ( is_array( $qv['post_type'] ) ) {
    1104                 $qv['post_type'] = array_map( 'sanitize_key', $qv['post_type'] );
     1114                $qv['post_type'] = array_map( 'sanitize_key', array_unique( $qv['post_type'] ) );
     1115                sort( $qv['post_type'] );
    11051116            } else {
    11061117                $qv['post_type'] = sanitize_key( $qv['post_type'] );
     
    11101121        if ( ! empty( $qv['post_status'] ) ) {
    11111122            if ( is_array( $qv['post_status'] ) ) {
    1112                 $qv['post_status'] = array_map( 'sanitize_key', $qv['post_status'] );
     1123                $qv['post_status'] = array_map( 'sanitize_key', array_unique( $qv['post_status'] ) );
     1124                sort( $qv['post_status'] );
    11131125            } else {
    11141126                $qv['post_status'] = preg_replace( '|[^a-z0-9_,-]|', '', $qv['post_status'] );
     
    11831195                $term = $q[ $t->query_var ];
    11841196
    1185                 if ( is_array( $term ) ) {
    1186                     $term = implode( ',', $term );
    1187                 }
     1197                if ( ! is_array( $term ) ) {
     1198                    $term = explode( ',', $term );
     1199                    $term = array_map( 'trim', $term );
     1200                }
     1201                sort( $term );
     1202                $term = implode( ',', $term );
    11881203
    11891204                if ( str_contains( $term, '+' ) ) {
     
    12211236            $cat_array = preg_split( '/[,\s]+/', urldecode( $q['cat'] ) );
    12221237            $cat_array = array_map( 'intval', $cat_array );
    1223             $q['cat']  = implode( ',', $cat_array );
     1238            sort( $cat_array );
     1239            $q['cat'] = implode( ',', $cat_array );
    12241240
    12251241            foreach ( $cat_array as $cat ) {
     
    12631279        if ( ! empty( $q['category__in'] ) ) {
    12641280            $q['category__in'] = array_map( 'absint', array_unique( (array) $q['category__in'] ) );
    1265             $tax_query[]       = array(
     1281            sort( $q['category__in'] );
     1282            $tax_query[] = array(
    12661283                'taxonomy'         => 'category',
    12671284                'terms'            => $q['category__in'],
     
    12731290        if ( ! empty( $q['category__not_in'] ) ) {
    12741291            $q['category__not_in'] = array_map( 'absint', array_unique( (array) $q['category__not_in'] ) );
     1292            sort( $q['category__not_in'] );
    12751293            $tax_query[]           = array(
    12761294                'taxonomy'         => 'category',
     
    12831301        if ( ! empty( $q['category__and'] ) ) {
    12841302            $q['category__and'] = array_map( 'absint', array_unique( (array) $q['category__and'] ) );
    1285             $tax_query[]        = array(
     1303            sort( $q['category__and'] );
     1304            $tax_query[] = array(
    12861305                'taxonomy'         => 'category',
    12871306                'terms'            => $q['category__and'],
     
    13011320        if ( '' !== $q['tag'] && ! $this->is_singular && $this->query_vars_changed ) {
    13021321            if ( str_contains( $q['tag'], ',' ) ) {
     1322                // @todo Handle normalizing `tag` query string.
    13031323                $tags = preg_split( '/[,\r\n\t ]+/', $q['tag'] );
    13041324                foreach ( (array) $tags as $tag ) {
    13051325                    $tag                 = sanitize_term_field( 'slug', $tag, 0, 'post_tag', 'db' );
    13061326                    $q['tag_slug__in'][] = $tag;
     1327                    sort( $q['tag_slug__in'] );
    13071328                }
    13081329            } elseif ( preg_match( '/[+\r\n\t ]+/', $q['tag'] ) || ! empty( $q['cat'] ) ) {
     
    13151336                $q['tag']            = sanitize_term_field( 'slug', $q['tag'], 0, 'post_tag', 'db' );
    13161337                $q['tag_slug__in'][] = $q['tag'];
     1338                sort( $q['tag_slug__in'] );
    13171339            }
    13181340        }
     
    13281350        if ( ! empty( $q['tag__in'] ) ) {
    13291351            $q['tag__in'] = array_map( 'absint', array_unique( (array) $q['tag__in'] ) );
    1330             $tax_query[]  = array(
     1352            sort( $q['tag__in'] );
     1353            $tax_query[] = array(
    13311354                'taxonomy' => 'post_tag',
    13321355                'terms'    => $q['tag__in'],
     
    13361359        if ( ! empty( $q['tag__not_in'] ) ) {
    13371360            $q['tag__not_in'] = array_map( 'absint', array_unique( (array) $q['tag__not_in'] ) );
     1361            sort( $q['tag__not_in'] );
    13381362            $tax_query[]      = array(
    13391363                'taxonomy' => 'post_tag',
     
    13451369        if ( ! empty( $q['tag__and'] ) ) {
    13461370            $q['tag__and'] = array_map( 'absint', array_unique( (array) $q['tag__and'] ) );
    1347             $tax_query[]   = array(
     1371            sort( $q['tag__and'] );
     1372            $tax_query[] = array(
    13481373                'taxonomy' => 'post_tag',
    13491374                'terms'    => $q['tag__and'],
     
    13541379        if ( ! empty( $q['tag_slug__in'] ) ) {
    13551380            $q['tag_slug__in'] = array_map( 'sanitize_title_for_query', array_unique( (array) $q['tag_slug__in'] ) );
    1356             $tax_query[]       = array(
     1381            sort( $q['tag_slug__in'] );
     1382            $tax_query[] = array(
    13571383                'taxonomy' => 'post_tag',
    13581384                'terms'    => $q['tag_slug__in'],
     
    13631389        if ( ! empty( $q['tag_slug__and'] ) ) {
    13641390            $q['tag_slug__and'] = array_map( 'sanitize_title_for_query', array_unique( (array) $q['tag_slug__and'] ) );
    1365             $tax_query[]        = array(
     1391            sort( $q['tag_slug__and'] );
     1392            $tax_query[] = array(
    13661393                'taxonomy' => 'post_tag',
    13671394                'terms'    => $q['tag_slug__and'],
     
    21872214        } elseif ( is_array( $q['post_name__in'] ) && ! empty( $q['post_name__in'] ) ) {
    21882215            $q['post_name__in'] = array_map( 'sanitize_title_for_query', $q['post_name__in'] );
    2189             $post_name__in      = "'" . implode( "','", $q['post_name__in'] ) . "'";
    2190             $where             .= " AND {$wpdb->posts}.post_name IN ($post_name__in)";
     2216            // Duplicate array before sorting to allow for the orderby clause.
     2217            $post_name__in_for_where = array_unique( $q['post_name__in'] );
     2218            sort( $post_name__in_for_where );
     2219            $post_name__in = "'" . implode( "','", $post_name__in_for_where ) . "'";
     2220            $where        .= " AND {$wpdb->posts}.post_name IN ($post_name__in)";
    21912221        }
    21922222
     
    22002230            $where .= " AND {$wpdb->posts}.ID = " . $q['p'];
    22012231        } elseif ( $q['post__in'] ) {
    2202             $post__in = implode( ',', array_map( 'absint', $q['post__in'] ) );
     2232            // Duplicate array before sorting to allow for the orderby clause.
     2233            $post__in_for_where = $q['post__in'];
     2234            $post__in_for_where = array_unique( array_map( 'absint', $post__in_for_where ) );
     2235            sort( $post__in_for_where );
     2236            $post__in = implode( ',', array_map( 'absint', $post__in_for_where ) );
    22032237            $where   .= " AND {$wpdb->posts}.ID IN ($post__in)";
    22042238        } elseif ( $q['post__not_in'] ) {
     2239            sort( $q['post__not_in'] );
    22052240            $post__not_in = implode( ',', array_map( 'absint', $q['post__not_in'] ) );
    22062241            $where       .= " AND {$wpdb->posts}.ID NOT IN ($post__not_in)";
     
    22102245            $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_parent = %d ", $q['post_parent'] );
    22112246        } elseif ( $q['post_parent__in'] ) {
    2212             $post_parent__in = implode( ',', array_map( 'absint', $q['post_parent__in'] ) );
     2247            // Duplicate array before sorting to allow for the orderby clause.
     2248            $post_parent__in_for_where = $q['post_parent__in'];
     2249            $post_parent__in_for_where = array_unique( array_map( 'absint', $post_parent__in_for_where ) );
     2250            sort( $post_parent__in_for_where );
     2251            $post_parent__in = implode( ',', array_map( 'absint', $post_parent__in_for_where ) );
    22132252            $where          .= " AND {$wpdb->posts}.post_parent IN ($post_parent__in)";
    22142253        } elseif ( $q['post_parent__not_in'] ) {
     2254            sort( $q['post_parent__not_in'] );
    22152255            $post_parent__not_in = implode( ',', array_map( 'absint', $q['post_parent__not_in'] ) );
    22162256            $where              .= " AND {$wpdb->posts}.post_parent NOT IN ($post_parent__not_in)";
     
    23422382            $q['author'] = addslashes_gpc( '' . urldecode( $q['author'] ) );
    23432383            $authors     = array_unique( array_map( 'intval', preg_split( '/[,\s]+/', $q['author'] ) ) );
     2384            sort( $authors );
    23442385            foreach ( $authors as $author ) {
    23452386                $key         = $author > 0 ? 'author__in' : 'author__not_in';
     
    23502391
    23512392        if ( ! empty( $q['author__not_in'] ) ) {
    2352             $author__not_in = implode( ',', array_map( 'absint', array_unique( (array) $q['author__not_in'] ) ) );
     2393            if ( is_array( $q['author__not_in'] ) ) {
     2394                $q['author__not_in'] = array_unique( array_map( 'absint', $q['author__not_in'] ) );
     2395                sort( $q['author__not_in'] );
     2396            }
     2397            $author__not_in = implode( ',', (array) $q['author__not_in'] );
    23532398            $where         .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
    23542399        } elseif ( ! empty( $q['author__in'] ) ) {
     2400            if ( is_array( $q['author__in'] ) ) {
     2401                $q['author__in'] = array_unique( array_map( 'absint', $q['author__in'] ) );
     2402                sort( $q['author__in'] );
     2403            }
    23552404            $author__in = implode( ',', array_map( 'absint', array_unique( (array) $q['author__in'] ) ) );
    23562405            $where     .= " AND {$wpdb->posts}.post_author IN ($author__in) ";
     
    25892638                $q_status = explode( ',', $q_status );
    25902639            }
     2640            sort( $q_status );
    25912641            $r_status = array();
    25922642            $p_status = array();
     
    49034953        sort( $args['post_type'] );
    49044954
     4955        /*
     4956         * Sort arrays that can be used for ordering prior to cache key generation.
     4957         *
     4958         * These arrays are sorted in the query generator for the purposes of the
     4959         * WHERE clause but the arguments are not modified as they can be used for
     4960         * the orderby clase.
     4961         *
     4962         * Their use in the orderby clause will generate a different SQL query so
     4963         * they can be sorted for the cache key generation.
     4964         */
     4965        $sortable_arrays_with_int_values = array(
     4966            'post__in',
     4967            'post_parent__in',
     4968        );
     4969        foreach ( $sortable_arrays_with_int_values as $key ) {
     4970            if ( isset( $args[ $key ] ) && is_array( $args[ $key ] ) ) {
     4971                $args[ $key ] = array_unique( array_map( 'absint', $args[ $key ] ) );
     4972                sort( $args[ $key ] );
     4973            }
     4974        }
     4975
     4976        // Sort and unique the 'post_name__in' for cache key generation.
     4977        if ( isset( $args['post_name__in'] ) && is_array( $args['post_name__in'] ) ) {
     4978            $args['post_name__in'] = array_unique( $args['post_name__in'] );
     4979            sort( $args['post_name__in'] );
     4980        }
     4981
    49054982        if ( isset( $args['post_status'] ) ) {
    49064983            $args['post_status'] = (array) $args['post_status'];
     
    49435020        }
    49445021
    4945         return "wp_query:$key:$last_changed";
     5022        $this->query_cache_key = "wp_query:$key:$last_changed";
     5023        return $this->query_cache_key;
    49465024    }
    49475025
Note: See TracChangeset for help on using the changeset viewer.