Table of Contents
The PostgreSQL documentation has some information about row locks. But typically, you cannot see them in pg_locks
, and not everybody knows how they work and how to track and debug row locks. This article intends to give some insight into how PostgreSQL row locks work “under the hood”.
In the old days, there were only FOR UPDATE
and FOR SHARE
. PostgreSQL used the latter for foreign key constraints: if you inserted a row in a table with a foreign key, PostgreSQL locked the referenced row with FOR SHARE
to prevent concurrent deletes. Unfortunately, that also prevented concurrent updates, which was bad for concurrency.
PostgreSQL 9.4 introduced FOR KEY SHARE
and FOR NO KEY UPDATE
. Now the former lock is taken on the row referenced by a newly inserted row, and the latter is taken for updates that do not modify a primary or unique key column. Since FOR KEY SHARE
and FOR NO KEY UPDATE
are compatible, you can now update a row even if a concurrent transaction inserts a row referencing the row you are updating.
SELECT ... FOR ...
?Most people use SELECT ... FOR UPDATE
to explicitly lock a row against concurrent updates. But most of the time, that is excessive. You should only take a FOR UPDATE
lock if you intend to delete the row or modify a primary or unique key column. Otherwise, it is better to take FOR NO KEY UPDATE
locks, so that you do not block inserts into tables referencing the table you are updating. I have seen cases where this simple change was enough to dissolve massive locking problems into thin air.
I have never seen a need to explicitly take a share lock on a row. PostgreSQL multiversioning allows you to see the unmodified version of a row even if it is concurrently updated or deleted.
PostgreSQL creates the shared memory lock table when the server starts, and it has a fixed size. Therefore, PostgreSQL does not keep row locks in the lock table. While the number of tables (and table locks) in a database cluster is (normally) moderate, the number of rows can be very large. That would engender the danger of overflow if we kept row locks in the lock table. There is the concept of lock escalation to promote many row locks to a page lock or a table lock, and some other databases do that. But apart from the extra work, lock escalation can lead to surprising and unpredictable deadlocks.
Instead, PostgreSQL stores row locks on the row itself. For that end, it repurposes the xmax
system column that is otherwise unemployed when a transaction is in progress (read my article for details).
While that allows PostgreSQL to take as many row locks as it needs, it also comes at a price: if you modify a table row, the buffer that contains the row becomes dirty (if it isn't already) and PostgreSQL has to write it out to disk during the next checkpoint. So taking row locks causes extra write operations in PostgreSQL.
First, we check the hint bits and the xmax
of the row to see if somebody holds a lock on the row. That will become important later on.
tuple
lock on the rowThen we take an exclusive “tuple
” lock on the row in question. This is a regular “heavyweight” lock in the shared memory lock table and serves two purposes:
If the initial check showed that other transactions are holding a lock on the row, we hang on to the tuple
lock and go to sleep waiting for the transaction ID of one of the locking transactions. As soon as all blocking transactions have terminated, we can proceed. But first, we check if there were concurrent modifications of the row while we were waiting. If yes, the behavior depends on the current transaction isolation level:
READ COMMITTED
, we fetch the latest committed version of the row (and check if is still satisfies the WHERE
condition). This is good for concurrency, but can lead to interesting anomalies.As soon as all blocking row locks are gone and we have the most recent version of the row, we can proceed. We modify xmax
and some hint bits on the row and release the tuple
lock.
The beauty of that algorithm is that no session ever needs more than two locks in the lock table, so there is no danger of running out of memory.
Since the above was a bit complicated, let's look at an example where three database transactions try to lock the same row at the same time.
tuple
lock on the row. Then it writes the row lock to the row and releases the tuple
lock immediately.tuple
lock on the row and goes to sleep waiting for the first transaction to finishtuple
lock on the row.If you see a tuple
lock in pg_locks
, that means that somebody is waiting for a row lock, no matter if that tuple
lock is granted
or not.
pgrowlocks
extensionpg_locks
is the catalog view that allows you to examine the lock table. Unless there are conflicts on row locks, you will never see a row lock in pg_locks
. So we cannot use pg_locks
to see who holds which row lock. But we can use the extension pgrowlocks:
1 2 3 4 5 6 7 8 9 |
CREATE EXTENSION IF NOT EXISTS pgrowlocks; SELECT * FROM pgrowlocks('table_name'); locked_row │ locker │ multi │ xids │ modes │ pids ════════════╪════════╪═══════╪═════════════════╪═══════════════════════════╪═══════════════ (0,2) │ 2 │ t │ {571228,571229} │ {'Key Share','Key Share'} │ {13644,13911} (0,3) │ 571230 │ f │ {571230} │ {'For No Key Update'} │ {13913} (2 rows) |
“locker
” is the value stored in xmax
. We see that two rows in the first table block are locked:
FOR KEY SHARE
lock on the row with ctid
(0,2)
. Since more than one transaction holds a lock on the row, PostgreSQL created the multixact with ID 2 as a placeholder.FOR NO KEY UPDATE
lock on the row with ctid
(0,3)
.Note that pgrowlocks()
performs a sequential scan on the table, so this can take a while and will put load on your disk.
Row locks are different, because PostgreSQL does not permanently store them in the shared memory lock table. Because of that, we normally cannot see them in pg_locks
. However, there is the pgrowlocks extension that can help with debugging lock problems. Next time you need to explicitly lock a row, use SELECT ... FOR NO KEY UPDATE
!
To learn more about SQL, find out about using the MERGE
command in PostgreSQL v15 and higher.
In order to receive regular updates on important changes in PostgreSQL, subscribe to our newsletter, or follow us on Twitter, Facebook, or LinkedIn.
You need to load content from reCAPTCHA to submit the form. Please note that doing so will share data with third-party providers.
More InformationYou are currently viewing a placeholder content from Facebook. To access the actual content, click the button below. Please note that doing so will share data with third-party providers.
More InformationYou are currently viewing a placeholder content from X. To access the actual content, click the button below. Please note that doing so will share data with third-party providers.
More Information
key column, you mean primary key? can this also apply to unique constraint column?
A primary or unique key column. Anything that could be referenced by a foreign key. I thought that was clear from the preceding sentences, but I have edited the text to make it clearer.
if the lock is stored in the row itself what the shared memory lock table is for?