Skip to main content
RunBook Academy

PostgreSQLIX · Locks, Blocking and DeadlocksLocks

Row locks versus table locks

Advanced⏱ ~30 minpsql

What you'll learn

  • Choose the correct row lock mode for a read-modify-write pattern
  • Explain why foreign keys need FOR KEY SHARE and what it prevents
  • Predict which row lock pairs conflict from the measured matrix
  • Use SKIP LOCKED and NOWAIT where waiting is the wrong behaviour

Prerequisites

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

Table locks arbitrate between statements. Row locks arbitrate between writers touching the same data, and PostgreSQL has four of them for a reason that becomes obvious once you see the foreign key case.

The four modes

Weakest to strongest.

ModeTaken byIntent
FOR KEY SHAREForeign key checks“Do not change this row’s key”
FOR SHARESELECT … FOR SHARE“Do not change this row at all”
FOR NO KEY UPDATEUPDATE of non-key columns“I am changing this row, but not its key”
FOR UPDATESELECT … FOR UPDATE, DELETE, key UPDATE“I am changing or removing this row”

The matrix, measured

Read-only / Saferow lock conflicts, measured pairwise on 18.6
$ psql -U postgres -c "SET lock_timeout='2s'; BEGIN; SELECT id FROM lockdemo WHERE id=1 FOR <mode>; COMMIT"   # against a held row lock
  FOR KEY SHARE      vs  FOR KEY SHARE      ->  COMPATIBLE
FOR KEY SHARE      vs  FOR UPDATE         ->  BLOCKS
FOR SHARE          vs  FOR NO KEY UPDATE  ->  BLOCKS
FOR NO KEY UPDATE  vs  FOR NO KEY UPDATE  ->  BLOCKS
FOR NO KEY UPDATE  vs  FOR KEY SHARE      ->  COMPATIBLE
FOR UPDATE         vs  FOR KEY SHARE      ->  BLOCKS

The two COMPATIBLE rows are the interesting ones. FOR KEY SHARE and FOR NO KEY UPDATE coexist: one session may be updating a row’s non-key columns while another holds it against key changes. Everything else in this matrix conflicts.

Why that pair exists

A foreign key check is a read that must remain true until the transaction commits. Inserting a child row requires the parent row to exist, and it must go on existing.

The naive implementation locks the parent row against all changes, which means every insert into a busy child table blocks every update of the parent. FOR KEY SHARE is the refinement: it forbids only changes to the key, which is the only thing the check depends on.

Read-only / Safea child insert, held open, and what it locks on the parent
$ psql -U postgres -c "SELECT locktype, mode, granted, relation::regclass FROM pg_locks WHERE relation IN ('parent'::regclass,'child'::regclass) ORDER BY relation::regclass::text, mode"
 locktype |       mode       | granted | relation
----------+------------------+---------+----------
relation | RowExclusiveLock | t       | child
relation | RowShareLock     | t       | parent
(2 rows)

Now the two updates of that parent row, with the child insert still uncommitted.

Read-only / Safeupdating a non-key column of the referenced row
$ psql -U postgres -c "SET lock_timeout='3s'; UPDATE parent SET label='changed' WHERE id=1"
UPDATE 1
Time: 4.931 ms
Service impact possibleupdating the key of the same referenced row
$ psql -U postgres -c "SET lock_timeout='3s'; UPDATE parent SET id=99 WHERE id=2"
ERROR:  canceling statement due to lock timeout
Time: 3001.345 ms (00:03.001)

4.9 ms against a timeout. Same table, same row, same open transaction on the other side — the only difference is whether the key changed.

Locking clauses in practice

-- read-modify-write: lock what you read
BEGIN;
SELECT balance FROM accounts WHERE id = 42 FOR UPDATE;
UPDATE accounts SET balance = balance - 30 WHERE id = 42;
COMMIT;

Note, from lesson VII-04, that this particular example is better written without the lock, as UPDATE accounts SET balance = balance - 30. Use FOR UPDATE when the application genuinely must decide something between the read and the write.

FOR NO KEY UPDATE is the weaker version, when you will change the row but not its key. It permits concurrent foreign key checks, which matters on a parent row that children are constantly being added to.

FOR SHARE prevents anyone from changing the row while you hold it, without claiming it for yourself. Multiple sessions can hold it at once.

FOR KEY SHARE is what you want when the invariant is “this row must still exist with this identity”, which is what a foreign key check needs and occasionally what application logic needs too.

What to take from this

  • Four row lock modes exist so a foreign key check does not block ordinary updates of the referenced row.
  • Measured: non-key update of a referenced row, 4.9 ms. Key update of the same row, timeout.
  • “Foreign keys cause locking problems” is pre-9.3 advice. The real defect is usually a missing index on the child’s referencing column.
  • SKIP LOCKED is what makes a PostgreSQL work queue scale. It trades consistency for concurrency, deliberately.
  • Concurrent row locks allocate multixacts, which have their own wraparound and their own disk.

Cross-course references

  • Observability for Production Sysadmins — Part LIX (Database observability) covers exporting lock waits, and Part XX (Alert quality) covers why an alert on lock count is noise while an alert on wait duration is not.
  • Linux for Production Sysadmins — Part XXXVII (Resource management) covers the shared-memory limits that a relation-lock explosion eventually reaches.

Quiz

Knowledge check · 6 questions

  1. Q1. With an uncommitted INSERT into a child table open, updating the referenced parent row's label column succeeded in under 5 ms while updating its id timed out. What explains the difference?

  2. Q2. A work queue where each of twelve workers runs SELECT ... ORDER BY created_at LIMIT 1 FOR UPDATE achieves no more throughput than one worker. What is missing?

  3. Q3. Deleting rows from a parent table has become extremely slow, and each delete appears to scan a large child table. What is the fix?

  4. Q4. Which statements about multixacts are correct? Select all that apply.

  5. Q5. FOR KEY SHARE and FOR NO KEY UPDATE can be held on the same row at the same time by different transactions.

  6. Q6. A team wants to drop foreign key constraints from a busy schema to reduce lock contention. What would you tell them?

Passing score: 75%. Answers are checked in this browser.