Make WordPress Core

Changeset 59993


Ignore:
Timestamp:
03/16/2025 10:55:11 PM (2 months ago)
Author:
peterwilsoncc
Message:

Query: Fix performance regression starting the loop for all fields.

Fixes a performance regression starting the loop after calling WP_Query( [ 'fields' => 'all' ] ). This changes how WP_Query::the_post() determines whether there is a need to traverse the posts for cache warming.

If IDs are queried, WP_Query::$posts is assumed to be an array of post IDs. If all fields are queried, WP_Query::$posts is assumed to be an array of fully populated post objects.

Follow up to [59919], [59937].

Props joemcgill, peterwilsoncc, SirLouen.
Fixes #56992.

Location:
trunk
Files:
2 edited

Legend:

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

    r59976 r59993  
    20682068                $fields = "{$wpdb->posts}.ID, {$wpdb->posts}.post_parent";
    20692069                break;
     2070            case '':
     2071                /*
     2072                 * Set the default to 'all'.
     2073                 *
     2074                 * This is used in `WP_Query::the_post` to determine if the
     2075                 * entire post object has been queried.
     2076                 */
     2077                $q['fields'] = 'all';
     2078                // Falls through.
    20702079            default:
    20712080                $fields = "{$wpdb->posts}.*";
     
    37403749
    37413750        if ( ! $this->in_the_loop ) {
    3742             // Get post IDs to prime incomplete post objects.
    3743             $post_ids = array_reduce(
    3744                 $this->posts,
    3745                 function ( $carry, $post ) {
    3746                     if ( is_numeric( $post ) && $post > 0 ) {
    3747                         // Query for post ID.
    3748                         $carry[] = $post;
    3749                     }
    3750 
    3751                     if ( is_object( $post ) && isset( $post->ID ) ) {
    3752                         // Query for object, either WP_Post or stdClass.
    3753                         $carry[] = $post->ID;
    3754                     }
    3755 
    3756                     return $carry;
    3757                 },
    3758                 array()
    3759             );
    3760             if ( $post_ids ) {
     3751            if ( 'all' === $this->query_vars['fields'] ) {
     3752                // Full post objects queried.
     3753                $post_objects = $this->posts;
     3754            } else {
     3755                if ( 'ids' === $this->query_vars['fields'] ) {
     3756                    // Post IDs queried.
     3757                    $post_ids = $this->posts;
     3758                } else {
     3759                    // Only partial objects queried, need to prime the cache for the loop.
     3760                    $post_ids = array_reduce(
     3761                        $this->posts,
     3762                        function ( $carry, $post ) {
     3763                            if ( isset( $post->ID ) ) {
     3764                                $carry[] = $post->ID;
     3765                            }
     3766
     3767                            return $carry;
     3768                        },
     3769                        array()
     3770                    );
     3771                }
    37613772                _prime_post_caches( $post_ids, $this->query_vars['update_post_term_cache'], $this->query_vars['update_post_meta_cache'] );
    3762             }
    3763             $post_objects = array_map( 'get_post', $post_ids );
     3773                $post_objects = array_map( 'get_post', $post_ids );
     3774            }
    37643775            update_post_author_caches( $post_objects );
    37653776        }
     
    37823793
    37833794        // Ensure a full post object is available.
    3784         if ( $post instanceof stdClass ) {
    3785             // stdClass indicates that a partial post object was queried.
    3786             $post = get_post( $post->ID );
    3787         } elseif ( is_numeric( $post ) ) {
    3788             // Numeric indicates that only post IDs were queried.
    3789             $post = get_post( $post );
     3795        if ( 'all' !== $this->query_vars['fields'] ) {
     3796            if ( 'ids' === $this->query_vars['fields'] ) {
     3797                // Post IDs queried.
     3798                $post = get_post( $post );
     3799            } elseif ( isset( $post->ID ) ) {
     3800                /*
     3801                 * Partial objecct queried.
     3802                 *
     3803                 * The post object was queried with a partial set of
     3804                 * fields, populate the entire object for the loop.
     3805                 */
     3806                $post = get_post( $post->ID );
     3807            }
    37903808        }
    37913809
  • trunk/tests/phpunit/tests/query/thePost.php

    r59937 r59993  
    5050
    5151    /**
     52     * Ensure custom 'fields' values are respected.
     53     *
     54     * @ticket 56992
     55     */
     56    public function test_wp_query_respects_custom_fields_values() {
     57        global $wpdb;
     58        add_filter(
     59            'posts_fields',
     60            function ( $fields, $query ) {
     61                global $wpdb;
     62
     63                if ( $query->get( 'fields' ) === 'custom' ) {
     64                    $fields = "$wpdb->posts.ID,$wpdb->posts.post_author";
     65                }
     66
     67                return $fields;
     68            },
     69            10,
     70            2
     71        );
     72
     73        $query = new WP_Query(
     74            array(
     75                'fields'    => 'custom',
     76                'post_type' => 'page',
     77                'post__in'  => self::$page_child_ids,
     78            )
     79        );
     80
     81        $this->assertNotEmpty( $query->posts, 'The query is expected to return results' );
     82        $this->assertSame( $query->get( 'fields' ), 'custom', 'The WP_Query class is expected to use the custom fields value' );
     83        $this->assertStringContainsString( "$wpdb->posts.ID,$wpdb->posts.post_author", $query->request, 'The database query is expected to use the custom fields value' );
     84    }
     85
     86    /**
     87     * Ensure custom 'fields' populates the global post in the loop.
     88     *
     89     * @ticket 56992
     90     */
     91    public function test_wp_query_with_custom_fields_value_populates_the_global_post() {
     92        global $wpdb;
     93        add_filter(
     94            'posts_fields',
     95            function ( $fields, $query ) {
     96                global $wpdb;
     97
     98                if ( $query->get( 'fields' ) === 'custom' ) {
     99                    $fields = "$wpdb->posts.ID,$wpdb->posts.post_author";
     100                }
     101
     102                return $fields;
     103            },
     104            10,
     105            2
     106        );
     107
     108        $query = new WP_Query(
     109            array(
     110                'fields'    => 'custom',
     111                'post_type' => 'page',
     112                'post__in'  => self::$page_child_ids,
     113                'orderby'   => 'id',
     114                'order'     => 'ASC',
     115            )
     116        );
     117
     118        $query->the_post();
     119
     120        // Get the global post and specific post.
     121        $global_post   = get_post();
     122        $specific_post = get_post( self::$page_child_ids[0], ARRAY_A );
     123
     124        $this->assertSameSetsWithIndex( $specific_post, $global_post->to_array(), 'The global post is expected to be fully populated.' );
     125
     126        $this->assertNotEmpty( get_the_title(), 'The title is expected to be populated.' );
     127        $this->assertNotEmpty( get_the_content(), 'The content is expected to be populated.' );
     128        $this->assertNotEmpty( get_the_excerpt(), 'The excerpt is expected to be populated.' );
     129    }
     130
     131    /**
    52132     * Ensure that a secondary loop populates the global post completely regardless of the fields parameter.
    53133     *
     
    76156        $specific_post = get_post( self::$page_child_ids[0], ARRAY_A );
    77157
     158        $this->assertSameSetsWithIndex( $specific_post, $global_post->to_array(), 'The global post is expected to be fully populated.' );
     159
    78160        $this->assertNotEmpty( get_the_title(), 'The title is expected to be populated.' );
    79161        $this->assertNotEmpty( get_the_content(), 'The content is expected to be populated.' );
    80162        $this->assertNotEmpty( get_the_excerpt(), 'The excerpt is expected to be populated.' );
    81 
    82         $this->assertSameSetsWithIndex( $specific_post, $global_post->to_array(), 'The global post is expected to be fully populated.' );
    83163    }
    84164
Note: See TracChangeset for help on using the changeset viewer.