bankDBMSdiskreader1BEGIN;2UPDATE acct SET bal = bal + 1003 WHERE id = 2;4UPDATE acct SET bal = bal - 1005 WHERE id = 1;6COMMIT;1BEGIN;2UPDATE acct SET bal = bal + 5003 WHERE id = 2;4UPDATE acct SET bal = bal - 5005 WHERE id = 1;6ROLLBACK;id name bal1 ann2 ben500400300400900T1 viewT2 viewT1 ben 300→400T1 ann 500→400T1 COMMIT500·300400·400+100-100+500-500WAL
Back row: the bank app, the DBMS, its disk and a reader sessionFront: the transaction, the acct table (locks on rows being changed) and the WALPucks: changes to a row · envelopes: requests and replies · WAL block: the log going to diskAt the back: the bank app that runs the transfer, the DBMS, its disk, and a second session that reads. In front: the transaction, the acct table, and the write-ahead log (WAL). Inside the transaction the money moves row by row, each row locked; others still read the last committed balances. COMMIT forces the log to disk before it answers, so a crash cannot lose the transfer. A second transfer of 500 breaks the rule CHECK (bal >= 0); the whole transaction rolls back and nothing of it remains. Real WAL records describe changes to 8 kB table and index pages, not "ben 300→400"; the figure writes them as rows so you can read them. The card shows only T1’s records: the failed transfer also writes WAL (its change to Ben and an abort record), left out to keep the card short.

Inside one transaction

  1. A transaction groups several statements into one unit of work: either all of them take effect, or none do. Our example moves 100 from Ann to Ben. Together they hold 800, and nobody should ever see 700 or 900.

Commit and crash

When it goes wrong

What happens

;; step 1/11 · unit
CREATE TABLE acct (id integer PRIMARY KEY, name text, bal integer CHECK (bal >= 0));
INSERT INTO acct VALUES (1, 'ann', 500);
INSERT INTO acct VALUES (2, 'ben', 300);

  id  name  bal
  1   ann   500
  2   ben   300
  total 800

the transfer is two UPDATEs:
  Ben + 100, then Ann - 100
a crash or error between them must not
leave 900 (or 700) in the table

Real WAL records describe changes to 8 kB table and index pages, not "ben 300→400"; the figure writes them as rows so you can read them. The card shows only T1’s records: the failed transfer also writes WAL (its change to Ben and an abort record), left out to keep the card short. COMMIT waits for the WAL flush because synchronous_commit is on by default. Turning it off makes commits faster but can lose the last few committed transactions in a crash (never corrupting the data).