CYBERTEC PostgreSQL Logo

How to do UPDATE ... LIMIT in PostgreSQL

09.2025
Category: 
Tags: 
Dolphin to elephant: "MySQL has got UPDATE ... LIMIT, and PostgreSQL doesn't." - Elephant to dolphin: "But on the other hand, there is no limit to what you can do with an UPDATE in PostgreSQL."
© Laurenz Albe 2025

If you are reading this hoping that PostgreSQL finally got UPDATE ... LIMIT like MySQL, I have to disappoint you. The LIMIT clause is not yet supported for DML statements in PostgreSQL. If you want to UPDATE only a limited number of rows, you have to use workarounds. This article will describe how to do this and how to avoid the pitfalls and race condition you may encounter. Note that most of the following also applies to DELETE ... LIMIT!

An example for how we would like to do UPDATE ... LIMIT

The following table contains 100 categories of 100 entries each. Since we don't discuss persistence, an UNLOGGED table will do:

Now we would like to set the flag on a single row with category 42:

But alas, we cannot do that. We'll have to look for other ways to achieve our goal.

Attempt to solve UPDATE ... LIMIT with a simple subquery

Since there is a LIMIT clause for SELECT, we can use a subquery as follows:

This query works as intended if nobody else is using the table. But it can fail to work as expected in the face of concurrent data modifications:

Now run our proposed update statement in a concurrent session, and it will hang. Now, if you COMMIT the DELETE, our UPDATE statement will delete nothing:

How did this happen? It is true that the entire query sees the same state of the database (the same snapshot). Owing to the benefits of multiversioning, the subquery has no trouble reading the row that is being deleted. However, the UPDATE has to take a FOR NO KEY UPDATE row lock and has to wait. When the deleting transaction commits, the UPDATE finds that the row is gone and has nothing to do. The documentation describes that as follows:

If the first updater commits, the second updater will ignore the row if the first updater deleted it, otherwise it will attempt to apply its operation to the updated version of the row.

Trying to improve our UPDATE ... LIMIT implementation by taking a lock in the subquery

The problem with our first attempt was that we didn't prevent the row from getting deleted after the subquery saw it. To prevent that from happening, we should lock the row in the subquery. There is one additional consideration: we don't care which row exactly we modify, as long as the category is 42. Consequently, we can improve concurrency by using SKIP LOCKED, so that we ignore locked rows and never have to wait:

Looks good, right? Well, here is the result when we execute the statement:

What? It updated all the remaining 99 rows in category 42! Before we file a bug against PostgreSQL for ignoring the LIMIT 1 clause, let's look at the execution plan:

PostgreSQL chose to execute the subquery using a nested loop join, and the subquery (which has the LockRows node) ended up in the inner loop. That is, PostgreSQL scans the 99 rows in category 42 and executes the subquery for each of them. Now, because each update writes a new row version to the end of the table, each execution of the subquery encounters a different row first — and this turns out to be the current row found by the outer index scan.

This was a particularly surprising outcome. Other kinds of bad behavior could occur if concurrent transactions lock and unlock rows between individual executions of the subquery. The root cause of these problems is the same: PostgreSQL executes the subquery more than once. In my example, I caused PostgreSQL to choose such a plan by making the subquery a correlated subquery (it references u.category from the outer query).

Fixing the UPDATE ... LIMIT implementation

To fix the nasty problem we encountered in the previous section, we have to make sure that PostgreSQL executes the subquery only once. in our case, it would be enough to change the first WHERE condition to

But the best way to make sure that the subquery is calculated only once is to put it into a materialized CTE:

This results in the following execution plan:

This version should be free from race conditions and other oddities!

One last improvement: avoid excessive locking

If you have read my diatribe against SELECT ... FOR UPDATE, you will guess what comes next. FOR UPDATE is not the correct lock for our UPDATE! Rather, we should use FOR NO KEY UPDATE. The advantage of using this lower row lock level is that it won't interfere with concurrent INSERT statements on a table with a foreign key referencing our table. So this is the final version of our UPDATE ... LIMIT statement:

Conclusion

We tried to come up with a good workaround for the lack of UPDATE ... LIMIT in PostgreSQL. Along the way, we encountered and solved several potential problems: race conditions, unexpected execution plans, and excessive locks. You can use the same technique if you want to DELETE only a few rows (and then FOR UPDATE would be the correct row lock).

Leave a Reply

Your email address will not be published. Required fields are marked *

CYBERTEC Logo white
Get the newest PostgreSQL Info & Tools


    This site is protected by reCAPTCHA and the Google Privacy Policy & Terms of Service apply.

    ©
    2025
    CYBERTEC PostgreSQL International GmbH
    phone-handsetmagnifiercrosscross-circle
    linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram