Skip to content
Advertisement

Deadlock involving SELECT FOR UPDATE

I have transaction with several queries. First, a select rows with FOR UPDATE lock:

Next, there is an update query:

And then there is an insert:

I have two application instances and sometimes I see deadlock errors in PostgreSQL log:

I assume that it may be caused by ON CONFLICT DO UPDATE statement, which may update rows which are not locked by previous SELECT FOR UPDATE

But I can’t understand how can SELECT ... FOR UPDATE query cause deadlock if it is the first query in transaction. There is not queries before it. Can SELECT ... FOR UPDATE statement lock several rows and then wait for other rows in condition to be unlocked?

Advertisement

Answer

SELECT FOR UPDATE is no safeguard against deadlocks. It just locks rows. Locks are acquired along the way, in the order instructed by ORDER BY, or in arbitrary order in the absence of ORDER BY. The best defense against deadlocks is to lock rows in consistent order across the whole transaction – and doing likewise in all concurrent transactions. Or, as the manual puts it:

The best defense against deadlocks is generally to avoid them by being certain that all applications using a database acquire locks on multiple objects in a consistent order.

Else, this can happen (row1, row2, … are rows numbered according to virtual the consistent order):

Adding ORDER BY to your SELECT... FOR UPDATE may already avoid your deadlocks. (It would avoid the one demonstrated above.) Or this happens and you have to do more:

Everything within the transaction must happen in consistent order to be absolutly sure.

Also, your UPDATE does not seem to be in line with the SELECT FOR UPDATE. component_id <> hw_component_id. Typo?
Also, f.archived_at IS NULL does not guarantee that the later SET archived_at = NOW() only affects these rows. You would have to add WHERE f.archived_at IS NULL to the UPDATE be in line. (Seems like a good idea in any case?)

I assume that it may be caused by ON CONFLICT DO UPDATE statement, which may update rows which are not locked by previous SELECT FOR UPDATE.

As long as the UPSERT (ON CONFLICT DO UPDATE) sticks to the consistent order, that wouldn’t be a problem. But that may be hard or impossible to enforce.

Can SELECT ... FOR UPDATE statement lock several rows and then wait for other rows in condition to be unlocked?

Yes, as explained above, locks are acquired along the way. It can have to stop and wait half way through.

NOWAIT

If all that still can’t resolve your deadlocks, the slow and sure method is to use Serializable Isolation Level. Then you have to be prepared for serialization failures and retry the transaction in this case. Considerably more expensive overall.

Or it might be enough to add NOWAIT:

The manual:

With NOWAIT, the statement reports an error, rather than waiting, if a selected row cannot be locked immediately.

You may even skip the ORDER BY clause with NOWAIT if you cannot establish consistent order with the UPSERT anyway.

Then you have to catch that error an retry the transaction. Similar to catching serialization failures, but much cheaper – and less reliable. For example, multiple transactions can still interlock with their UPSERT alone. But it gets less and less likely.

User contributions licensed under: CC BY-SA
8 People found this is helpful
Advertisement