Make WordPress Core


Ignore:
Timestamp:
10/14/2020 12:49:52 AM (4 years ago)
Author:
peterwilsoncc
Message:

Taxonomy: Improve performance of term recounting database queries.

When modifying terms assigned to an object, replace full term recounts with incrementing/decrementing the count as appropriate. This provides a significant performance boost on sites with a high number of term/object relationships and/or posts.

Introduces the functions wp_increment_term_count(), wp_decrement_term_count(), wp_modify_term_count_by() and wp_modify_term_count_by_now() for updating the term count.

Introduces the function _wp_prevent_term_counting() for preventing double counting on posts that are about to transition.

Adds the parameter update_count_by_callback to register_taxonomy() to allow developers to use a custom callback for incrementing or decrementing a term count.

Props boonebgorges, davidbaumwald, hellofromTonya, johnbillion, lcyh78, mattoperry, peterwilsoncc, rebasaurus, whyisjake.
Fixes #40351.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/term/termCounts.php

    r49000 r49141  
    1919     */
    2020    public static $post_ids;
     21
     22    /**
     23     * Array of tag IDs.
     24     *
     25     * @var int[]
     26     */
     27    public static $tag_ids;
     28
     29    /**
     30     * Term ID for testing user counts.
     31     *
     32     * @var int
     33     */
     34    public static $user_term;
     35
     36    /**
     37     * User ID for testing user counts.
     38     *
     39     * @var int
     40     */
     41    public static $user_id;
    2142
    2243    /**
     
    3152        }
    3253
    33         register_taxonomy( 'wp_test_tax_counts', array( 'post', 'attachment' ) );
     54        // Extra published post.
     55        self::$post_ids['publish_two'] = $factory->post->create( array( 'post_status' => 'publish' ) );
     56
     57        self::$user_id = $factory->user->create( array( 'role' => 'author' ) );
     58
     59        self::register_taxonomies();
    3460        self::$attachment_term = $factory->term->create( array( 'taxonomy' => 'wp_test_tax_counts' ) );
     61        self::$user_term       = $factory->term->create( array( 'taxonomy' => 'wp_test_user_tax_counts' ) );
     62        self::$tag_ids         = $factory->term->create_many( 5 );
    3563    }
    3664
    3765    public function setUp() {
    3866        parent::setUp();
    39 
     67        self::register_taxonomies();
     68    }
     69
     70    /**
     71     * Register taxonomies used by tests.
     72     *
     73     * This is called both before class and before each test as the global is
     74     * reset in each test's tearDown.
     75     */
     76    public static function register_taxonomies() {
    4077        register_taxonomy( 'wp_test_tax_counts', array( 'post', 'attachment' ) );
     78        register_taxonomy( 'wp_test_user_tax_counts', 'user' );
     79    }
     80
     81    /**
     82     * Term counts are not double incremented when post created.
     83     *
     84     * @covers wp_modify_term_count_by
     85     * @dataProvider data_term_count_changes_for_post_statuses
     86     * @ticket 40351
     87     *
     88     * @param string $post_status New post status.
     89     * @param int    $change      Expected change.
     90     */
     91    public function test_term_count_changes_for_post_statuses( $post_status, $change ) {
     92        $term_count = get_term( get_option( 'default_category' ) )->count;
     93        // Do not use shared fixture for this test as it relies on a new post.
     94        $post_id = $this->factory()->post->create( array( 'post_status' => $post_status ) );
     95
     96        $expected = $term_count + $change;
     97        $this->assertSame( $expected, get_term( get_option( 'default_category' ) )->count );
     98    }
     99
     100    /**
     101     * Data provider for test_term_count_changes_for_post_statuses.
     102     *
     103     * @return array[] {
     104     *     @type string $post_status New post status.
     105     *     @type int    $change      Expected change.
     106     * }
     107     */
     108    function data_term_count_changes_for_post_statuses() {
     109        return array(
     110            // 0. Published post
     111            array( 'publish', 1 ),
     112            // 1. Auto draft
     113            array( 'auto-draft', 0 ),
     114            // 2. Draft
     115            array( 'draft', 0 ),
     116            // 3. Private post
     117            array( 'private', 0 ),
     118        );
    41119    }
    42120
     
    45123     *
    46124     * @covers wp_publish_post
    47      * @covers wp_count_terms
     125     * @covers wp_modify_term_count_by
    48126     * @dataProvider data_term_counts_incremented_on_publish
    49127     * @ticket 40351
     
    85163
    86164    /**
     165     * Test post status transition update term counts correctly.
     166     *
     167     * @covers wp_modify_term_count_by
     168     * @dataProvider data_term_count_transitions_update_term_counts
     169     * @ticket 40351
     170     *
     171     * @param string $original_post_status Post status upon create.
     172     * @param string $new_post_status      Post status after update.
     173     * @param int    $change               Expected change upon publish.
     174     */
     175    function test_term_count_transitions_update_term_counts( $original_post_status, $new_post_status, $change ) {
     176        $post_id    = self::$post_ids[ $original_post_status ];
     177        $term_count = get_term( get_option( 'default_category' ) )->count;
     178
     179        wp_update_post(
     180            array(
     181                'ID'          => $post_id,
     182                'post_status' => $new_post_status,
     183            )
     184        );
     185
     186        $expected = $term_count + $change;
     187        $this->assertSame( $expected, get_term( get_option( 'default_category' ) )->count );
     188    }
     189
     190    /**
     191     * Data provider for test_term_count_transitions_update_term_counts.
     192     *
     193     * @return array[] {
     194     *     @type string $original_post_status Post status upon create.
     195     *     @type string $new_post_status      Post status after update.
     196     *     @type int    $change               Expected change upon publish.
     197     * }
     198     */
     199    function data_term_count_transitions_update_term_counts() {
     200        return array(
     201            // 0. Draft -> published post
     202            array( 'draft', 'publish', 1 ),
     203            // 1. Auto draft -> published post
     204            array( 'auto-draft', 'publish', 1 ),
     205            // 2. Private -> published post
     206            array( 'private', 'publish', 1 ),
     207            // 3. Published -> published post
     208            array( 'publish', 'publish', 0 ),
     209
     210            // 4. Draft -> private post
     211            array( 'draft', 'private', 0 ),
     212            // 5. Auto draft -> private post
     213            array( 'auto-draft', 'private', 0 ),
     214            // 6. Private -> private post
     215            array( 'private', 'private', 0 ),
     216            // 7. Published -> private post
     217            array( 'publish', 'private', -1 ),
     218
     219            // 8. Draft -> draft post
     220            array( 'draft', 'draft', 0 ),
     221            // 9. Auto draft -> draft post
     222            array( 'auto-draft', 'draft', 0 ),
     223            // 10. Private -> draft post
     224            array( 'private', 'draft', 0 ),
     225            // 11. Published -> draft post
     226            array( 'publish', 'draft', -1 ),
     227        );
     228    }
     229
     230    /**
     231     * Term counts are not double incremented when post created.
     232     *
     233     * @covers wp_modify_term_count_by
     234     * @dataProvider data_term_count_changes_for_post_statuses_with_attachments
     235     * @ticket 40351
     236     *
     237     * @param string $post_status New post status.
     238     * @param int    $change      Expected change.
     239     */
     240    public function test_term_count_changes_for_post_statuses_with_attachments( $post_status, $change ) {
     241        $term_count = get_term( self::$attachment_term )->count;
     242        // Do not use shared fixture for this test as it relies on a new post.
     243        $post_id = $this->factory()->post->create( array( 'post_status' => $post_status ) );
     244        wp_add_object_terms( $post_id, self::$attachment_term, 'wp_test_tax_counts' );
     245        $attachment_id = self::factory()->attachment->create_object(
     246            array(
     247                'file'        => 'image.jpg',
     248                'post_parent' => $post_id,
     249                'post_status' => 'inherit',
     250            )
     251        );
     252        wp_add_object_terms( $attachment_id, self::$attachment_term, 'wp_test_tax_counts' );
     253
     254        $expected = $term_count + $change;
     255        $this->assertSame( $expected, get_term( self::$attachment_term )->count );
     256    }
     257
     258    /**
     259     * Data provider for test_term_count_changes_for_post_statuses_with_attachments.
     260     *
     261     * @return array[] {
     262     *     @type string $post_status New post status.
     263     *     @type int    $change      Expected change.
     264     * }
     265     */
     266    function data_term_count_changes_for_post_statuses_with_attachments() {
     267        return array(
     268            // 0. Published post
     269            array( 'publish', 2 ),
     270            // 1. Auto draft
     271            array( 'auto-draft', 0 ),
     272            // 2. Draft
     273            array( 'draft', 0 ),
     274            // 3. Private post
     275            array( 'private', 0 ),
     276        );
     277    }
     278
     279    /**
    87280     * Term counts increments correctly when post status becomes published.
    88281     *
    89282     * @covers wp_publish_post
     283     * @covers wp_modify_term_count_by
    90284     * @dataProvider data_term_counts_incremented_on_publish_with_attachments
    91285     * @ticket 40351
     
    136330
    137331    /**
     332     * Test post status transition update term counts correctly.
     333     *
     334     * @covers wp_modify_term_count_by
     335     * @dataProvider data_term_count_transitions_update_term_counts_with_attachments
     336     * @ticket 40351
     337     *
     338     * @param string $original_post_status Post status upon create.
     339     * @param string $new_post_status      Post status after update.
     340     * @param int    $change               Expected change upon publish.
     341     */
     342    function test_term_count_transitions_update_term_counts_with_attachments( $original_post_status, $new_post_status, $change ) {
     343        $post_id = self::$post_ids[ $original_post_status ];
     344        wp_add_object_terms( $post_id, self::$attachment_term, 'wp_test_tax_counts' );
     345        $attachment_id = self::factory()->attachment->create_object(
     346            array(
     347                'file'        => 'image.jpg',
     348                'post_parent' => $post_id,
     349                'post_status' => 'inherit',
     350            )
     351        );
     352        wp_add_object_terms( $attachment_id, self::$attachment_term, 'wp_test_tax_counts' );
     353        $term_count = get_term( self::$attachment_term )->count;
     354
     355        wp_update_post(
     356            array(
     357                'ID'          => $post_id,
     358                'post_status' => $new_post_status,
     359            )
     360        );
     361
     362        $expected = $term_count + $change;
     363        $this->assertSame( $expected, get_term( self::$attachment_term )->count );
     364    }
     365
     366    /**
     367     * Data provider for test_term_count_transitions_update_term_counts_with_attachments.
     368     *
     369     * @return array[] {
     370     *     @type string $original_post_status Post status upon create.
     371     *     @type string $new_post_status      Post status after update.
     372     *     @type int    $change               Expected change upon publish.
     373     * }
     374     */
     375    function data_term_count_transitions_update_term_counts_with_attachments() {
     376        return array(
     377            // 0. Draft -> published post
     378            array( 'draft', 'publish', 2 ),
     379            // 1. Auto draft -> published post
     380            array( 'auto-draft', 'publish', 2 ),
     381            // 2. Private -> published post
     382            array( 'private', 'publish', 2 ),
     383            // 3. Published -> published post
     384            array( 'publish', 'publish', 0 ),
     385
     386            // 4. Draft -> private post
     387            array( 'draft', 'private', 0 ),
     388            // 5. Auto draft -> private post
     389            array( 'auto-draft', 'private', 0 ),
     390            // 6. Private -> private post
     391            array( 'private', 'private', 0 ),
     392            // 7. Published -> private post
     393            array( 'publish', 'private', -2 ),
     394
     395            // 8. Draft -> draft post
     396            array( 'draft', 'draft', 0 ),
     397            // 9. Auto draft -> draft post
     398            array( 'auto-draft', 'draft', 0 ),
     399            // 10. Private -> draft post
     400            array( 'private', 'draft', 0 ),
     401            // 11. Published -> draft post
     402            array( 'publish', 'draft', -2 ),
     403        );
     404    }
     405
     406    /**
     407     * Term counts are not double incremented when post created.
     408     *
     409     * @covers wp_modify_term_count_by
     410     * @dataProvider data_term_count_changes_for_post_statuses_with_untermed_attachments
     411     * @ticket 40351
     412     *
     413     * @param string $post_status New post status.
     414     * @param int    $change      Expected change.
     415     */
     416    public function test_term_count_changes_for_post_statuses_with_untermed_attachments( $post_status, $change ) {
     417        $term_count = get_term( self::$attachment_term )->count;
     418        // Do not use shared fixture for this test as it relies on a new post.
     419        $post_id = $this->factory()->post->create( array( 'post_status' => $post_status ) );
     420        wp_add_object_terms( $post_id, self::$attachment_term, 'wp_test_tax_counts' );
     421        $attachment_id = self::factory()->attachment->create_object(
     422            array(
     423                'file'        => 'image.jpg',
     424                'post_parent' => $post_id,
     425                'post_status' => 'inherit',
     426            )
     427        );
     428
     429        $expected = $term_count + $change;
     430        $this->assertSame( $expected, get_term( self::$attachment_term )->count );
     431    }
     432
     433    /**
     434     * Data provider for test_term_count_changes_for_post_statuses_with_untermed_attachments.
     435     *
     436     * @return array[] {
     437     *     @type string $post_status New post status.
     438     *     @type int    $change      Expected change.
     439     * }
     440     */
     441    function data_term_count_changes_for_post_statuses_with_untermed_attachments() {
     442        return array(
     443            // 0. Published post
     444            array( 'publish', 1 ),
     445            // 1. Auto draft
     446            array( 'auto-draft', 0 ),
     447            // 2. Draft
     448            array( 'draft', 0 ),
     449            // 3. Private post
     450            array( 'private', 0 ),
     451        );
     452    }
     453
     454    /**
    138455     * Term counts increments correctly when post status becomes published.
    139456     *
     457     * @covers wp_modify_term_count_by
    140458     * @covers wp_publish_post
    141459     * @dataProvider data_term_counts_incremented_on_publish_with_untermed_attachments
     
    184502        );
    185503    }
     504
     505    /**
     506     * Test post status transition update term counts correctly.
     507     *
     508     * @covers wp_modify_term_count_by
     509     * @dataProvider data_term_count_transitions_update_term_counts_with_untermed_attachments
     510     * @ticket 40351
     511     *
     512     * @param string $original_post_status Post status upon create.
     513     * @param string $new_post_status      Post status after update.
     514     * @param int    $change               Expected change upon publish.
     515     */
     516    function test_term_count_transitions_update_term_counts_with_untermed_attachments( $original_post_status, $new_post_status, $change ) {
     517        $post_id = self::$post_ids[ $original_post_status ];
     518        wp_add_object_terms( $post_id, self::$attachment_term, 'wp_test_tax_counts' );
     519        $attachment_id = self::factory()->attachment->create_object(
     520            array(
     521                'file'        => 'image.jpg',
     522                'post_parent' => $post_id,
     523                'post_status' => 'inherit',
     524            )
     525        );
     526        $term_count    = get_term( self::$attachment_term )->count;
     527
     528        wp_update_post(
     529            array(
     530                'ID'          => $post_id,
     531                'post_status' => $new_post_status,
     532            )
     533        );
     534
     535        $expected = $term_count + $change;
     536        $this->assertSame( $expected, get_term( self::$attachment_term )->count );
     537    }
     538
     539    /**
     540     * Data provider for test_term_count_transitions_update_term_counts_with_untermed_attachments.
     541     *
     542     * @return array[] {
     543     *     @type string $original_post_status Post status upon create.
     544     *     @type string $new_post_status      Post status after update.
     545     *     @type int    $change               Expected change upon publish.
     546     * }
     547     */
     548    function data_term_count_transitions_update_term_counts_with_untermed_attachments() {
     549        return array(
     550            // 0. Draft -> published post
     551            array( 'draft', 'publish', 1 ),
     552            // 1. Auto draft -> published post
     553            array( 'auto-draft', 'publish', 1 ),
     554            // 2. Private -> published post
     555            array( 'private', 'publish', 1 ),
     556            // 3. Published -> published post
     557            array( 'publish', 'publish', 0 ),
     558
     559            // 4. Draft -> private post
     560            array( 'draft', 'private', 0 ),
     561            // 5. Auto draft -> private post
     562            array( 'auto-draft', 'private', 0 ),
     563            // 6. Private -> private post
     564            array( 'private', 'private', 0 ),
     565            // 7. Published -> private post
     566            array( 'publish', 'private', -1 ),
     567
     568            // 8. Draft -> draft post
     569            array( 'draft', 'draft', 0 ),
     570            // 9. Auto draft -> draft post
     571            array( 'auto-draft', 'draft', 0 ),
     572            // 10. Private -> draft post
     573            array( 'private', 'draft', 0 ),
     574            // 11. Published -> draft post
     575            array( 'publish', 'draft', -1 ),
     576        );
     577    }
     578
     579    /**
     580     * User taxonomy term counts increments when added to an account.
     581     *
     582     * @covers wp_modify_term_count_by
     583     * @ticket 51292
     584     */
     585    public function test_term_counts_user_adding_term() {
     586        $term_count = get_term( self::$user_term )->count;
     587        wp_add_object_terms( self::$user_id, self::$user_term, 'wp_test_user_tax_counts' );
     588
     589        $expected = $term_count + 1;
     590        $this->assertSame( $expected, get_term( self::$user_term )->count );
     591    }
     592
     593    /**
     594     * User taxonomy term counts decrement when term deleted from user.
     595     *
     596     * @covers wp_modify_term_count_by
     597     * @ticket 51292
     598     */
     599    public function test_term_counts_user_removing_term() {
     600        wp_add_object_terms( self::$user_id, self::$user_term, 'wp_test_user_tax_counts' );
     601        $term_count = get_term( self::$user_term )->count;
     602
     603        wp_remove_object_terms( self::$user_id, self::$user_term, 'wp_test_user_tax_counts' );
     604        $expected = $term_count - 1;
     605        $this->assertSame( $expected, get_term( self::$user_term )->count );
     606    }
     607
     608    /**
     609     * Ensure DB queries for deferred counts are nullified for net zero gain.
     610     *
     611     * @covers wp_modify_term_count_by
     612     * @covers wp_defer_term_counting
     613     * @ticket 51292
     614     */
     615    public function test_counts_after_deferral_net_zero() {
     616        $post_one = self::$post_ids['publish'];
     617        $post_two = self::$post_ids['publish_two'];
     618        $terms    = self::$tag_ids;
     619
     620        wp_set_object_terms( $post_one, $terms[0], 'post_tag', true );
     621
     622        // Net gain 0;
     623        wp_defer_term_counting( true );
     624        wp_remove_object_terms( $post_one, $terms[0], 'post_tag' );
     625        wp_set_object_terms( $post_two, $terms[0], 'post_tag', true );
     626        $num_queries = get_num_queries();
     627        wp_defer_term_counting( false );
     628        // Ensure number of queries unchanged.
     629        $this->assertSame( $num_queries, get_num_queries() );
     630    }
     631
     632    /**
     633     * Ensure full recounts follow modify by X recounts to avoid miscounts.
     634     *
     635     * @covers wp_modify_term_count_by
     636     * @covers wp_update_term_count
     637     * @covers wp_defer_term_counting
     638     * @ticket 51292
     639     */
     640    public function test_counts_after_deferral_full_before_partial() {
     641        $post_one   = self::$post_ids['publish'];
     642        $terms      = self::$tag_ids;
     643        $term_count = get_term( $terms[0] )->count;
     644
     645        // Net gain 1;
     646        wp_defer_term_counting( true );
     647        wp_set_object_terms( $post_one, $terms[0], 'post_tag', true );
     648        wp_update_term_count( get_term( $terms[0] )->term_taxonomy_id, 'post_tag' );
     649        wp_defer_term_counting( false );
     650
     651        // Ensure term count is correct.
     652        $expected = $term_count + 1;
     653        $this->assertSame( $expected, get_term( $terms[0] )->count );
     654    }
     655
     656    /**
     657     * Ensure DB queries for deferred counts are combined.
     658     *
     659     * @covers wp_modify_term_count_by
     660     * @covers wp_defer_term_counting
     661     * @ticket 51292
     662     */
     663    public function test_counts_after_deferral_matching_changes() {
     664        $post_one = self::$post_ids['publish'];
     665        $post_two = self::$post_ids['publish_two'];
     666        $terms    = self::$tag_ids;
     667
     668        wp_set_object_terms( $post_one, $terms[0], 'post_tag', true );
     669
     670        // Net gain 0:
     671        wp_defer_term_counting( true );
     672        wp_remove_object_terms( $post_one, $terms[0], 'post_tag' );
     673        wp_set_object_terms( $post_two, $terms[0], 'post_tag', true );
     674
     675        // Net gain 1:
     676        wp_set_object_terms( $post_one, $terms[1], 'post_tag', true );
     677        wp_set_object_terms( $post_two, $terms[2], 'post_tag', true );
     678
     679        // Net gain 2:
     680        wp_set_object_terms( $post_one, array( $terms[3], $terms[4] ), 'post_tag', true );
     681        wp_set_object_terms( $post_two, array( $terms[3], $terms[4] ), 'post_tag', true );
     682
     683        $num_queries = get_num_queries();
     684        wp_defer_term_counting( false );
     685
     686        /*
     687         * Each count is expected to produce two queries:
     688         * 1) The count update
     689         * 2) The SELECT in `clean_term_cache()`.
     690         */
     691        $expected = $num_queries + ( 2 * 2 );
     692        // Ensure number of queries correct.
     693        $this->assertSame( $expected, get_num_queries() );
     694    }
    186695}
Note: See TracChangeset for help on using the changeset viewer.