How to avoid wp_query returning the same post I’m on in results?

There are 2 methods. The ultra slow method, and the fast method

The Ultra Slow Method

This method is easy to implement, but can place a massive load on the database, and can cause major performance issues ( as well as severely reduce the number of people who can access your site at the same time ).

This is the post__not_in argument. I do not recommend using this. I mention it only for completeness, and because people unaware of the performance hit will recommend it.

Why is This So Terrible?

Database queries are built around searching for what you want, not what you do not want.

So if you exclude posts at the database level, the database will sometimes copy the entire posts table into memory, remove the rows you wanted to exclude, then perform the query. This is extremely slow and expensive.

In my time working on enterprise WordPress hosting I’ve seen NOT IN type queries cripple sites, either by bringing them down or slowing them to unusable levels. Removing it caused dramatic improvements.

The danger is that when testing these queries out on a smaller test site, they don’t look slow, not great, but okay… Add in multiple users hitting the site at the same time, and bump up the size of the posts and meta tables, and that okay looking query grows to monstrous proportions.

The Fast Method

Ask for 4 posts instead and filter out the posts you don’t want in PHP.

This method is super fast.

For example:

$args = [
    'posts_per_page' => 4,
];

$q = new WP_Query($args);
$count = 0;
while ( $q->have_posts() ) {
    $q->the_post();
    if ( $excluded_id === get_ID() ) {
        continue;
    }
    $count += 1;
    if ( $count > 3 ) {
        break;
    }

    //  ......
}

Notice that if we’ve already shown 3 posts, or if the post we showed was excluded then we break/continue.

A Final Note

The above also applies to any argument that ends in __not_in, they all have terrible performance. Another solution that doesn’t apply to this use case is to use data to solve the problem. For example instead of adding a checkbox to hide a post from the homepage, add a term named show_on_homepage that’s added by default.

Also note that your while loop needs to be wrapped in an if statement checking if have_posts returns true. If you do not then there is a bug, if no posts are found then wp_reset_postdata() is called when there is no post data to reset, interfering with any surrounding post loops.

The code in the question is also missing escaping and as a result is vulnerable to HTML injection attacks.