Hook: Optimistic locking is the database version of 'I’ll check before I commit.' Interviewers love it because it tests whether you understand lost updates, not just UPDATE syntax.
Question: What is optimistic locking in SQL?
Answer: Optimistic locking is a way to stop one user from silently overwriting another user’s changes. You read a row, remember its version, and when you write back you include that version in the WHERE clause. If the row was changed by someone else, zero rows are updated and you know there was a conflict.
Interview-Ready Answer: I use optimistic locking when I expect conflicts to be rare. I add a version column, read the current version, and update with a condition like WHERE id = ? AND version = ?; then I increment the version on success. If the update affects zero rows, I know another transaction won, so I re-read and retry or show a conflict message instead of overwriting data.
Optimistic locking is not a special database lock. It is a concurrency check: a rule that says, 'only write if the row still looks like the version I read.' A conflict is detected at write time, not by blocking other users while you think.
version = 7.UPDATE that includes both the primary key and the old version in the WHERE clause.8.The key idea is that the old version acts like a receipt. If the receipt no longer matches the row, someone else already changed it.
In practice, a version column is usually an integer starting at 1. That is safer than a timestamp because clock precision can be coarse, and two writes in the same tick can look identical. SQL itself does not standardize a built-in optimistic-locking clause; most systems implement it with a version field or a timestamp field plus a careful WHERE condition.
| Approach | Behavior | Best for | Tradeoff |
|---|---|---|---|
| Optimistic | Check on write | Rare conflicts | Retry on conflict |
| Pessimistic | Lock first | Hot rows | Waits and deadlocks |
| Serializable | Database enforces order | Strict correctness | More aborts |
Pessimistic locking often uses SELECT ... FOR UPDATE, which blocks other writers until the transaction ends. That is useful when a row is very hot, but it can reduce throughput and increase lock waits. Optimistic locking usually wins when writes are short and conflicts are uncommon.
INTEGER version column is usually 4 bytes per row.O(log n) search.A good retry policy is usually 3 tries with small backoff, such as 50 to 200 ms. If conflicts happen constantly, optimistic locking can thrash: many requests keep failing and retrying. In that case, switch to pessimistic locking, split the hot row into smaller rows, or redesign the write path.
Real-World Example: Imagine a checkout service for a concert ticket store. Two users open the same last ticket at almost the same time. Both screens show quantity = 1, both click buy, and both requests reach the database.
With optimistic locking, the first order update succeeds and increments the version. The second request tries to update the same row with the old version, gets zero affected rows, and the app returns 'sold out' or 'your cart expired.' That is the correct behavior because only one customer can buy the last ticket.
What goes wrong without it: the system uses a plain UPDATE by product id, so both writes succeed and the stock can become -1, or two order confirmations are sent for one item. Symptoms show up as duplicate receipts, inventory mismatches, and support tickets that say 'I paid but the item disappeared.' In logs you might see two successful payment events but only one real unit in inventory, or a later reconciliation job shouting about negative stock.
This is why interviewers care: optimistic locking is not theory. It is the difference between clean conflict handling and a silent data bug that only appears under real user load.
-- Optimistic locking demo in PostgreSQL-compatible SQL
-- Goal: show how a version column prevents a stale write from silently overwriting newer data.
DROP TABLE IF EXISTS accounts;
CREATE TABLE accounts (
id BIGINT PRIMARY KEY,
owner TEXT NOT NULL,
balance NUMERIC(12,2) NOT NULL CHECK (balance >= 0),
version INTEGER NOT NULL DEFAULT 1
);
INSERT INTO accounts (id, owner, balance) VALUES
(1, 'Ava', 100.00),
(2, 'Ben', 50.00);
-- Session A reads row 1 and remembers version = 1.
SELECT id, owner, balance, version
FROM accounts
WHERE id = 1;
-- Session B also reads the same row and still thinks version = 1.
-- In a real app, both users would now edit in memory or a form.
-- Session A writes first. The version check matches, so this update succeeds.
UPDATE accounts
SET balance = balance - 30.00,
version = version + 1
WHERE id = 1
AND version = 1
RETURNING id, owner, balance, version;
-- Session B is stale now. It still tries to write with version = 1.
-- Because the row changed already, this returns zero rows.
-- Your application should treat that as a conflict, then re-read and retry.
UPDATE accounts
SET balance = balance - 50.00,
version = version + 1
WHERE id = 1
AND version = 1
RETURNING id, owner, balance, version;
-- Correct retry path: after re-reading, Session B sees version = 2.
-- Now the update can succeed safely.
UPDATE accounts
SET balance = balance - 50.00,
version = version + 1
WHERE id = 1
AND version = 2
RETURNING id, owner, balance, version;
-- Final state: only one writer at a time effectively won the row update.
SELECT id, owner, balance, version
FROM accounts
ORDER BY id;
-- Anti-pattern to avoid: if you remove the version predicate, a stale write can overwrite newer data.
-- UPDATE accounts SET balance = balance - 50.00, version = version + 1 WHERE id = 1;Follow-up & Tricky Questions:
UPDATE touches zero rows, the version is stale and the app should re-read or show an error.SELECT ... FOR UPDATE? No. FOR UPDATE is pessimistic locking and blocks other writers, while optimistic locking lets everyone proceed and only checks for conflicts when writing.Common Mistakes:
Memory Hook: Read, edit, prove. Read the row, edit it locally, then prove nobody changed it by matching the old version on write.
Cheat Sheet:
version column, usually an integer.WHERE id = ? AND version = ?.Practice Tasks:
version column to an orders table and protect status updates with it.SELECT ... FOR UPDATE in a hot-row scenario and note which one blocks.