TIL: pessimistic vs optimistic locking
· til · databases · sql · concurrency
Two ways databases handle "what if two transactions touch the same row at once":
Pessimistic locking assumes a conflict is likely, so it locks the row the moment it's read, and everyone else has to wait until the transaction releases it:
BEGIN;
SELECT balance FROM accounts
WHERE id = 1
FOR UPDATE; -- locks this row; other transactions trying to
-- SELECT ... FOR UPDATE or UPDATE it will block
-- and wait here
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
COMMIT; -- lock releasedDrop the FOR UPDATE and it's just a normal read — no lock, nothing
blocks. FOR UPDATE is exactly the clause that turns "reading" into
"reading with intent to update, so hold a lock until commit."
Optimistic locking assumes conflicts are rare, so it doesn't lock
anything. Other transactions can freely read and write the row. Instead,
you read a version column alongside the data, and when you update, you
fold that version into the WHERE clause:
-- read
SELECT balance, version FROM accounts WHERE id = 1;
-- app gets balance = 500, version = 3
-- update, guarded by the version just read
UPDATE accounts
SET balance = 400, version = version + 1
WHERE id = 1 AND version = 3;Check the affected-row count after: 1 row means nobody else touched it since your read; 0 rows means the version has moved on, so re-fetch and tell the user to retry instead of silently overwriting.
That guard only works if the version is actually part of the WHERE.
Leave it out —
UPDATE accounts SET balance = 400 WHERE id = 1;— and you get a lost update: the write always succeeds, unconditionally clobbering whatever's there, with no signal a conflict ever happened.
Why the plain SELECT never blocks: databases use MVCC
(multi-version concurrency control) — instead of overwriting a row in
place, an UPDATE creates a new row version tagged with the transaction
that made it, and old versions stick around until nothing needs them.
Every transaction reads a consistent snapshot of the data as of when it
started, so reads never wait on writes and vice versa — only two writers
touching the same row actually contend. FOR UPDATE is how you opt out
of "just read a snapshot" and into "lock the current version for me."
See MVCC — why reads don't block writes
for the full breakdown.