Make WordPress Core

Changeset 29902


Ignore:
Timestamp:
10/15/2014 04:39:19 PM (12 years ago)
Author:
boonebgorges
Message:

Avoid redundant table joins in WP_Tax_Query.

IN clauses that are connected by OR require only a single table join. To avoid
extraneous joins, keep track of generated table aliases, and let sibling
clauses piggy-back on those aliases when possible.

Introduces WP_Tax_Query::sanitize_relation() to reduce some repeated code.

Adds unit tests to verify the JOIN consolidation, and integration tests for
cases where JOINS are being combined.

Props boonebgorges, otto42, jakub.tyrcha.
Fixes #18105.

Location:
trunk
Files:
3 edited

Legend:

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

    r29896 r29902  
    716716         */
    717717        public function __construct( $tax_query ) {
    718                 if ( isset( $tax_query['relation'] ) && strtoupper( $tax_query['relation'] ) == 'OR' ) {
    719                         $this->relation = 'OR';
     718                if ( isset( $tax_query['relation'] ) ) {
     719                        $this->relation = $this->sanitize_relation( $tax_query['relation'] );
    720720                } else {
    721721                        $this->relation = 'AND';
     
    750750                foreach ( $queries as $key => $query ) {
    751751                        if ( 'relation' === $key ) {
    752                                 $cleaned_query['relation'] = $query;
     752                                $cleaned_query['relation'] = $this->sanitize_relation( $query );
    753753
    754754                        // First-order clause.
     
    787787
    788788                                if ( ! empty( $cleaned_subquery ) ) {
     789                                        // All queries with children must have a relation.
     790                                        if ( ! isset( $cleaned_subquery['relation'] ) ) {
     791                                                $cleaned_subquery['relation'] = 'AND';
     792                                        }
     793
    789794                                        $cleaned_query[] = $cleaned_subquery;
    790795                                }
     
    793798
    794799                return $cleaned_query;
     800        }
     801
     802        /**
     803         * Sanitize a 'relation' operator.
     804         *
     805         * @since 4.1.0
     806         * @access public
     807         *
     808         * @param string $relation Raw relation key from the query argument.
     809         * @return Sanitized relation ('AND' or 'OR').
     810         */
     811        public function sanitize_relation( $relation ) {
     812                if ( 'OR' === strtoupper( $relation ) ) {
     813                        return 'OR';
     814                } else {
     815                        return 'AND';
     816                }
    795817        }
    796818
     
    853875         */
    854876        protected function get_sql_clauses() {
    855                 $sql = $this->get_sql_for_query( $this->queries );
     877                /*
     878                 * $queries are passed by reference to get_sql_for_query() for recursion.
     879                 * To keep $this->queries unaltered, pass a copy.
     880                 */
     881                $queries = $this->queries;
     882                $sql = $this->get_sql_for_query( $queries );
    856883
    857884                if ( ! empty( $sql['where'] ) ) {
     
    881908         * }
    882909         */
    883         protected function get_sql_for_query( $query, $depth = 0 ) {
     910        protected function get_sql_for_query( &$query, $depth = 0 ) {
    884911                $sql_chunks = array(
    885912                        'join'  => array(),
     
    897924                }
    898925
    899                 foreach ( $query as $key => $clause ) {
     926                foreach ( $query as $key => &$clause ) {
    900927                        if ( 'relation' === $key ) {
    901928                                $relation = $query['relation'];
     
    962989         * }
    963990         */
    964         public function get_sql_for_clause( $clause, $parent_query ) {
     991        public function get_sql_for_clause( &$clause, $parent_query ) {
    965992                global $wpdb;
    966993
     
    9891016                        $terms = implode( ',', $terms );
    9901017
    991                         $i = count( $this->table_aliases );
    992                         $alias = $i ? 'tt' . $i : $wpdb->term_relationships;
    993                         $this->table_aliases[] = $alias;
    994 
    995                         $join .= " INNER JOIN $wpdb->term_relationships";
    996                         $join .= $i ? " AS $alias" : '';
    997                         $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)";
     1018                        /*
     1019                         * Before creating another table join, see if this clause has a
     1020                         * sibling with an existing join that can be shared.
     1021                         */
     1022                        $alias = $this->find_compatible_table_alias( $clause, $parent_query );
     1023                        if ( false === $alias ) {
     1024                                $i = count( $this->table_aliases );
     1025                                $alias = $i ? 'tt' . $i : $wpdb->term_relationships;
     1026
     1027                                // Store the alias as part of a flat array to build future iterators.
     1028                                $this->table_aliases[] = $alias;
     1029
     1030                                // Store the alias with this clause, so later siblings can use it.
     1031                                $clause['alias'] = $alias;
     1032
     1033                                $join .= " INNER JOIN $wpdb->term_relationships";
     1034                                $join .= $i ? " AS $alias" : '';
     1035                                $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)";
     1036                        }
     1037
    9981038
    9991039                        $where = "$alias.term_taxonomy_id $operator ($terms)";
     
    10481088        }
    10491089
     1090        /**
     1091         * Identify an existing table alias that is compatible with the current query clause.
     1092         *
     1093         * We avoid unnecessary table joins by allowing each clause to look for
     1094         * an existing table alias that is compatible with the query that it
     1095         * needs to perform. An existing alias is compatible if (a) it is a
     1096         * sibling of $clause (ie, it's under the scope of the same relation),
     1097         * and (b) the combination of operator and relation between the clauses
     1098         * allows for a shared table join. In the case of WP_Tax_Query, this
     1099         * only applies to IN clauses that are connected by the relation OR.
     1100         *
     1101         * @since 4.1.0
     1102         * @access protected
     1103         *
     1104         * @param  array       $clause       Query clause.
     1105         * @param  array       $parent_query Parent query of $clause.
     1106         * @return string|bool Table alias if found, otherwise false.
     1107         */
     1108        protected function find_compatible_table_alias( $clause, $parent_query ) {
     1109                $alias = false;
     1110
     1111                // Sanity check. Only IN queries use the JOIN syntax .
     1112                if ( ! isset( $clause['operator'] ) || 'IN' !== $clause['operator'] ) {
     1113                        return $alias;
     1114                }
     1115
     1116                // Since we're only checking IN queries, we're only concerned with OR relations.
     1117                if ( ! isset( $parent_query['relation'] ) || 'OR' !== $parent_query['relation'] ) {
     1118                        return $alias;
     1119                }
     1120
     1121                $compatible_operators = array( 'IN' );
     1122
     1123                foreach ( $parent_query as $sibling ) {
     1124                        if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
     1125                                continue;
     1126                        }
     1127
     1128                        if ( empty( $sibling['alias'] ) || empty( $sibling['operator'] ) ) {
     1129                                continue;
     1130                        }
     1131
     1132                        // The sibling must both have compatible operator to share its alias.
     1133                        if ( in_array( strtoupper( $sibling['operator'] ), $compatible_operators ) ) {
     1134                                $alias = $sibling['alias'];
     1135                                break;
     1136                        }
     1137                }
     1138
     1139                return $alias;
     1140        }
    10501141
    10511142        /**
  • trunk/tests/phpunit/tests/post/query.php

    r29896 r29902  
    13771377        /**
    13781378         * @group taxonomy
     1379         * @ticket 18105
     1380         */
     1381        public function test_tax_query_single_query_multiple_queries_operator_not_in() {
     1382                $t1 = $this->factory->term->create( array(
     1383                        'taxonomy' => 'category',
     1384                        'slug' => 'foo',
     1385                        'name' => 'Foo',
     1386                ) );
     1387                $t2 = $this->factory->term->create( array(
     1388                        'taxonomy' => 'category',
     1389                        'slug' => 'bar',
     1390                        'name' => 'Bar',
     1391                ) );
     1392                $p1 = $this->factory->post->create();
     1393                $p2 = $this->factory->post->create();
     1394                $p3 = $this->factory->post->create();
     1395
     1396                wp_set_post_terms( $p1, $t1, 'category' );
     1397                wp_set_post_terms( $p2, $t2, 'category' );
     1398
     1399                $q = new WP_Query( array(
     1400                        'fields' => 'ids',
     1401                        'update_post_meta_cache' => false,
     1402                        'update_post_term_cache' => false,
     1403                        'tax_query' => array(
     1404                                'relation' => 'AND',
     1405                                array(
     1406                                        'taxonomy' => 'category',
     1407                                        'terms' => array( 'foo' ),
     1408                                        'field' => 'slug',
     1409                                        'operator' => 'NOT IN',
     1410                                ),
     1411                                array(
     1412                                        'taxonomy' => 'category',
     1413                                        'terms' => array( 'bar' ),
     1414                                        'field' => 'slug',
     1415                                        'operator' => 'NOT IN',
     1416                                ),
     1417                        ),
     1418                ) );
     1419
     1420                $this->assertEquals( array( $p3 ), $q->posts );
     1421        }
     1422
     1423        /**
     1424         * @group taxonomy
    13791425         */
    13801426        public function test_tax_query_single_query_multiple_terms_operator_and() {
  • trunk/tests/phpunit/tests/term/query.php

    r29805 r29902  
    219219                $this->assertTrue( is_wp_error( $tq->queries[0] ) );
    220220        }
     221
     222        /**
     223         * @ticket 18105
     224         */
     225        public function test_get_sql_relation_or_operator_in() {
     226                register_taxonomy( 'wptests_tax', 'post' );
     227
     228                $t1 = $this->factory->term->create( array(
     229                        'taxonomy' => 'wptests_tax',
     230                ) );
     231                $t2 = $this->factory->term->create( array(
     232                        'taxonomy' => 'wptests_tax',
     233                ) );
     234                $t3 = $this->factory->term->create( array(
     235                        'taxonomy' => 'wptests_tax',
     236                ) );
     237
     238                $tq = new WP_Tax_Query( array(
     239                        'relation' => 'OR',
     240                        array(
     241                                'taxonomy' => 'wptests_tax',
     242                                'field' => 'term_id',
     243                                'terms' => $t1,
     244                        ),
     245                        array(
     246                                'taxonomy' => 'wptests_tax',
     247                                'field' => 'term_id',
     248                                'terms' => $t2,
     249                        ),
     250                        array(
     251                                'taxonomy' => 'wptests_tax',
     252                                'field' => 'term_id',
     253                                'terms' => $t3,
     254                        ),
     255                ) );
     256
     257                global $wpdb;
     258                $sql = $tq->get_sql( $wpdb->posts, 'ID' );
     259
     260                // Only one JOIN is required with OR + IN.
     261                $this->assertSame( 1, substr_count( $sql['join'], 'JOIN' ) );
     262
     263                _unregister_taxonomy( 'wptests_tax' );
     264        }
     265
     266        /**
     267         * @ticket 18105
     268         */
     269        public function test_get_sql_relation_and_operator_in() {
     270                register_taxonomy( 'wptests_tax', 'post' );
     271
     272                $t1 = $this->factory->term->create( array(
     273                        'taxonomy' => 'wptests_tax',
     274                ) );
     275                $t2 = $this->factory->term->create( array(
     276                        'taxonomy' => 'wptests_tax',
     277                ) );
     278                $t3 = $this->factory->term->create( array(
     279                        'taxonomy' => 'wptests_tax',
     280                ) );
     281
     282                $tq = new WP_Tax_Query( array(
     283                        'relation' => 'AND',
     284                        array(
     285                                'taxonomy' => 'wptests_tax',
     286                                'field' => 'term_id',
     287                                'terms' => $t1,
     288                        ),
     289                        array(
     290                                'taxonomy' => 'wptests_tax',
     291                                'field' => 'term_id',
     292                                'terms' => $t2,
     293                        ),
     294                        array(
     295                                'taxonomy' => 'wptests_tax',
     296                                'field' => 'term_id',
     297                                'terms' => $t3,
     298                        ),
     299                ) );
     300
     301                global $wpdb;
     302                $sql = $tq->get_sql( $wpdb->posts, 'ID' );
     303
     304                $this->assertSame( 3, substr_count( $sql['join'], 'JOIN' ) );
     305
     306                _unregister_taxonomy( 'wptests_tax' );
     307        }
     308
     309        /**
     310         * @ticket 18105
     311         */
     312        public function test_get_sql_nested_relation_or_operator_in() {
     313                register_taxonomy( 'wptests_tax', 'post' );
     314
     315                $t1 = $this->factory->term->create( array(
     316                        'taxonomy' => 'wptests_tax',
     317                ) );
     318                $t2 = $this->factory->term->create( array(
     319                        'taxonomy' => 'wptests_tax',
     320                ) );
     321                $t3 = $this->factory->term->create( array(
     322                        'taxonomy' => 'wptests_tax',
     323                ) );
     324
     325                $tq = new WP_Tax_Query( array(
     326                        'relation' => 'OR',
     327                        array(
     328                                'taxonomy' => 'wptests_tax',
     329                                'field' => 'term_id',
     330                                'terms' => $t1,
     331                        ),
     332                        array(
     333                                'relation' => 'OR',
     334                                array(
     335                                        'taxonomy' => 'wptests_tax',
     336                                        'field' => 'term_id',
     337                                        'terms' => $t2,
     338                                ),
     339                                array(
     340                                        'taxonomy' => 'wptests_tax',
     341                                        'field' => 'term_id',
     342                                        'terms' => $t3,
     343                                ),
     344                        ),
     345                ) );
     346
     347                global $wpdb;
     348                $sql = $tq->get_sql( $wpdb->posts, 'ID' );
     349
     350                $this->assertSame( 2, substr_count( $sql['join'], 'JOIN' ) );
     351
     352                _unregister_taxonomy( 'wptests_tax' );
     353        }
     354
    221355}
Note: See TracChangeset for help on using the changeset viewer.