If you are the only user, the query should be fine. In particular, there is no race condition or deadlock within the query itself (between the outer query and the subquery). The manual:
However, a transaction never conflicts with itself.
For concurrent use, the matter may be more complicated. You would be on the safe side with SERIALIZABLE
transaction mode:
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
RETURNING *
COMMIT;
You need to prepare for serialization failures and retry your query in such a case.
But I am not entirely sure if this isn't overkill. I'll ask @kgrittn to stop by .. he is the expert with concurrency and serializable transactions ..
And he did. :)
Best of both worlds
Run the query in default transaction mode READ COMMITTED
.
For Postgres 9.5 or later use FOR UPDATE SKIP LOCKED
. See:
For older versions recheck the condition computed IS NULL
explicitly in the outer UPDATE
:
UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
AND computed IS NULL;
As @kgrittn's advised in the comment to his answer, this query could come up empty, without having done anything, in the (unlikely) case it got intertwined with a concurrent transaction.
Therefore, it would work much like the first variant in transaction mode SERIALIZABLE
, you would have to retry - just without the performance penalty.
The only problem: While the conflict is very unlikely because the window of opportunity is just so tiny, it can happen under heavy load. You could not tell for sure whether there are finally no more rows left.
If that does not matter (like in your case), you are done here.
If it does, to be absolutely sure, start one more query with explicit locking after you get an empty result. If this comes up empty, you are done. If not, continue.
In plpgsql it could look like this:
LOOP
UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id FROM stuff WHERE computed IS NULL
LIMIT 1 FOR UPDATE SKIP LOCKED); -- pg 9.5+
-- WHERE id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
-- AND computed IS NULL; -- pg 9.4-
CONTINUE WHEN FOUND; -- continue outside loop, may be a nested loop
UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id FROM stuff WHERE computed IS NULL
LIMIT 1 FOR UPDATE);
EXIT WHEN NOT FOUND; -- exit function (end)
END LOOP;
That should give you the best of both worlds: performance and reliability.