Skip to content

Commit c22e267

Browse files
committed
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_query`s 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. git-svn-id: https://develop.svn.wordpress.org/trunk@59766 602fd350-edb4-49c9-b593-d223f7449a82
1 parent a11571e commit c22e267

File tree

4 files changed

+495
-35
lines changed

4 files changed

+495
-35
lines changed

src/wp-includes/class-wp-query.php

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,16 @@ class WP_Query {
474474

475475
private $compat_methods = array( 'init_query_flags', 'parse_tax_query' );
476476

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 = '';
486+
477487
/**
478488
* Resets query flags to false.
479489
*
@@ -1101,15 +1111,17 @@ public function parse_query( $query = '' ) {
11011111

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'] );
11071118
}
11081119
}
11091120

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'] );
11151127
}
@@ -1182,9 +1194,12 @@ public function parse_tax_query( &$q ) {
11821194

11831195
$term = $q[ $t->query_var ];
11841196

1185-
if ( is_array( $term ) ) {
1186-
$term = implode( ',', $term );
1197+
if ( ! is_array( $term ) ) {
1198+
$term = explode( ',', $term );
1199+
$term = array_map( 'trim', $term );
11871200
}
1201+
sort( $term );
1202+
$term = implode( ',', $term );
11881203

11891204
if ( str_contains( $term, '+' ) ) {
11901205
$terms = preg_split( '/[+]+/', $term );
@@ -1220,7 +1235,8 @@ public function parse_tax_query( &$q ) {
12201235

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 ) {
12261242
if ( $cat > 0 ) {
@@ -1262,7 +1278,8 @@ public function parse_tax_query( &$q ) {
12621278

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'],
12681285
'field' => 'term_id',
@@ -1272,6 +1289,7 @@ public function parse_tax_query( &$q ) {
12721289

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',
12771295
'terms' => $q['category__not_in'],
@@ -1282,7 +1300,8 @@ public function parse_tax_query( &$q ) {
12821300

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'],
12881307
'field' => 'term_id',
@@ -1300,10 +1319,12 @@ public function parse_tax_query( &$q ) {
13001319

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'] ) ) {
13091330
$tags = preg_split( '/[+\r\n\t ]+/', $q['tag'] );
@@ -1314,6 +1335,7 @@ public function parse_tax_query( &$q ) {
13141335
} else {
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
}
13191341

@@ -1327,14 +1349,16 @@ public function parse_tax_query( &$q ) {
13271349

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'],
13331356
);
13341357
}
13351358

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',
13401364
'terms' => $q['tag__not_in'],
@@ -1344,7 +1368,8 @@ public function parse_tax_query( &$q ) {
13441368

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'],
13501375
'operator' => 'AND',
@@ -1353,7 +1378,8 @@ public function parse_tax_query( &$q ) {
13531378

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'],
13591385
'field' => 'slug',
@@ -1362,7 +1388,8 @@ public function parse_tax_query( &$q ) {
13621388

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'],
13681395
'field' => 'slug',
@@ -2186,8 +2213,11 @@ public function get_posts() {
21862213
$where .= " AND {$wpdb->posts}.post_name = '" . $q['attachment'] . "'";
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

21932223
// If an attachment is requested by number, let it supersede any post number.
@@ -2199,19 +2229,29 @@ public function get_posts() {
21992229
if ( $q['p'] ) {
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)";
22072242
}
22082243

22092244
if ( is_numeric( $q['post_parent'] ) ) {
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)";
22172257
}
@@ -2341,6 +2381,7 @@ public function get_posts() {
23412381
if ( ! empty( $q['author'] ) && '0' != $q['author'] ) {
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';
23462387
$q[ $key ][] = abs( $author );
@@ -2349,9 +2390,17 @@ public function get_posts() {
23492390
}
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) ";
23572406
}
@@ -2588,6 +2637,7 @@ public function get_posts() {
25882637
if ( ! is_array( $q_status ) ) {
25892638
$q_status = explode( ',', $q_status );
25902639
}
2640+
sort( $q_status );
25912641
$r_status = array();
25922642
$p_status = array();
25932643
$e_status = array();
@@ -4902,6 +4952,33 @@ protected function generate_cache_key( array $args, $sql ) {
49024952
// Sort post types to ensure same cache key generation.
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'];
49074984
// Sort post status to ensure same cache key generation.
@@ -4942,7 +5019,8 @@ static function ( &$value ) use ( $wpdb, $placeholder ) {
49425019
$last_changed .= wp_cache_get_last_changed( 'terms' );
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

49485026
/**

0 commit comments

Comments
 (0)