From 7ba59300aed53c800b7d532a3f5505a928f2020a Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Wed, 15 Jul 2026 12:59:30 +0530 Subject: [PATCH] Taxonomy: Improve AND term query performance --- src/wp-includes/class-wp-tax-query.php | 23 +++++++ tests/phpunit/tests/term/taxQuery.php | 94 ++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/src/wp-includes/class-wp-tax-query.php b/src/wp-includes/class-wp-tax-query.php index c6ec3258cc201..db2f944206737 100644 --- a/src/wp-includes/class-wp-tax-query.php +++ b/src/wp-includes/class-wp-tax-query.php @@ -451,6 +451,29 @@ public function get_sql_for_clause( &$clause, $parent_query ) { return $sql; } + /* + * Individual joins avoid a correlated subquery for small term intersections. + * Limit total taxonomy joins to three because additional joins can cost more + * than the subquery for common terms. + */ + if ( 3 >= count( $terms ) + count( $this->table_aliases ) ) { + foreach ( $terms as $term ) { + $i = count( $this->table_aliases ); + $alias = $i ? 'tt' . $i : $wpdb->term_relationships; + + $this->table_aliases[] = $alias; + + $join = " LEFT JOIN $wpdb->term_relationships"; + $join .= $i ? " AS $alias" : ''; + $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)"; + + $sql['join'][] = $join; + $sql['where'][] = "$alias.term_taxonomy_id IN ($term)"; + } + + return $sql; + } + $num_terms = count( $terms ); $terms = implode( ',', $terms ); diff --git a/tests/phpunit/tests/term/taxQuery.php b/tests/phpunit/tests/term/taxQuery.php index 40f1d29612e70..aab8214a9bcdf 100644 --- a/tests/phpunit/tests/term/taxQuery.php +++ b/tests/phpunit/tests/term/taxQuery.php @@ -505,6 +505,100 @@ public function test_get_sql_operator_and_empty_terms() { _unregister_taxonomy( 'wptests_tax' ); } + /** + * @ticket 16706 + */ + public function test_get_sql_operator_and_uses_joins_for_three_or_fewer_terms() { + $tq = new WP_Tax_Query( + array( + array( + 'field' => 'term_taxonomy_id', + 'operator' => 'AND', + 'terms' => range( 1, 3 ), + ), + ) + ); + + global $wpdb; + $sql = $tq->get_sql( $wpdb->posts, 'ID' ); + + $this->assertSame( 3, substr_count( $sql['join'], 'JOIN' ) ); + $this->assertStringNotContainsString( 'SELECT COUNT(1)', $sql['where'] ); + } + + /** + * @ticket 16706 + */ + public function test_get_sql_operator_and_uses_subquery_for_more_than_three_terms() { + $tq = new WP_Tax_Query( + array( + array( + 'field' => 'term_taxonomy_id', + 'operator' => 'AND', + 'terms' => range( 1, 4 ), + ), + ) + ); + + global $wpdb; + $sql = $tq->get_sql( $wpdb->posts, 'ID' ); + + $this->assertSame( '', $sql['join'] ); + $this->assertStringContainsString( 'SELECT COUNT(1)', $sql['where'] ); + } + + /** + * @ticket 16706 + */ + public function test_get_sql_operator_and_limits_total_joins_across_clauses() { + $tq = new WP_Tax_Query( + array( + array( + 'field' => 'term_taxonomy_id', + 'operator' => 'AND', + 'terms' => range( 1, 2 ), + ), + array( + 'field' => 'term_taxonomy_id', + 'operator' => 'AND', + 'terms' => range( 3, 4 ), + ), + ) + ); + + global $wpdb; + $sql = $tq->get_sql( $wpdb->posts, 'ID' ); + + $this->assertSame( 2, substr_count( $sql['join'], 'JOIN' ) ); + $this->assertStringContainsString( 'SELECT COUNT(1)', $sql['where'] ); + } + + /** + * @ticket 16706 + */ + public function test_get_sql_operator_and_counts_existing_in_join_toward_limit() { + $tq = new WP_Tax_Query( + array( + array( + 'field' => 'term_taxonomy_id', + 'operator' => 'IN', + 'terms' => 1, + ), + array( + 'field' => 'term_taxonomy_id', + 'operator' => 'AND', + 'terms' => range( 2, 4 ), + ), + ) + ); + + global $wpdb; + $sql = $tq->get_sql( $wpdb->posts, 'ID' ); + + $this->assertSame( 1, substr_count( $sql['join'], 'JOIN' ) ); + $this->assertStringContainsString( 'SELECT COUNT(1)', $sql['where'] ); + } + /** * @ticket 18105 * @covers WP_Tax_Query::get_sql