7

Working on a site that has a lot of posts, I need to display 3 posts from a particular category, but all of them need to be from the latest 10 published on the site. I can either grab 3 completely random posts (which tends to pull posts that are very old) or grab 10 posts (but I don't know how to then randomize the order and only display 3).

So far, I have this query:

$args = array(
    'post_type' => 'post',
    'category_name' => 'mycategory',
    'posts_per_page' => 10,
    'orderby' => 'date',
    'order' => 'DESC',
    'meta_key' => '_thumbnail_id',
    'no_found_rows' => 'true'
);
$query = new WP_Query( $args );

along with this attempt to get 3 random posts from the 10 queried:

$randomPosts = shuffle( $query ); 
$randomPosts = array_slice( $randomPosts, 0, 3 );

But treating the results as an array doesn't work, since it's actually an object.
My only other thought is to use 'posts_per_page' = 3 with 'orderby' => 'rand' to grab 3 random posts and add a 'date_query' to restrict it to the past 6 months. That would be close, but it would be preferable to restrict the query to the 10 most recent posts (they may all be published 3 days ago or 5 months ago, they are published together in uneven spurts).

What is the best approach?
Query the 10 latest posts as I'm doing, then convert the object to an array, shuffle, and slice, and convert it back to an object, or is there a simpler, more efficient way to accomplish the goal?

12

There's one way with:

$args = [
    'post_type'             => 'post',
    'posts_per_page'        => 10,
    'orderby'               => 'date',
    'order'                 => 'DESC',
    'no_found_rows'         => 'true',
    '_shuffle_and_pick'     => 3 // <-- our custom argument
];

$query = new \WP_Query( $args );

where the custom _shuffle_and_pick attribute is supported by this demo plugin:

<?php
/**
 * Plugin Name: Support for the _shuffle_and_pick WP_Query argument.
 */
add_filter( 'the_posts', function( $posts, \WP_Query $query )
{
    if( $pick = $query->get( '_shuffle_and_pick' ) )
    {
        shuffle( $posts );
        $posts = array_slice( $posts, 0, (int) $pick );
    }
    return $posts;
}, 10, 2 );
  • Fantastic, that did exactly what I needed. Thank you! – WebElaine Mar 20 '17 at 14:28
  • Glad to hear it worked for you @WebElaine – birgire Mar 20 '17 at 14:31
  • This is actually really simple and neat. I hope my clients will never come across you as I will probably be out of work. haha – Christine Cooper Mar 21 '17 at 16:33
  • 1
    This is awesome - works like a charm, thanks! BTW - is the backslash before WP_Query a typo or is that some helpful trick I don't know about. – squarecandy Dec 21 '17 at 19:07
  • 1
    @squarecandy thanks for your comment. When we use a custom namespace in our plugin, then we need to "backslash" the WP_Query class as there is no special WordPress namespace. – birgire Dec 21 '17 at 19:43
5

You can obviously take all the posts and randomize the result with PHP like shown in this answer. Or, you can do the randomization with SQL as well.

Handling the randomization in Database:

There is no built in WordPress function (or argument) to achieve that, however, you may use the posts_request filter to alter the original SQL query set by WP_Query to achieve the randomization from the database alone.

You may use the following CODE in the active theme's functions.php file or as a new custom plugin:

<?php
/**
 *  Plugin Name: Randomize Posts
 *  Plugin URI: https://wordpress.stackexchange.com/a/260877/110572
 *  Description: Randomize posts basd on '_randomize_posts_count' query argument
 *  Author: Fayaz
 *  Version: 1.0
 *  Author URI: http://fmy.me/
 */

function wpse260713_randomize_posts( $sql_query, $query ) {
    $rand = (int) $query->get( '_randomize_posts_count' );
    if( $rand ) {
        $found_rows = '';
        if( stripos( $sql_query, 'SQL_CALC_FOUND_ROWS' ) !== FALSE ) {
            $found_rows = 'SQL_CALC_FOUND_ROWS';
            $sql_query = str_replace( 'SQL_CALC_FOUND_ROWS ', '', $sql_query );
        }
        $sql_query = sprintf( 'SELECT %s wp_posts.* from ( %s ) wp_posts ORDER BY rand() LIMIT %d', $found_rows, $sql_query, $rand );
    }
    return $sql_query;
}
add_filter( 'posts_request', 'wpse260713_randomize_posts', 10, 2 );

Then you may use the query as follows:

$args = array(
    'post_type' => 'post',
    'posts_per_page' => 10,
    'orderby' => 'date',
    'order' => 'DESC',
    'meta_key' => '_thumbnail_id',
    'no_found_rows' => 'true',
    '_randomize_posts_count' => 3
);
$query = new WP_Query( $args );

Comparative analysis:

  • This method will bring only the maximum number of posts defined by _randomize_posts_count from the database, as opposed to bringing all the results and randomizing on the PHP end. So it's better optimized for data communication with the database. This is better if your database server is separate from your web server.

  • If query cache is not enabled, then this solution will be much faster when the difference between the number of random posts shown vs. total posts selection is big. For example: if you are showing 3 random posts from most recent 200 posts, then this method will be a lot faster.

  • If query cache is enabled, then Birgire's method will be faster as it'll avoid later SQL requests. However, for bigger sample size it may still be slower since you'll have to store a lot of information in the query cache.

  • Best if you consider the sample size carefully and select the solution that suits your use case better.

Note: Random methods are very slow (and often non-scalable) compared to general CODE, so no matter what method you choose, be extra cautious when your sample size of randomisation is considerably large (like thousands).

  • 1
    Nice to see other approaches. ps: there are many interesting workarounds for slow random ordering. I just recalled playing with one here based on an interesting article here by Josh Hartman. Yet another approach is just to store (not too many) ids with the transients API and select the random ones from there ;-) – birgire Mar 21 '17 at 20:28
  • 1
    I will have to spend some time determining which option runs most efficiently. I appreciate the great detail you included! – WebElaine Mar 21 '17 at 21:53
  • Thanks for those links @birgire, will be useful for many people. – Fayaz Mar 22 '17 at 0:15
3

Great work by Fayaz and birgire - much more expert than I might have come up with - but I think there is an easier way, unless I don't understand the question (quite possible!): 1) use get_posts() or, easiest, wp_get_recent_posts(), both of which return arrays by default, accept WP Query $args, and also use no_found_rows=true by default, 2) shuffle the array, 3) then slice off three.

That's how I solved a problem similar to this one for myself, at a point at which I understood close to nothing, as compared to my current state of understanding how close to nothing I understood then. However, birgire and Fayaz's code is cool, so please feel free to go with one or the other!

  • Thanks for adding this alternative approach. All 3 answers meet the goal and I like the fact that yours doesn't require adding a new function to the theme! – WebElaine Mar 21 '17 at 21:53

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.