1

I have six regular categories, not custom (each category having between 5 and 10 posts) and I am trying to display one random post from each category on the page.

The problem with the output is getting the random posts is fine, but some posts are taken from the same category which I do not want. Here is the code I am using it:

$args_ = array(
    'posts_per_page' => 6,
    'orderby' => 'rand',
    'exclude' => $postid , // the current post ID
    'category' => $cat_id_array // here is the array of categories
);

$myposts = get_posts( $args_ );
//var_dump($myposts); // I have duplicate category here

foreach ( $myposts as $post ) : setup_postdata( $post ); ?>
    <div class="col-md-4">
        <?php the_post_thumbnail( 'medium', array( 'class' => 'img-responsive' ) );?><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
    </div>
<?php endforeach;
wp_reset_postdata();

Any help would be appreciated.

  • You'll need to loop through your cat_id_array and generate a get_posts() for each category instead of pulling posts from all those categories as you're currently doing. – Howdy_McGee Jul 7 '16 at 15:29
  • @Howdy_McGee With random ordering, that method is quite expensive to run – Pieter Goosen Jul 8 '16 at 7:32
2

Random ordering is quite expensive operations in SQL and can become a headache on very big sites. Getting a random post from a category for 6 categories will mean you might need to run 6 separate queries, each ordered randomly. This can really break the bank and leave you bankrupt.

From your answer and looking at the fact that you said you are running 30 queries, this is quite evident. Also, what really makes this expensive is that post thumbnails aren't cached for custom queries, so you are making a db call everytime you query a post thumbnail.

Lets trey a different approach, and if we are clever, we can reduce your queries to almost nothing. This is what we will do

  • Query all the posts from the db. It is here where we need to be clever as this is an extremely expensive operation. The only thing that we really need from the posts is their ID's, so we will only query the post ID's, and that will drastically reduce db queries and time spend to run the query

  • Once we have all the post ID's, we will sort them according to categories

  • We will then pick 6 random ids from the sorted array, one per category, and this will be passed to our "main" query which will output the posts.

The first two operations will only require 2 or three queries, this you can verify with a plugin like Query Monitor

Lets look at some code: (NOTE: All code is untested and requires PHP 5.4+)

THE QUERY

// Lets get all the post ids
$args = [
    'posts_per_page' => -1,
    'fields'         => 'ids' // Only get the post ID's
];
$ids_array = get_posts( $args );

// Make sure we have a valid array of ids
if ( $ids_array ) {
    // Lets update the post term cache
    update_object_term_cache( $ids_array, 'post' );

    // Loop through the post ids and create an array with post ids and terms
    $id_and_term_array = [];
    foreach ( $ids_array as $id ) {
        // Get all the post categories
        $terms = get_the_terms( $id, 'category' );

        // Make sure we have a valid array of terms
        if (    $terms
             && !is_wp_error( $terms )
        ) {
            // Loop through the terms and create our array with post ids and term
            foreach ( $terms as $term )
                $id_and_term_array[$term->term_id][] = $id;
        }
    }

    // TO BE CONTINUED......
}

Great, $id_and_term_array should now contain an array with arrays where the keys are term ids and the values are an array of post ids. We have created all of this without breaking the bank. If you check query monitor, you will see all of this required only 2 db calls in about no time. We have slightly abused memory, but we will later look at something to avoid eating up memory.

SELECTING RANDOM ID'S

What we will do next is to loop through $id_and_term_array and pick a random post id from each term id array. We also need to exclude the current post id and need to avoid duplicate post id's which we will certainly have if posts belongs to more than one term.

So lets continue where we last placed TO BE CONTINUED....

// Make sure we have a valid array
if ( $id_and_term_array ) {
    // Get the current post ID
    $current_post_id = get_the_ID();
    // If this is a single post page, we can do
    // $current_post_id = $GLOBALS['wp_the_query']->get_queried_object_id();

    // Lets loop through $id_and_term_array
    $unique_rand_array = [];
    foreach ( $id_and_term_array as $value ) {
        // Shuffle the $value array to randomize it
        shuffle ( $value );

        // Loop through $value and get the first post id
        foreach ( $value as $v ) {
            // Skip the post ID if it mathes the current post or if it is a duplicate
            if ( $v == $current_post_id )
                continue;

            if ( in_array( $v, $unique_rand_array ) )
                continue;

            // We have a unique id, lets store it and bail
            $unique_rand_array[] = $v;

            break;
        }
    }
}

We now have an array with x amount of unique post id's which is stored in $unique_rand_array. We can now pass that array of id's to our final query

FINAL QUERY

// First see if we have post ids in array
if ( $unique_rand_array ) {
    // Lets run our query
    $final_args = [
        'posts_per_page' => 6,
        'post__in'       => shuffle( $unique_rand_array ), // Randomize posts
        'orderby'        => 'post__in' // Keep post order the same as the order of post ids
    ];
    $q = new WP_Query( $final_args );

    // Lets cache our post thumbnails
    update_post_thumbnail_cache( $q );

    while ( $q->have_posts() ) :
        $q->the_post();

        ?>
        <div class="col-md-4">
            <?php the_post_thumbnail( 'medium', array( 'class' => 'img-responsive' ) );?>
            <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
        </div>
        <?php

    endwhile;
    wp_reset_postdata();
}

TRANSIENTS

One last thing we can do is to savethe results for our first query in transient because we do not want to run that query on every page load. This will reduce memory abuse and avoid. We will also delete our transient when we publish a new post

ALL TOGETHER NOW

Let put everything together.

// Check if we have a transient
if ( false === ( $id_and_term_array = get_transient( 'random_term_post_ids' ) ) ) {

    // Lets get all the post ids
    $args = [
        'posts_per_page' => -1,
        'fields'         => 'ids' // Only get the post ID's
    ];
    $ids_array = get_posts( $args );

    // Define the array which will hold the term and post_ids
    $id_and_term_array = [];
    // Make sure we have a valid array of ids
    if ( $ids_array ) {
        // Lets update the post term cache
        update_object_term_cache( $ids_array, 'post' );

        // Loop through the post ids and create an array with post ids and terms
        foreach ( $ids_array as $id ) {
            // Get all the post categories
            $terms = get_the_terms( $id, 'category' );

            // Make sure we have a valid array of terms
            if (    $terms
                 && !is_wp_error( $terms )
            ) {
                // Loop through the terms and create our array with post ids and term
                foreach ( $terms as $term )
                    $id_and_term_array[$term->term_id][] = $id;
            }
        }
    }
    // Set our transient for 30 days
    set_transient( 'random_term_post_ids', $id_and_term_array, 30 * DAYS_IN_SECONDS );
}

// Make sure we have a valid array
if ( $id_and_term_array ) {
    // Get the current post ID
    $current_post_id = get_the_ID();
    // If this is a single post page, we can do
    // $current_post_id = $GLOBALS['wp_the_query']->get_queried_object_id();

    // Lets loop through $id_and_term_array
    $unique_rand_array = [];
    foreach ( $id_and_term_array as $value ) {
        // Shuffle the $value array to randomize it
        shuffle ( $value );

        // Loop through $value and get the first post id
        foreach ( $value as $v ) {
            // Skip the post ID if it mathes the current post or if it is a duplicate
            if ( $v == $current_post_id )
                continue;

            if ( in_array( $v, $unique_rand_array ) )
                continue;

            // We have a unique id, lets store it and bail
            $unique_rand_array[] = $v;

            break;
        }
    }

    // First see if we have post ids in array
    if ( $unique_rand_array ) {
        // Lets run our query
        $final_args = [
            'posts_per_page' => 6,
            'post__in'       => shuffle( $unique_rand_array ), // Randomize posts
            'orderby'        => 'post__in' // Keep post order the same as the order of post ids
        ];
        $q = new WP_Query( $final_args );

        // Lets cache our post thumbnails
        update_post_thumbnail_cache( $q );

        while ( $q->have_posts() ) :
            $q->the_post();

            ?>
            <div class="col-md-4">
                <?php the_post_thumbnail( 'medium', array( 'class' => 'img-responsive' ) );?>
                <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
            </div>
            <?php

        endwhile;
        wp_reset_postdata();
    }
}

Finally, the following code goes into your functions file. This will flush the transient when a new post is published, a post is deleted, updated or undeleted

add_action( 'transition_post_status', function () 
{
   delete_transient( 'random_term_post_ids' );
});
  • This is pretty neat! I'm just wondering though, wouldn't update_object_term_cache() with -1 posts eventually be more expensive on the database than 30 small queries? – Howdy_McGee Jul 8 '16 at 14:44
  • Thanks a lot for the nice work and explanations. You are right, memory and query the db is expensive. I will make a little add-on such as switching from the_post_thumbnail which has 2 queries against db with get post meta. The last one cost you ZERO. Of course you should add it first. Again thanks a lot – cadobe Jul 9 '16 at 2:36
  • 1
    @Howdy_McGee no, it will always just be one db call. This is the same method wich WP_Query internally use to cache post terms. I have done something similar a while ago with test results. As you'll see, apart from the "main" query to actually query the six posts you need, the query to get all the post ids and terms will always just be 2 db queries in super quicktime. The transient is just there to avoid the punch on memory. You should actually test this yourself ;-) – Pieter Goosen Jul 9 '16 at 7:53
1

I just fix it using bueltge sugestion such us:

foreach ( $cat_id_array as $cat ) :

    $args =  array( 'posts_per_page' => 1, 'cat' => $cat );
    $myposts = get_posts( $args );
    //var_dump($myposts);

    foreach($myposts as $posts) :
         var_dump($posts->ID);

    endforeach;
endforeach;

which is working fine now. Thanks for the heads up. DO I have any chance to reduce the queries number for this particular code? It is showing as 30 queries

  • Yes, this is quite an expensive operation, and with random ordering it is even more expensive. See my answer. The code is untested and might be buggy, but will definitely give you far better results – Pieter Goosen Jul 8 '16 at 7:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.