Set post number to single posts

The answer from the OP inspired me to quickly write this function which is not resource intensive and really fast. I also decided to try another method as described by the OP

To accomplish this, I’ve made use of the Transient API and the post_updated action

THE LOGIC

To get the position of a post, you will first need to get all the posts and determine where in the array the post is and add 1 to its current position/key (Remember, an array starts at 0

This is quite an expensive operation when you are doing this on each page load, and it can become really slow if you have several hundred or thousands of posts

So my idea here was to use a transient to store the resultant post position. I have made the transient to expire in 365 days, you can just set it to your liking.

My decision to make it this long comes from the following idea:

A specific post’s position will only change when:

  • A post before it has been deleted or

  • A deleted post before it has been undeleted

You can however extend this if you wish to include other changes in a post’s status. I would however exclude when a new post is published. This will never have any effect on previous posts

There is a need to delete all the post positions whenever a post is deleted or undeleted and give each post a new post position. So this means you have to delete the current transients and replace it with a new value. The new value will be set when the post is opened for the first time or when the transient does not exist.

My SQL is not great, so I borrowed code from this answer to delete all the transients whenever a post id deleted or undeleted

HERE IS THE CODE

(I have commented the code to explain what happens and why.)

All of this goes into your functions.php

function display_post_number() {

    $query_object   = get_queried_object(); // More safe that $post global
    $id             = $query_object->ID;
    $transient_id   = 'post_number_' . $id; // Give every post a unique transient

    if ( false === ( $post_number = get_transient( $transient_id ) ) ) {
        $post_args = [ 
            'post_type'         => $query_object->post_type, // Set the current post's type as post type
            'fields'            => 'ids', // Only get post ID's
            'posts_per_page'    => -1,
            'order'             => 'ASC', //Get posts from first to last
        ];

        $q = get_posts( $post_args );

        $post_number = array_search( $id, $q ) + 1; //Use array_search to get post position and add 1 

        set_transient( $transient_id, $post_number, 365 * DAY_IN_SECONDS );
    }

    echo $post_number;
}

add_action( 'post_updated', function ( $post, $post_after, $post_before ) {

   if ( 
        $post_after->post_status == 'trash' && $post_before->post_status == 'publish'
        || $post_after->post_status == 'publish' && $post_before->post_status == 'trash'   
    ) { // Only delete the transients if a post is deleted or undeleted
        global $wpdb;
        $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient%_post_number_%')" );
        $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient_timeout%_post_number_%')" );
   }

}, 10, 3 );

You can then use display_post_number() in your single.php to display the post number

EDIT 1

If you need to show something like Post X of Y you can adjust the code as follows.

function display_post_number() {

    $query_object   = get_queried_object(); // More safe that $post global
    $id             = $query_object->ID;
    $transient_id   = 'post_number_' . $id; // Give every post a unique transient

    if ( false === ( $text = get_transient( $transient_id ) ) ) {
        $post_args = [ 
            'post_type'         => $query_object->post_type, // Set the current post's type as post type
            'fields'            => 'ids', // Only get post ID's
            'posts_per_page'    => -1,
            'order'             => 'ASC', //Get posts from first to last
        ];

        $q = get_posts( $post_args );

        $total_posts = count( $q ); // Get total posts
        $post_number = array_search( $id, $q ) + 1; //Use array_search to get post position and add 1 

        $text="You are currently viewing post #" . $post_number . ' of ' . $total_posts;

        set_transient( $transient_id, $text, 365 * DAY_IN_SECONDS );
    }

    echo $text;

}

Then, instead of using the post_updated hook, make use of the transition_post_status to delete the post positions whenever a post’s status change as everytime a post status change, the amount of posts will changes.

add_action( 'transition_post_status', function ( $new_status, $old_status, $post )
{
    if ( 'publish' !== $new_status or 'publish' === $old_status )
        return;

    if ( 'post' !== $post->post_type )
        return; // restrict the filter to a specific post type

        global $wpdb;
        $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient%_post_number_%')" );
        $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient_timeout%_post_number_%')" );

}, 10, 3 );

EDIT 2

From your comments

what would be a good method of finding out which operations are “expensive”? I love myself some lean, fast code, but it’s not always easy to spot what might be slowing things down. Generally speaking, what should one keep and eye on/avoid?

There is really no general rule. To see what I mean, see this post I have just done. In that post I have showed that four custom queries is faster than a single query in that particular situation, but in another situations it can be reversed. So, it all depends on situation and what you want to do. As I always say, 100 well constructed queries can be faster than one lump query

The only way to know is to test, and this is really easy. I have actually created myself a test page on my dev site just for testing. You can use any page/template. Here is my page

<?php
/**
 * Template Name: TEST Page
 */
get_header(); ?>

<div id="main-content" class="main-content">

    <div id="primary" class="content-area">
        <div id="content" class="site-content" role="main">


<?php

timer_start();      
/**-----------------------------------------------------------------------------
 *
 *  DON'T EDIT ABOVE THIS LINE
 *
*------------------------------------------------------------------------------*/   ?>
<?php

// Add all your code to test in this section

?>
<?php /**-------------------------------------------------------------------------
 *
 *  DON'T EDIT BELOW THIS LINE
 *
*------------------------------------------------------------------------------*/   ?>  
<p><?php echo get_num_queries(); ?> queries in <?php timer_stop(1, 5); ?> seconds. </p>

        <!--#page-content-->

        </div><!-- #content -->
    </div><!-- #primary -->
    <?php get_sidebar( 'content' ); ?>
</div><!-- #main-content -->

<?php
get_footer();

The functions that are important here are the following which you can read up on

When you fist set up your page, run it and record the default amount of queries. On my test site I get 11 queries in 0.00000 seconds., so I know I need to subtract 11 from all my test data. (SIDENOTE: Don’t test with cache plugins installed and also never test with transients. The reason is, once a transient is set, you will need to manually remove it from the database on every page refresh which is quite a shlep. Always add it last)

I’ve done couple of test with a post count of 26 posts, and here are the results. Just a few quick notes, I have removed the transient from my code to test, and I have already subtracted 11 from the results that I have pasted here. I have also removed the category parameter.

TEST 1

Your code were I added updateNumbers(); in place of // Add all your code to test in this section

Result – 83 queries in 0.13476 seconds.

TEST 2

The linked code from your answer

Result – 133 queries in 0.16016 seconds.

TEST 3

My code without the transient with display_post_number().

Result – 1 queries in 0.00293 seconds.

TEST 4

My code with the transient. Note the extra query, but the reduction in time, so, as you can see, 2 queries can be faster than one 🙂

Result – 2 queries in 0.00098 seconds.

TO CONCLUDE

Less is not always faster. The only way to really know is to test, and as you can see, the proof is in the numbers. I hope this helps you in fututre