Query random post from different categories

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="https://wordpress.stackexchange.com/questions/231711/<?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="https://wordpress.stackexchange.com/questions/231711/<?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' );
});