Skip to main content
eLearner.app
Module 11 · Lesson 3 of 443/57 in the course~15 min
Module lessons (3/4)

Locking and SELECT FOR UPDATE

Understanding how a database works in isolation on a single terminal is simple... no wait, I already said that! Sometimes "Atomic Updates" (e.g. stock = stock - 1) aren't enough.

Let's imagine you have to check whether the user has enough balance to buy 50€ worth of stuff:

  1. SELECT balance FROM users WHERE id = 1;
  2. JS: If balance is < 50, block them!
  3. Otherwise proceed with checkout...
  4. Run the deduction! (And here you find a user who, while validating the check at step 2, fired off another parallel one that burned through their account and now you're going below zero!).

FOR UPDATE Explicit Locking

Adding FOR UPDATE at the end of a SELECT magically transforms your innocent read into a sealed Lock against other clients until the next COMMIT. No one else will be able to do UPDATE (or concurrent reads-for-update) on those specific selected rows until you decide what to do with them!

SQL
BEGIN;

-- If id=3 had already been locked by T1, T2 will sit in perpetual loading on this "read"
-- frozen in place, before receiving the raw data, until T1 runs COMMIT freeing everyone!
SELECT balance
FROM users
WHERE id = 3
FOR UPDATE;

-- at this point our Node JS instance lives with the guarantee, knowing for certain that NO ONE ELSE in the world has
-- manipulated (nor been able to read with intent to alter) that balance while we complete the application logic

UPDATE users SET balance = balance - 50 WHERE id = 3;

COMMIT;

There's even FOR SHARE which lets other readers keep reading (if on a base lock) without blocking state reads, but blocking any possible subsequent update until you commit. For modifications though, always use the brute force of the exclusive locking clause:

Try it yourself

Exercise#sql.m11.l3.e1
Attempts: 0Loading…

Open a normal time-scoped transaction and exclusively lock the read of product ID=5.\nRun a two-step query:\n1. Open the transaction (BEGIN)\n2. Query 'products' fetching the full row for ID 5 with the guarantee that no one will alter it, by attaching the 'FOR UPDATE' option.

Loading editor…
Show hint

BEGIN;, newline, SELECT * FROM products WHERE id = 5 FOR UPDATE;

Solution available after 3 attempts

Exercise#sql.m11.l3.e2
Attempts: 0Loading…

Sometimes the transaction hangs for disastrous periods (Deadlock or infinite waits).\nTo prevent this you can command the database to give up the moment the row isn't immediately available: using NOWAIT at the end, which would cause an SQL server exception to bounce back to JS to be try-catched without hanging forever.\nRun a SELECT for category ID=1 and use FOR UPDATE but enforcing NOWAIT.

Loading editor…
Show hint

SELECT * FROM categories WHERE id = 1 FOR UPDATE NOWAIT;

Solution available after 3 attempts