WordPress.org

Make WordPress Core

Changeset 29902


Ignore:
Timestamp:
10/15/2014 04:39:19 PM (5 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.