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/src/wp-includes/taxonomy.php

    r49108 r49141  
    337337 * @since 5.4.0 Added the registered taxonomy object as a return value.
    338338 * @since 5.5.0 Introduced `default_term` argument.
     339 * @since 5.6.0 Introduced `update_count_by_callback` argument.
    339340 *
    340341 * @global array $wp_taxonomies Registered taxonomies.
     
    345346 *     Optional. Array or query string of arguments for registering a taxonomy.
    346347 *
    347  *     @type array         $labels                An array of labels for this taxonomy. By default, Tag labels are
    348  *                                                used for non-hierarchical taxonomies, and Category labels are used
    349  *                                                for hierarchical taxonomies. See accepted values in
    350  *                                                get_taxonomy_labels(). Default empty array.
    351  *     @type string        $description           A short descriptive summary of what the taxonomy is for. Default empty.
    352  *     @type bool          $public                Whether a taxonomy is intended for use publicly either via
    353  *                                                the admin interface or by front-end users. The default settings
    354  *                                                of `$publicly_queryable`, `$show_ui`, and `$show_in_nav_menus`
    355  *                                                are inherited from `$public`.
    356  *     @type bool          $publicly_queryable    Whether the taxonomy is publicly queryable.
    357  *                                                If not set, the default is inherited from `$public`
    358  *     @type bool          $hierarchical          Whether the taxonomy is hierarchical. Default false.
    359  *     @type bool          $show_ui               Whether to generate and allow a UI for managing terms in this taxonomy in
    360  *                                                the admin. If not set, the default is inherited from `$public`
    361  *                                                (default true).
    362  *     @type bool          $show_in_menu          Whether to show the taxonomy in the admin menu. If true, the taxonomy is
    363  *                                                shown as a submenu of the object type menu. If false, no menu is shown.
    364  *                                                `$show_ui` must be true. If not set, default is inherited from `$show_ui`
    365  *                                                (default true).
    366  *     @type bool          $show_in_nav_menus     Makes this taxonomy available for selection in navigation menus. If not
    367  *                                                set, the default is inherited from `$public` (default true).
    368  *     @type bool          $show_in_rest          Whether to include the taxonomy in the REST API. Set this to true
    369  *                                                for the taxonomy to be available in the block editor.
    370  *     @type string        $rest_base             To change the base url of REST API route. Default is $taxonomy.
    371  *     @type string        $rest_controller_class REST API Controller class name. Default is 'WP_REST_Terms_Controller'.
    372  *     @type bool          $show_tagcloud         Whether to list the taxonomy in the Tag Cloud Widget controls. If not set,
    373  *                                                the default is inherited from `$show_ui` (default true).
    374  *     @type bool          $show_in_quick_edit    Whether to show the taxonomy in the quick/bulk edit panel. It not set,
    375  *                                                the default is inherited from `$show_ui` (default true).
    376  *     @type bool          $show_admin_column     Whether to display a column for the taxonomy on its post type listing
    377  *                                                screens. Default false.
    378  *     @type bool|callable $meta_box_cb           Provide a callback function for the meta box display. If not set,
    379  *                                                post_categories_meta_box() is used for hierarchical taxonomies, and
    380  *                                                post_tags_meta_box() is used for non-hierarchical. If false, no meta
    381  *                                                box is shown.
    382  *     @type callable      $meta_box_sanitize_cb  Callback function for sanitizing taxonomy data saved from a meta
    383  *                                                box. If no callback is defined, an appropriate one is determined
    384  *                                                based on the value of `$meta_box_cb`.
     348 *     @type array         $labels                   An array of labels for this taxonomy. By default, Tag labels are
     349 *                                                   used for non-hierarchical taxonomies, and Category labels are used
     350 *                                                   for hierarchical taxonomies. See accepted values in
     351 *                                                   get_taxonomy_labels(). Default empty array.
     352 *     @type string        $description              A short descriptive summary of what the taxonomy is for. Default empty.
     353 *     @type bool          $public                   Whether a taxonomy is intended for use publicly either via
     354 *                                                   the admin interface or by front-end users. The default settings
     355 *                                                   of `$publicly_queryable`, `$show_ui`, and `$show_in_nav_menus`
     356 *                                                   are inherited from `$public`.
     357 *     @type bool          $publicly_queryable       Whether the taxonomy is publicly queryable.
     358 *                                                   If not set, the default is inherited from `$public`
     359 *     @type bool          $hierarchical             Whether the taxonomy is hierarchical. Default false.
     360 *     @type bool          $show_ui                  Whether to generate and allow a UI for managing terms in this taxonomy in
     361 *                                                   the admin. If not set, the default is inherited from `$public`
     362 *                                                   (default true).
     363 *     @type bool          $show_in_menu             Whether to show the taxonomy in the admin menu. If true, the taxonomy is
     364 *                                                   shown as a submenu of the object type menu. If false, no menu is shown.
     365 *                                                   `$show_ui` must be true. If not set, default is inherited from `$show_ui`
     366 *                                                   (default true).
     367 *     @type bool          $show_in_nav_menus        Makes this taxonomy available for selection in navigation menus. If not
     368 *                                                   set, the default is inherited from `$public` (default true).
     369 *     @type bool          $show_in_rest             Whether to include the taxonomy in the REST API. Set this to true
     370 *                                                   for the taxonomy to be available in the block editor.
     371 *     @type string        $rest_base                To change the base url of REST API route. Default is $taxonomy.
     372 *     @type string        $rest_controller_class    REST API Controller class name. Default is 'WP_REST_Terms_Controller'.
     373 *     @type bool          $show_tagcloud            Whether to list the taxonomy in the Tag Cloud Widget controls. If not set,
     374 *                                                   the default is inherited from `$show_ui` (default true).
     375 *     @type bool          $show_in_quick_edit       Whether to show the taxonomy in the quick/bulk edit panel. It not set,
     376 *                                                   the default is inherited from `$show_ui` (default true).
     377 *     @type bool          $show_admin_column        Whether to display a column for the taxonomy on its post type listing
     378 *                                                   screens. Default false.
     379 *     @type bool|callable $meta_box_cb              Provide a callback function for the meta box display. If not set,
     380 *                                                   post_categories_meta_box() is used for hierarchical taxonomies, and
     381 *                                                   post_tags_meta_box() is used for non-hierarchical. If false, no meta
     382 *                                                   box is shown.
     383 *     @type callable      $meta_box_sanitize_cb     Callback function for sanitizing taxonomy data saved from a meta
     384 *                                                   box. If no callback is defined, an appropriate one is determined
     385 *                                                   based on the value of `$meta_box_cb`.
    385386 *     @type array         $capabilities {
    386387 *         Array of capabilities for this taxonomy.
     
    400401 *         @type int    $ep_mask      Assign an endpoint mask. Default `EP_NONE`.
    401402 *     }
    402  *     @type string|bool   $query_var             Sets the query var key for this taxonomy. Default `$taxonomy` key. If
    403  *                                                false, a taxonomy cannot be loaded at `?{query_var}={term_slug}`. If a
    404  *                                                string, the query `?{query_var}={term_slug}` will be valid.
    405  *     @type callable      $update_count_callback Works much like a hook, in that it will be called when the count is
    406  *                                                updated. Default _update_post_term_count() for taxonomies attached
    407  *                                                to post types, which confirms that the objects are published before
    408  *                                                counting them. Default _update_generic_term_count() for taxonomies
    409  *                                                attached to other object types, such as users.
     403 *     @type string|bool   $query_var                Sets the query var key for this taxonomy. Default `$taxonomy` key. If
     404 *                                                   false, a taxonomy cannot be loaded at `?{query_var}={term_slug}`. If a
     405 *                                                   string, the query `?{query_var}={term_slug}` will be valid.
     406 *     @type callable      $update_count_callback    Works much like a hook, in that it will be called when the count is
     407 *                                                   updated. Default _update_post_term_count() for taxonomies attached
     408 *                                                   to post types, which confirms that the objects are published before
     409 *                                                   counting them. Default _update_generic_term_count() for taxonomies
     410 *                                                   attached to other object types, such as users.
     411 *     @type callable      $update_count_by_callback Works much like a hook, in that it will be called when the count is
     412 *                                                   incremented or decremented. Defaults to the value of `$update_count_callback` if
     413 *                                                   a custom callack is defined, otherwise uses wp_modify_term_count_by().
    410414 *     @type string|array  $default_term {
    411415 *         Default term to be used for the taxonomy.
     
    415419 *         @type string $description  Description for default term. Default empty.
    416420 *     }
    417  *     @type bool          $_builtin              This taxonomy is a "built-in" taxonomy. INTERNAL USE ONLY!
    418  *                                                Default false.
     421 *     @type bool          $_builtin                 This taxonomy is a "built-in" taxonomy. INTERNAL USE ONLY!
     422 *                                                   Default false.
    419423 * }
    420424 * @return WP_Taxonomy|WP_Error The registered taxonomy object on success, WP_Error object on failure.
     
    25622566    }
    25632567
     2568    $taxonomy_object = get_taxonomy( $taxonomy );
     2569
     2570    $object_types = (array) $taxonomy_object->object_type;
     2571    foreach ( $object_types as &$object_type ) {
     2572        if ( 0 === strpos( $object_type, 'attachment:' ) ) {
     2573            list( $object_type ) = explode( ':', $object_type );
     2574        }
     2575    }
     2576
     2577    if ( array_filter( $object_types, 'post_type_exists' ) !== $object_types ) {
     2578        // This taxonomy applies to non-posts, count changes now.
     2579        $do_recount = ! _wp_prevent_term_counting();
     2580    } elseif ( 'publish' === get_post_status( $object_id ) ) {
     2581        // Published post, count changes now.
     2582        $do_recount = ! _wp_prevent_term_counting();
     2583    } else {
     2584        $do_recount = false;
     2585    }
     2586
    25642587    if ( ! is_array( $terms ) ) {
    25652588        $terms = array( $terms );
     
    26472670    }
    26482671
    2649     if ( $new_tt_ids ) {
    2650         wp_update_term_count( $new_tt_ids, $taxonomy );
     2672    if ( $new_tt_ids && $do_recount ) {
     2673        wp_increment_term_count( $new_tt_ids, $taxonomy );
    26512674    }
    26522675
     
    26662689    }
    26672690
    2668     $t = get_taxonomy( $taxonomy );
    2669 
    2670     if ( ! $append && isset( $t->sort ) && $t->sort ) {
     2691    if ( ! $append && isset( $taxonomy_object->sort ) && $taxonomy_object->sort ) {
    26712692        $values     = array();
    26722693        $term_order = 0;
     
    27472768    if ( ! taxonomy_exists( $taxonomy ) ) {
    27482769        return new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
     2770    }
     2771
     2772    $taxonomy_object = get_taxonomy( $taxonomy );
     2773
     2774    $object_types = (array) $taxonomy_object->object_type;
     2775    foreach ( $object_types as &$object_type ) {
     2776        if ( 0 === strpos( $object_type, 'attachment:' ) ) {
     2777            list( $object_type ) = explode( ':', $object_type );
     2778        }
     2779    }
     2780
     2781    if ( array_filter( $object_types, 'post_type_exists' ) !== $object_types ) {
     2782        // This taxonomy applies to non-posts, count changes now.
     2783        $do_recount = ! _wp_prevent_term_counting();
     2784    } elseif (
     2785        'publish' === get_post_status( $object_id ) ||
     2786        (
     2787            'inherit' === get_post_status( $object_id ) &&
     2788            'publish' === get_post_status( wp_get_post_parent_id( $object_id ) )
     2789        )
     2790    ) {
     2791        // Published post, count changes now.
     2792        $do_recount = ! _wp_prevent_term_counting();
     2793    } else {
     2794        $do_recount = false;
    27492795    }
    27502796
     
    28072853        do_action( 'deleted_term_relationships', $object_id, $tt_ids, $taxonomy );
    28082854
    2809         wp_update_term_count( $tt_ids, $taxonomy );
     2855        if ( $do_recount ) {
     2856            wp_decrement_term_count( $tt_ids, $taxonomy );
     2857        }
    28102858
    28112859        return (bool) $deleted;
     
    32273275        // Flush any deferred counts.
    32283276        if ( ! $defer ) {
     3277            wp_modify_term_count_by( null, null, null, true );
    32293278            wp_update_term_count( null, null, true );
    32303279        }
     
    32323281
    32333282    return $_defer;
     3283}
     3284
     3285/**
     3286 * Prevents add/removing a term from modifying a term count.
     3287 *
     3288 * This is used by functions calling wp_transition_post_status() to indicate the
     3289 * term count will be handled during the post's transition.
     3290 *
     3291 * @private
     3292 * @since 5.6.0
     3293 *
     3294 * @param bool $new_setting The new setting for preventing term counts.
     3295 * @return bool Whether term count prevention is enabled or disabled.
     3296 */
     3297function _wp_prevent_term_counting( $new_setting = null ) {
     3298    static $prevent = false;
     3299
     3300    if ( is_bool( $new_setting ) ) {
     3301        $prevent = $new_setting;
     3302    }
     3303
     3304    return $prevent;
     3305}
     3306
     3307/**
     3308 * Increments the amount of terms in taxonomy.
     3309 *
     3310 * If there is a taxonomy callback applied, then it will be called for updating
     3311 * the count.
     3312 *
     3313 * The default action is to increment the count by one and update the database.
     3314 *
     3315 * @since 5.6.0
     3316 *
     3317 * @param int|array $tt_ids       The term_taxonomy_id of the terms.
     3318 * @param string    $taxonomy     The context of the term.
     3319 * @param int       $increment_by By how many the term count is to be incremented. Default 1.
     3320 * @param bool      $do_deferred  Whether to flush the deferred term counts too. Default false.
     3321 * @return bool If no terms will return false, and if successful will return true.
     3322 */
     3323function wp_increment_term_count( $tt_ids, $taxonomy, $increment_by = 1, $do_deferred = false ) {
     3324    return wp_modify_term_count_by( $tt_ids, $taxonomy, $increment_by, $do_deferred );
     3325}
     3326
     3327/**
     3328 * Decrements the amount of terms in taxonomy.
     3329 *
     3330 * If there is a taxonomy callback applied, then it will be called for updating
     3331 * the count.
     3332 *
     3333 * The default action is to decrement the count by one and update the database.
     3334 *
     3335 * @since 5.6.0
     3336 *
     3337 * @param int|array $tt_ids       The term_taxonomy_id of the terms.
     3338 * @param string    $taxonomy     The context of the term.
     3339 * @param int       $decrement_by By how many the term count is to be decremented. Default 1.
     3340 * @param bool      $do_deferred  Whether to flush the deferred term counts too. Default false.
     3341 * @return bool If no terms will return false, and if successful will return true.
     3342 */
     3343function wp_decrement_term_count( $tt_ids, $taxonomy, $decrement_by = 1, $do_deferred = false ) {
     3344    return wp_modify_term_count_by( $tt_ids, $taxonomy, $decrement_by * -1, $do_deferred );
     3345}
     3346
     3347/**
     3348 * Modifies the amount of terms in taxonomy.
     3349 *
     3350 * If there is a taxonomy callback applied, then it will be called for updating
     3351 * the count.
     3352 *
     3353 * The default action is to decrement the count by one and update the database.
     3354 *
     3355 * @since 5.6.0
     3356 *
     3357 * @param int|array $tt_ids      The term_taxonomy_id of the terms.
     3358 * @param string    $taxonomy    The context of the term.
     3359 * @param int       $modify_by   By how many the term count is to be modified.
     3360 * @param bool      $do_deferred Whether to flush the deferred term counts too. Default false.
     3361 * @return bool If no terms will return false, and if successful will return true.
     3362 */
     3363function wp_modify_term_count_by( $tt_ids, $taxonomy, $modify_by, $do_deferred = false ) {
     3364    static $_deferred = array();
     3365
     3366    if ( $do_deferred ) {
     3367        foreach ( (array) $_deferred as $taxonomy_name => $modifications ) {
     3368            $tax_by_count = array_reduce(
     3369                array_keys( $modifications ),
     3370                function( $by_count, $tt_id ) use ( $modifications ) {
     3371                    if ( ! isset( $by_count[ $modifications[ $tt_id ] ] ) ) {
     3372                        $by_count[ $modifications[ $tt_id ] ] = array();
     3373                    }
     3374                    $by_count[ $modifications[ $tt_id ] ][] = $tt_id;
     3375                    return $by_count;
     3376                },
     3377                array()
     3378            );
     3379
     3380            foreach ( $tax_by_count as $_modify_by => $_tt_ids ) {
     3381                wp_modify_term_count_by_now( $_tt_ids, $taxonomy_name, $_modify_by );
     3382            }
     3383            unset( $_deferred[ $taxonomy_name ] );
     3384        }
     3385    }
     3386
     3387    if ( empty( $tt_ids ) ) {
     3388        return false;
     3389    }
     3390
     3391    if ( ! is_array( $tt_ids ) ) {
     3392        $tt_ids = array( $tt_ids );
     3393    }
     3394
     3395    if ( wp_defer_term_counting() ) {
     3396        foreach ( $tt_ids as $tt_id ) {
     3397            if ( ! isset( $_deferred[ $taxonomy ][ $tt_id ] ) ) {
     3398                $_deferred[ $taxonomy ][ $tt_id ] = 0;
     3399            }
     3400            $_deferred[ $taxonomy ][ $tt_id ] += $modify_by;
     3401        }
     3402        return true;
     3403    }
     3404
     3405    return wp_modify_term_count_by_now( $tt_ids, $taxonomy, $modify_by );
     3406}
     3407
     3408/**
     3409 * Modifies the amount of terms in taxonomy immediately
     3410 *
     3411 * If there is a taxonomy callback applied, then it will be called for updating
     3412 * the count.
     3413 *
     3414 * The default action is to decrement the count by one and update the database.
     3415 *
     3416 * @since 5.6.0
     3417 *
     3418 * @param int|array $tt_ids      The term_taxonomy_id of the terms.
     3419 * @param string    $taxonomy    The context of the term.
     3420 * @param int       $modify_by   By how many the term count is to be modified.
     3421 * @return bool If no terms will return false, and if successful will return true.
     3422 */
     3423function wp_modify_term_count_by_now( $tt_ids, $taxonomy, $modify_by ) {
     3424    global $wpdb;
     3425
     3426    if ( 0 === $modify_by ) {
     3427        return false;
     3428    }
     3429
     3430    $tt_ids = array_filter( array_map( 'intval', (array) $tt_ids ) );
     3431
     3432    if ( empty( $tt_ids ) ) {
     3433        return false;
     3434    }
     3435
     3436    $taxonomy = get_taxonomy( $taxonomy );
     3437    if ( ! empty( $taxonomy->update_count_by_callback ) ) {
     3438        call_user_func( $taxonomy->update_count_by_callback, $tt_ids, $taxonomy, $modify_by );
     3439        clean_term_cache( $tt_ids, '', false );
     3440        return true;
     3441    }
     3442
     3443    $tt_ids_string = '(' . implode( ',', $tt_ids ) . ')';
     3444
     3445    foreach ( $tt_ids as $tt_id ) {
     3446        /** This action is documented in wp-includes/taxonomy.php */
     3447        do_action( 'edit_term_taxonomy', $tt_id, $taxonomy );
     3448    }
     3449
     3450    $result = $wpdb->query(
     3451        $wpdb->prepare(
     3452            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     3453            "UPDATE {$wpdb->term_taxonomy} AS tt SET tt.count = GREATEST( 0, tt.count + %d ) WHERE tt.term_taxonomy_id IN $tt_ids_string",
     3454            $modify_by
     3455        )
     3456    );
     3457
     3458    if ( ! $result ) {
     3459        return false;
     3460    }
     3461
     3462    foreach ( $tt_ids as $tt_id ) {
     3463        /** This action is documented in wp-includes/taxonomy.php */
     3464        do_action( 'edited_term_taxonomy', $tt_id, $taxonomy );
     3465    }
     3466
     3467    clean_term_cache( $tt_ids, '', false );
     3468
     3469    return true;
    32343470}
    32353471
Note: See TracChangeset for help on using the changeset viewer.