[{"data":1,"prerenderedAt":3349},["ShallowReactive",2],{"page-\u002Fsql\u002F15-transactions-and-isolation":3},{"id":4,"title":5,"body":6,"description":16,"extension":3343,"meta":3344,"navigation":227,"path":3345,"seo":3346,"stem":3347,"__hash__":3348},"content\u002Fsql\u002F15-transactions-and-isolation.md","15 — Transactions & Isolation",{"type":7,"value":8,"toc":3315},"minimark",[9,13,17,22,98,102,301,306,456,460,463,533,663,667,671,674,684,687,691,694,702,705,709,712,720,723,727,735,743,746,750,757,786,790,812,816,837,840,862,866,901,911,915,1681,1685,1696,1704,1722,1849,1853,1856,2122,2128,2132,2135,2237,2241,2441,2445,2578,2582,2669,2673,2832,2836,2839,3086,3311],[10,11,5],"h1",{"id":12},"_15-transactions-isolation",[14,15,16],"p",{},"Transactions group operations into atomic, consistent, isolated, durable units. Without them, partial failures corrupt data. This chapter is a code-first reference for ACID, isolation levels, MVCC, anomalies, explicit locking, deadlocks, and the retry patterns that make SERIALIZABLE safe.",[18,19,21],"h2",{"id":20},"acid","ACID",[23,24,25,41],"table",{},[26,27,28],"thead",{},[29,30,31,35,38],"tr",{},[32,33,34],"th",{},"Property",[32,36,37],{},"Meaning",[32,39,40],{},"Enforced By",[42,43,44,59,72,85],"tbody",{},[29,45,46,53,56],{},[47,48,49],"td",{},[50,51,52],"strong",{},"Atomicity",[47,54,55],{},"All operations succeed or none do — no partial commits.",[47,57,58],{},"DB engine (WAL + rollback).",[29,60,61,66,69],{},[47,62,63],{},[50,64,65],{},"Consistency",[47,67,68],{},"Transaction takes the DB from one valid state to another (constraints hold).",[47,70,71],{},"Constraints (Chapter 12).",[29,73,74,79,82],{},[47,75,76],{},[50,77,78],{},"Isolation",[47,80,81],{},"Concurrent transactions appear to run serially — effects don't interleave.",[47,83,84],{},"Isolation level + MVCC.",[29,86,87,92,95],{},[47,88,89],{},[50,90,91],{},"Durability",[47,93,94],{},"Once committed, the change survives crashes.",[47,96,97],{},"WAL (write-ahead log) flushed before commit returns.",[18,99,101],{"id":100},"transaction-control","Transaction Control",[103,104,106],"code-wrapper",{"language":105},"sql",[107,108,112],"pre",{"className":109,"code":110,"language":105,"meta":111,"style":111},"language-sql shiki shiki-themes github-light github-dark","-- Basic transaction: BEGIN ... COMMIT or ROLLBACK.\nBEGIN;                  -- start a transaction (or: BEGIN TRANSACTION)\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\nCOMMIT;                 -- apply both updates atomically\n\n-- Rollback: undo all changes since BEGIN.\nBEGIN;\nDELETE FROM orders WHERE id = 999;\nROLLBACK;               -- the delete is undone\n\n-- PostgreSQL: every statement is implicitly in a transaction.\n-- A standalone statement gets an implicit BEGIN + COMMIT.\n-- MySQL: autocommit is on by default (each statement is its own transaction).\n","",[113,114,115,124,138,179,210,222,229,235,242,266,278,283,289,295],"code",{"__ignoreMap":111},[116,117,120],"span",{"class":118,"line":119},"line",1,[116,121,123],{"class":122},"sdCPZ","-- Basic transaction: BEGIN ... COMMIT or ROLLBACK.\n",[116,125,127,131,135],{"class":118,"line":126},2,[116,128,130],{"class":129},"svdQ7","BEGIN",[116,132,134],{"class":133},"ssxIu",";                  ",[116,136,137],{"class":122},"-- start a transaction (or: BEGIN TRANSACTION)\n",[116,139,141,144,147,150,153,156,158,161,165,168,171,173,176],{"class":118,"line":140},3,[116,142,143],{"class":129},"UPDATE",[116,145,146],{"class":133}," accounts ",[116,148,149],{"class":129},"SET",[116,151,152],{"class":133}," balance ",[116,154,155],{"class":129},"=",[116,157,152],{"class":133},[116,159,160],{"class":129},"-",[116,162,164],{"class":163},"snvgF"," 100",[116,166,167],{"class":129}," WHERE",[116,169,170],{"class":133}," id ",[116,172,155],{"class":129},[116,174,175],{"class":163}," 1",[116,177,178],{"class":133},";\n",[116,180,182,184,186,188,190,192,194,197,199,201,203,205,208],{"class":118,"line":181},4,[116,183,143],{"class":129},[116,185,146],{"class":133},[116,187,149],{"class":129},[116,189,152],{"class":133},[116,191,155],{"class":129},[116,193,152],{"class":133},[116,195,196],{"class":129},"+",[116,198,164],{"class":163},[116,200,167],{"class":129},[116,202,170],{"class":133},[116,204,155],{"class":129},[116,206,207],{"class":163}," 2",[116,209,178],{"class":133},[116,211,213,216,219],{"class":118,"line":212},5,[116,214,215],{"class":129},"COMMIT",[116,217,218],{"class":133},";                 ",[116,220,221],{"class":122},"-- apply both updates atomically\n",[116,223,225],{"class":118,"line":224},6,[116,226,228],{"emptyLinePlaceholder":227},true,"\n",[116,230,232],{"class":118,"line":231},7,[116,233,234],{"class":122},"-- Rollback: undo all changes since BEGIN.\n",[116,236,238,240],{"class":118,"line":237},8,[116,239,130],{"class":129},[116,241,178],{"class":133},[116,243,245,248,251,254,257,259,261,264],{"class":118,"line":244},9,[116,246,247],{"class":129},"DELETE",[116,249,250],{"class":129}," FROM",[116,252,253],{"class":133}," orders ",[116,255,256],{"class":129},"WHERE",[116,258,170],{"class":133},[116,260,155],{"class":129},[116,262,263],{"class":163}," 999",[116,265,178],{"class":133},[116,267,269,272,275],{"class":118,"line":268},10,[116,270,271],{"class":129},"ROLLBACK",[116,273,274],{"class":133},";               ",[116,276,277],{"class":122},"-- the delete is undone\n",[116,279,281],{"class":118,"line":280},11,[116,282,228],{"emptyLinePlaceholder":227},[116,284,286],{"class":118,"line":285},12,[116,287,288],{"class":122},"-- PostgreSQL: every statement is implicitly in a transaction.\n",[116,290,292],{"class":118,"line":291},13,[116,293,294],{"class":122},"-- A standalone statement gets an implicit BEGIN + COMMIT.\n",[116,296,298],{"class":118,"line":297},14,[116,299,300],{"class":122},"-- MySQL: autocommit is on by default (each statement is its own transaction).\n",[302,303,305],"h3",{"id":304},"savepoints-partial-rollbacks","SAVEPOINTs — Partial Rollbacks",[103,307,308],{"language":105},[107,309,311],{"className":109,"code":310,"language":105,"meta":111,"style":111},"BEGIN;\nINSERT INTO orders (...) VALUES (...);\nSAVEPOINT after_insert;\nUPDATE inventory SET qty = qty - 1 WHERE product_id = 5;\n-- Something went wrong with the inventory update.\nROLLBACK TO SAVEPOINT after_insert;   -- undo only the UPDATE, keep the INSERT\n-- Continue with other work...\nUPDATE inventory SET qty = qty - 1 WHERE product_id = 6;\nCOMMIT;   -- commits the INSERT + the second UPDATE\n\n-- SAVEPOINTs can be nested:\n-- SAVEPOINT outer; ... SAVEPOINT inner; ... ROLLBACK TO inner; ... RELEASE SAVEPOINT outer;\n-- RELEASE SAVEPOINT discards the savepoint (you can no longer roll back to it).\n-- There are no true nested transactions in PG — savepoints are the mechanism.\n",[113,312,313,319,333,338,370,375,388,393,422,432,436,441,446,451],{"__ignoreMap":111},[116,314,315,317],{"class":118,"line":119},[116,316,130],{"class":129},[116,318,178],{"class":133},[116,320,321,324,327,330],{"class":118,"line":126},[116,322,323],{"class":129},"INSERT INTO",[116,325,326],{"class":133}," orders (...) ",[116,328,329],{"class":129},"VALUES",[116,331,332],{"class":133}," (...);\n",[116,334,335],{"class":118,"line":140},[116,336,337],{"class":133},"SAVEPOINT after_insert;\n",[116,339,340,342,345,347,350,352,354,356,358,360,363,365,368],{"class":118,"line":181},[116,341,143],{"class":129},[116,343,344],{"class":133}," inventory ",[116,346,149],{"class":129},[116,348,349],{"class":133}," qty ",[116,351,155],{"class":129},[116,353,349],{"class":133},[116,355,160],{"class":129},[116,357,175],{"class":163},[116,359,167],{"class":129},[116,361,362],{"class":133}," product_id ",[116,364,155],{"class":129},[116,366,367],{"class":163}," 5",[116,369,178],{"class":133},[116,371,372],{"class":118,"line":212},[116,373,374],{"class":122},"-- Something went wrong with the inventory update.\n",[116,376,377,379,382,385],{"class":118,"line":224},[116,378,271],{"class":129},[116,380,381],{"class":129}," TO",[116,383,384],{"class":133}," SAVEPOINT after_insert;   ",[116,386,387],{"class":122},"-- undo only the UPDATE, keep the INSERT\n",[116,389,390],{"class":118,"line":231},[116,391,392],{"class":122},"-- Continue with other work...\n",[116,394,395,397,399,401,403,405,407,409,411,413,415,417,420],{"class":118,"line":237},[116,396,143],{"class":129},[116,398,344],{"class":133},[116,400,149],{"class":129},[116,402,349],{"class":133},[116,404,155],{"class":129},[116,406,349],{"class":133},[116,408,160],{"class":129},[116,410,175],{"class":163},[116,412,167],{"class":129},[116,414,362],{"class":133},[116,416,155],{"class":129},[116,418,419],{"class":163}," 6",[116,421,178],{"class":133},[116,423,424,426,429],{"class":118,"line":244},[116,425,215],{"class":129},[116,427,428],{"class":133},";   ",[116,430,431],{"class":122},"-- commits the INSERT + the second UPDATE\n",[116,433,434],{"class":118,"line":268},[116,435,228],{"emptyLinePlaceholder":227},[116,437,438],{"class":118,"line":280},[116,439,440],{"class":122},"-- SAVEPOINTs can be nested:\n",[116,442,443],{"class":118,"line":285},[116,444,445],{"class":122},"-- SAVEPOINT outer; ... SAVEPOINT inner; ... ROLLBACK TO inner; ... RELEASE SAVEPOINT outer;\n",[116,447,448],{"class":118,"line":291},[116,449,450],{"class":122},"-- RELEASE SAVEPOINT discards the savepoint (you can no longer roll back to it).\n",[116,452,453],{"class":118,"line":297},[116,454,455],{"class":122},"-- There are no true nested transactions in PG — savepoints are the mechanism.\n",[18,457,459],{"id":458},"isolation-levels","Isolation Levels",[14,461,462],{},"Isolation levels trade off consistency for performance. Higher isolation prevents more anomalies but reduces concurrency.",[23,464,465,478],{},[26,466,467],{},[29,468,469,472,475],{},[32,470,471],{},"Level",[32,473,474],{},"Prevents",[32,476,477],{},"PG Implementation",[42,479,480,493,507,520],{},[29,481,482,487,490],{},[47,483,484],{},[50,485,486],{},"READ UNCOMMITTED",[47,488,489],{},"(Nothing — allows dirty reads. PG maps to READ COMMITTED.)",[47,491,492],{},"N\u002FA (aliased)",[29,494,495,501,504],{},[47,496,497,500],{},[50,498,499],{},"READ COMMITTED"," (PG default)",[47,502,503],{},"Dirty reads. Each statement sees a fresh snapshot.",[47,505,506],{},"Per-statement snapshot.",[29,508,509,514,517],{},[47,510,511],{},[50,512,513],{},"REPEATABLE READ",[47,515,516],{},"Dirty reads, non-repeatable reads, phantom reads (PG\u002FSI).",[47,518,519],{},"Per-transaction snapshot.",[29,521,522,527,530],{},[47,523,524],{},[50,525,526],{},"SERIALIZABLE",[47,528,529],{},"All anomalies — full serializability.",[47,531,532],{},"SSI (Serializable Snapshot Isolation).",[103,534,535],{"language":105},[107,536,538],{"className":109,"code":537,"language":105,"meta":111,"style":111},"-- Set isolation for a single transaction.\nBEGIN;\nSET TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n-- ... work ...\nCOMMIT;\n\n-- Shorthand:\nBEGIN ISOLATION LEVEL SERIALIZABLE;\n-- ... work ...\nCOMMIT;\n\n-- Set default isolation for the session.\nSET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;\n\n-- ⚠️ SET TRANSACTION ISOLATION LEVEL must come BEFORE the first query.\n-- The snapshot is taken at the first statement — setting it after is too late.\n",[113,539,540,545,551,572,577,583,587,592,605,609,615,619,624,647,651,657],{"__ignoreMap":111},[116,541,542],{"class":118,"line":119},[116,543,544],{"class":122},"-- Set isolation for a single transaction.\n",[116,546,547,549],{"class":118,"line":126},[116,548,130],{"class":129},[116,550,178],{"class":133},[116,552,553,555,558,561,564,567,570],{"class":118,"line":140},[116,554,149],{"class":129},[116,556,557],{"class":129}," TRANSACTION",[116,559,560],{"class":129}," ISOLATION",[116,562,563],{"class":129}," LEVEL",[116,565,566],{"class":129}," REPEATABLE",[116,568,569],{"class":129}," READ",[116,571,178],{"class":133},[116,573,574],{"class":118,"line":181},[116,575,576],{"class":122},"-- ... work ...\n",[116,578,579,581],{"class":118,"line":212},[116,580,215],{"class":129},[116,582,178],{"class":133},[116,584,585],{"class":118,"line":224},[116,586,228],{"emptyLinePlaceholder":227},[116,588,589],{"class":118,"line":231},[116,590,591],{"class":122},"-- Shorthand:\n",[116,593,594,596,598,600,603],{"class":118,"line":237},[116,595,130],{"class":129},[116,597,560],{"class":129},[116,599,563],{"class":129},[116,601,602],{"class":129}," SERIALIZABLE",[116,604,178],{"class":133},[116,606,607],{"class":118,"line":244},[116,608,576],{"class":122},[116,610,611,613],{"class":118,"line":268},[116,612,215],{"class":129},[116,614,178],{"class":133},[116,616,617],{"class":118,"line":280},[116,618,228],{"emptyLinePlaceholder":227},[116,620,621],{"class":118,"line":285},[116,622,623],{"class":122},"-- Set default isolation for the session.\n",[116,625,626,628,631,634,637,639,641,643,645],{"class":118,"line":291},[116,627,149],{"class":129},[116,629,630],{"class":129}," SESSION",[116,632,633],{"class":133}," CHARACTERISTICS ",[116,635,636],{"class":129},"AS",[116,638,557],{"class":129},[116,640,560],{"class":129},[116,642,563],{"class":129},[116,644,602],{"class":129},[116,646,178],{"class":133},[116,648,649],{"class":118,"line":297},[116,650,228],{"emptyLinePlaceholder":227},[116,652,654],{"class":118,"line":653},15,[116,655,656],{"class":122},"-- ⚠️ SET TRANSACTION ISOLATION LEVEL must come BEFORE the first query.\n",[116,658,660],{"class":118,"line":659},16,[116,661,662],{"class":122},"-- The snapshot is taken at the first statement — setting it after is too late.\n",[18,664,666],{"id":665},"the-anomalies","The Anomalies",[302,668,670],{"id":669},"dirty-read-prevented-at-read-committed","Dirty Read (prevented at READ COMMITTED+)",[14,672,673],{},"T1 reads uncommitted data from T2. If T2 rolls back, T1 saw data that never \"happened.\"",[103,675,677],{"language":676},"text",[107,678,682],{"className":679,"code":681,"language":676,"meta":111},[680],"language-text","T1: BEGIN; UPDATE accounts SET balance = balance + 100 WHERE id = 1;   -- not committed\nT2: SELECT balance FROM accounts WHERE id = 1;   -- sees the +100 (dirty read)\nT1: ROLLBACK;   -- T2 saw a value that never committed\n",[113,683,681],{"__ignoreMap":111},[14,685,686],{},"PostgreSQL doesn't allow dirty reads at any level (READ UNCOMMITTED is aliased to READ COMMITTED). MVCC ensures readers always see committed snapshots.",[302,688,690],{"id":689},"non-repeatable-read-prevented-at-repeatable-read","Non-Repeatable Read (prevented at REPEATABLE READ+)",[14,692,693],{},"T1 reads a row twice and gets different values because T2 modified it in between.",[103,695,696],{"language":676},[107,697,700],{"className":698,"code":699,"language":676,"meta":111},[680],"T1: BEGIN; SELECT balance FROM accounts WHERE id = 1;   -- 100\nT2: UPDATE accounts SET balance = 200 WHERE id = 1; COMMIT;\nT1: SELECT balance FROM accounts WHERE id = 1;   -- 200 (different!) — READ COMMITTED\nT1: COMMIT;\n\n-- At REPEATABLE READ: T1's second SELECT still returns 100 (same snapshot).\n",[113,701,699],{"__ignoreMap":111},[14,703,704],{},"READ COMMITTED allows this (each statement gets a fresh snapshot). REPEATABLE READ prevents it (one snapshot per transaction).",[302,706,708],{"id":707},"phantom-read-prevented-at-repeatable-read-in-pg-serializable-in-standard","Phantom Read (prevented at REPEATABLE READ in PG; SERIALIZABLE in standard)",[14,710,711],{},"T1 runs a query twice and gets different rows because T2 inserted\u002Fdeleted matching rows.",[103,713,714],{"language":676},[107,715,718],{"className":716,"code":717,"language":676,"meta":111},[680],"T1: BEGIN; SELECT count(*) FROM orders WHERE amount > 100;   -- 5\nT2: INSERT INTO orders (amount, ...) VALUES (150, ...); COMMIT;\nT1: SELECT count(*) FROM orders WHERE amount > 100;   -- 6 (phantom!) — standard RR\nT1: COMMIT;\n\n-- In PostgreSQL, REPEATABLE READ (snapshot isolation) prevents phantoms.\n-- In the SQL standard, REPEATABLE READ allows phantoms (you need SERIALIZABLE).\n",[113,719,717],{"__ignoreMap":111},[14,721,722],{},"This is a PostgreSQL strength — its REPEATABLE READ is stronger than the standard's.",[302,724,726],{"id":725},"write-skew-prevented-only-at-serializable","Write Skew (prevented only at SERIALIZABLE)",[14,728,729,730,734],{},"Two transactions read overlapping data, make decisions based on what they read, and write to ",[731,732,733],"em",{},"disjoint"," rows — the combined result is inconsistent, but neither transaction saw the other's write.",[103,736,737],{"language":676},[107,738,741],{"className":739,"code":740,"language":676,"meta":111},[680],"-- Constraint: at least one doctor must be on call.\nT1: SELECT count(*) FROM doctors WHERE on_call = true;   -- 2\nT2: SELECT count(*) FROM doctors WHERE on_call = true;   -- 2\nT1: UPDATE doctors SET on_call = false WHERE id = 1; COMMIT;  -- thinks 1 remains\nT2: UPDATE doctors SET on_call = false WHERE id = 2; COMMIT;  -- thinks 1 remains\n-- Now 0 doctors on call — invariant violated, but each transaction saw a valid state.\n",[113,742,740],{"__ignoreMap":111},[14,744,745],{},"Snapshot isolation (REPEATABLE READ) doesn't prevent write skew — only SERIALIZABLE does, via SSI (Serializable Snapshot Isolation) which tracks read\u002Fwrite dependencies and aborts one transaction.",[18,747,749],{"id":748},"postgresqls-mvcc-implementation","PostgreSQL's MVCC Implementation",[14,751,752,753,756],{},"PostgreSQL uses ",[50,754,755],{},"MVCC"," (Multi-Version Concurrency Control):",[758,759,760,768,771,779],"ul",{},[761,762,763,764,767],"li",{},"Each statement\u002Ftransaction sees a ",[50,765,766],{},"snapshot"," — a consistent view of the DB as of a point in time.",[761,769,770],{},"Writers don't block readers; readers don't block writers.",[761,772,773,775,776,778],{},[113,774,143],{},"\u002F",[113,777,247],{}," create new row versions (old versions remain for in-flight transactions).",[761,780,781,782,785],{},"Old versions are cleaned by ",[113,783,784],{},"VACUUM"," (dead tuples).",[302,787,789],{"id":788},"read-committed-default","READ COMMITTED (default)",[758,791,792,795,798,809],{},[761,793,794],{},"Each statement gets a fresh snapshot (as of the statement's start).",[761,796,797],{},"Within a transaction, repeated reads can see different data (non-repeatable reads allowed).",[761,799,800,775,802,804,805,808],{},[113,801,143],{},[113,803,247],{}," see the latest committed version of rows; ",[113,806,807],{},"SELECT"," sees committed data as of statement start.",[761,810,811],{},"Good default for most workloads — high concurrency, no dirty reads.",[302,813,815],{"id":814},"repeatable-read-snapshot-isolation-in-pg","REPEATABLE READ (Snapshot Isolation in PG)",[758,817,818,821,824,834],{},[761,819,820],{},"One snapshot per transaction (taken at the first statement).",[761,822,823],{},"Repeated reads return the same data — no non-repeatable reads, no phantoms.",[761,825,826,829,830,833],{},[50,827,828],{},"Write conflicts",": if T1 updates row R, and T2 (REPEATABLE READ) tries to update R after T1 commits, T2 gets ",[113,831,832],{},"ERROR: could not serialize access due to concurrent update"," and must retry. This is \"first-updater-wins.\"",[761,835,836],{},"Prevents phantoms (stronger than the standard's REPEATABLE READ).",[302,838,526],{"id":839},"serializable",[758,841,842,849,856,859],{},[761,843,844,845,848],{},"Serializable Snapshot Isolation (SSI) — detects serialization anomalies (like write skew) and aborts one transaction with ",[113,846,847],{},"ERROR: could not serialize access due to read\u002Fwrite dependencies among transactions",".",[761,850,851,852,855],{},"SQLSTATE: ",[113,853,854],{},"40001"," (serialization_failure).",[761,857,858],{},"Requires retry logic: on a serialization failure, rerun the entire transaction.",[761,860,861],{},"Lower throughput than lower levels (runtime checks have overhead), but correct for critical invariants.",[18,863,865],{"id":864},"choosing-an-isolation-level","Choosing an Isolation Level",[23,867,868,877],{},[26,869,870],{},[29,871,872,875],{},[32,873,874],{},"Workload",[32,876,471],{},[42,878,879,886,893],{},[29,880,881,884],{},[47,882,883],{},"Most web apps, read-heavy, tolerant of slightly stale reads",[47,885,789],{},[29,887,888,891],{},[47,889,890],{},"Reports that must see a consistent snapshot",[47,892,513],{},[29,894,895,898],{},[47,896,897],{},"Financial invariants (\"at least one doctor on call,\" \"no double-spend\")",[47,899,900],{},"SERIALIZABLE (with retry)",[14,902,903,904,907,908,910],{},"For SERIALIZABLE, you ",[50,905,906],{},"must"," implement retry on ",[113,909,854],{}," (serialization_failure). Without retry, SERIALIZABLE just adds aborts with no correctness benefit.",[18,912,914],{"id":913},"complex-implementation-bank-transfer-with-retry-logic","Complex Implementation: Bank Transfer with Retry Logic",[103,916,917],{"language":105},[107,918,920],{"className":109,"code":919,"language":105,"meta":111,"style":111},"-- A bank transfer at SERIALIZABLE isolation with retry on serialization failure.\n-- This PL\u002FpgSQL function encapsulates the retry loop.\n-- In application code (Python, Go, etc.), use the same pattern with your driver's\n-- transaction API and catch SQLSTATE 40001.\n\nCREATE OR REPLACE FUNCTION transfer_money(\n  p_from   BIGINT,\n  p_to     BIGINT,\n  p_amount NUMERIC\n) RETURNS VOID\nLANGUAGE plpgsql\nAS $$\nDECLARE\n  max_retries INTEGER := 5;\n  attempt     INTEGER := 0;\n  balance     NUMERIC;\nBEGIN\n  \u003C\u003Cretry>>\n  LOOP\n    attempt := attempt + 1;\n    IF attempt > max_retries THEN\n      RAISE EXCEPTION 'transfer failed after % attempts', max_retries;\n    END IF;\n\n    BEGIN\n      -- SERIALIZABLE: detect write skew and other anomalies.\n      BEGIN ISOLATION LEVEL SERIALIZABLE;\n\n      -- Lock both rows in deterministic order (ascending id) to prevent deadlocks.\n      -- FOR UPDATE ensures no other transaction can modify these rows concurrently.\n      IF p_from \u003C p_to THEN\n        SELECT balance INTO balance FROM accounts WHERE id = p_from FOR UPDATE;\n        SELECT balance INTO balance FROM accounts WHERE id = p_to   FOR UPDATE;\n      ELSE\n        SELECT balance INTO balance FROM accounts WHERE id = p_to   FOR UPDATE;\n        SELECT balance INTO balance FROM accounts WHERE id = p_from FOR UPDATE;\n      END IF;\n\n      -- Check sufficient balance.\n      SELECT balance INTO balance FROM accounts WHERE id = p_from;\n      IF balance \u003C p_amount THEN\n        RAISE EXCEPTION 'insufficient balance: % \u003C %', balance, p_amount;\n      END IF;\n\n      -- Atomic read-modify-write — safe at any isolation level.\n      UPDATE accounts SET balance = balance - p_amount WHERE id = p_from;\n      UPDATE accounts SET balance = balance + p_amount WHERE id = p_to;\n\n      COMMIT;\n      RETURN;  -- success, exit the retry loop\n\n    EXCEPTION\n      WHEN serialization_failure OR deadlock_detected THEN\n        ROLLBACK;\n        -- Retry the entire transaction — re-read, re-decide, re-write.\n        CONTINUE retry;\n    END;\n  END LOOP;\nEND;\n$$;\n\n-- Application-level pseudo-code (Python \u002F psycopg2):\n-- for attempt in range(max_retries):\n--     try:\n--         conn.execute(\"BEGIN ISOLATION LEVEL SERIALIZABLE\")\n--         conn.execute(\"SELECT balance FROM accounts WHERE id = %s FOR UPDATE\", (p_from,))\n--         ... check + update ...\n--         conn.execute(\"COMMIT\")\n--         break\n--     except psycopg2.errors.SerializationFailure:\n--         conn.execute(\"ROLLBACK\")\n--         continue\n--     except psycopg2.errors.DeadlockDetected:\n--         conn.execute(\"ROLLBACK\")\n--         continue\n",[113,921,922,927,932,937,942,946,961,972,981,989,1000,1008,1015,1020,1037,1053,1063,1069,1081,1087,1104,1121,1134,1145,1150,1156,1162,1176,1181,1187,1193,1210,1244,1274,1280,1309,1338,1348,1353,1359,1384,1398,1410,1419,1424,1430,1458,1486,1491,1499,1511,1516,1522,1539,1547,1553,1562,1569,1580,1588,1594,1599,1605,1611,1617,1623,1629,1635,1641,1647,1653,1659,1665,1671,1676],{"__ignoreMap":111},[116,923,924],{"class":118,"line":119},[116,925,926],{"class":122},"-- A bank transfer at SERIALIZABLE isolation with retry on serialization failure.\n",[116,928,929],{"class":118,"line":126},[116,930,931],{"class":122},"-- This PL\u002FpgSQL function encapsulates the retry loop.\n",[116,933,934],{"class":118,"line":140},[116,935,936],{"class":122},"-- In application code (Python, Go, etc.), use the same pattern with your driver's\n",[116,938,939],{"class":118,"line":181},[116,940,941],{"class":122},"-- transaction API and catch SQLSTATE 40001.\n",[116,943,944],{"class":118,"line":212},[116,945,228],{"emptyLinePlaceholder":227},[116,947,948,951,954,958],{"class":118,"line":224},[116,949,950],{"class":129},"CREATE OR REPLACE",[116,952,953],{"class":129}," FUNCTION",[116,955,957],{"class":956},"sIsaT"," transfer_money",[116,959,960],{"class":133},"(\n",[116,962,963,966,969],{"class":118,"line":231},[116,964,965],{"class":133},"  p_from   ",[116,967,968],{"class":129},"BIGINT",[116,970,971],{"class":133},",\n",[116,973,974,977,979],{"class":118,"line":237},[116,975,976],{"class":133},"  p_to     ",[116,978,968],{"class":129},[116,980,971],{"class":133},[116,982,983,986],{"class":118,"line":244},[116,984,985],{"class":133},"  p_amount ",[116,987,988],{"class":129},"NUMERIC\n",[116,990,991,994,997],{"class":118,"line":268},[116,992,993],{"class":133},") ",[116,995,996],{"class":129},"RETURNS",[116,998,999],{"class":133}," VOID\n",[116,1001,1002,1005],{"class":118,"line":280},[116,1003,1004],{"class":129},"LANGUAGE",[116,1006,1007],{"class":133}," plpgsql\n",[116,1009,1010,1012],{"class":118,"line":285},[116,1011,636],{"class":129},[116,1013,1014],{"class":133}," $$\n",[116,1016,1017],{"class":118,"line":291},[116,1018,1019],{"class":129},"DECLARE\n",[116,1021,1022,1025,1028,1031,1033,1035],{"class":118,"line":297},[116,1023,1024],{"class":133},"  max_retries ",[116,1026,1027],{"class":129},"INTEGER",[116,1029,1030],{"class":133}," :",[116,1032,155],{"class":129},[116,1034,367],{"class":163},[116,1036,178],{"class":133},[116,1038,1039,1042,1044,1046,1048,1051],{"class":118,"line":653},[116,1040,1041],{"class":133},"  attempt     ",[116,1043,1027],{"class":129},[116,1045,1030],{"class":133},[116,1047,155],{"class":129},[116,1049,1050],{"class":163}," 0",[116,1052,178],{"class":133},[116,1054,1055,1058,1061],{"class":118,"line":659},[116,1056,1057],{"class":133},"  balance     ",[116,1059,1060],{"class":129},"NUMERIC",[116,1062,178],{"class":133},[116,1064,1066],{"class":118,"line":1065},17,[116,1067,1068],{"class":129},"BEGIN\n",[116,1070,1072,1075,1078],{"class":118,"line":1071},18,[116,1073,1074],{"class":129},"  \u003C\u003C",[116,1076,1077],{"class":133},"retry",[116,1079,1080],{"class":129},">>\n",[116,1082,1084],{"class":118,"line":1083},19,[116,1085,1086],{"class":129},"  LOOP\n",[116,1088,1090,1093,1095,1098,1100,1102],{"class":118,"line":1089},20,[116,1091,1092],{"class":133},"    attempt :",[116,1094,155],{"class":129},[116,1096,1097],{"class":133}," attempt ",[116,1099,196],{"class":129},[116,1101,175],{"class":163},[116,1103,178],{"class":133},[116,1105,1107,1110,1112,1115,1118],{"class":118,"line":1106},21,[116,1108,1109],{"class":129},"    IF",[116,1111,1097],{"class":133},[116,1113,1114],{"class":129},">",[116,1116,1117],{"class":133}," max_retries ",[116,1119,1120],{"class":129},"THEN\n",[116,1122,1124,1127,1131],{"class":118,"line":1123},22,[116,1125,1126],{"class":133},"      RAISE EXCEPTION ",[116,1128,1130],{"class":1129},"sJ6F3","'transfer failed after % attempts'",[116,1132,1133],{"class":133},", max_retries;\n",[116,1135,1137,1140,1143],{"class":118,"line":1136},23,[116,1138,1139],{"class":129},"    END",[116,1141,1142],{"class":129}," IF",[116,1144,178],{"class":133},[116,1146,1148],{"class":118,"line":1147},24,[116,1149,228],{"emptyLinePlaceholder":227},[116,1151,1153],{"class":118,"line":1152},25,[116,1154,1155],{"class":129},"    BEGIN\n",[116,1157,1159],{"class":118,"line":1158},26,[116,1160,1161],{"class":122},"      -- SERIALIZABLE: detect write skew and other anomalies.\n",[116,1163,1165,1168,1170,1172,1174],{"class":118,"line":1164},27,[116,1166,1167],{"class":129},"      BEGIN",[116,1169,560],{"class":129},[116,1171,563],{"class":129},[116,1173,602],{"class":129},[116,1175,178],{"class":133},[116,1177,1179],{"class":118,"line":1178},28,[116,1180,228],{"emptyLinePlaceholder":227},[116,1182,1184],{"class":118,"line":1183},29,[116,1185,1186],{"class":122},"      -- Lock both rows in deterministic order (ascending id) to prevent deadlocks.\n",[116,1188,1190],{"class":118,"line":1189},30,[116,1191,1192],{"class":122},"      -- FOR UPDATE ensures no other transaction can modify these rows concurrently.\n",[116,1194,1196,1199,1202,1205,1208],{"class":118,"line":1195},31,[116,1197,1198],{"class":129},"      IF",[116,1200,1201],{"class":133}," p_from ",[116,1203,1204],{"class":129},"\u003C",[116,1206,1207],{"class":133}," p_to ",[116,1209,1120],{"class":129},[116,1211,1213,1216,1218,1221,1223,1226,1228,1230,1232,1234,1236,1239,1242],{"class":118,"line":1212},32,[116,1214,1215],{"class":129},"        SELECT",[116,1217,152],{"class":133},[116,1219,1220],{"class":129},"INTO",[116,1222,152],{"class":133},[116,1224,1225],{"class":129},"FROM",[116,1227,146],{"class":133},[116,1229,256],{"class":129},[116,1231,170],{"class":133},[116,1233,155],{"class":129},[116,1235,1201],{"class":133},[116,1237,1238],{"class":129},"FOR",[116,1240,1241],{"class":129}," UPDATE",[116,1243,178],{"class":133},[116,1245,1247,1249,1251,1253,1255,1257,1259,1261,1263,1265,1268,1270,1272],{"class":118,"line":1246},33,[116,1248,1215],{"class":129},[116,1250,152],{"class":133},[116,1252,1220],{"class":129},[116,1254,152],{"class":133},[116,1256,1225],{"class":129},[116,1258,146],{"class":133},[116,1260,256],{"class":129},[116,1262,170],{"class":133},[116,1264,155],{"class":129},[116,1266,1267],{"class":133}," p_to   ",[116,1269,1238],{"class":129},[116,1271,1241],{"class":129},[116,1273,178],{"class":133},[116,1275,1277],{"class":118,"line":1276},34,[116,1278,1279],{"class":129},"      ELSE\n",[116,1281,1283,1285,1287,1289,1291,1293,1295,1297,1299,1301,1303,1305,1307],{"class":118,"line":1282},35,[116,1284,1215],{"class":129},[116,1286,152],{"class":133},[116,1288,1220],{"class":129},[116,1290,152],{"class":133},[116,1292,1225],{"class":129},[116,1294,146],{"class":133},[116,1296,256],{"class":129},[116,1298,170],{"class":133},[116,1300,155],{"class":129},[116,1302,1267],{"class":133},[116,1304,1238],{"class":129},[116,1306,1241],{"class":129},[116,1308,178],{"class":133},[116,1310,1312,1314,1316,1318,1320,1322,1324,1326,1328,1330,1332,1334,1336],{"class":118,"line":1311},36,[116,1313,1215],{"class":129},[116,1315,152],{"class":133},[116,1317,1220],{"class":129},[116,1319,152],{"class":133},[116,1321,1225],{"class":129},[116,1323,146],{"class":133},[116,1325,256],{"class":129},[116,1327,170],{"class":133},[116,1329,155],{"class":129},[116,1331,1201],{"class":133},[116,1333,1238],{"class":129},[116,1335,1241],{"class":129},[116,1337,178],{"class":133},[116,1339,1341,1344,1346],{"class":118,"line":1340},37,[116,1342,1343],{"class":129},"      END",[116,1345,1142],{"class":129},[116,1347,178],{"class":133},[116,1349,1351],{"class":118,"line":1350},38,[116,1352,228],{"emptyLinePlaceholder":227},[116,1354,1356],{"class":118,"line":1355},39,[116,1357,1358],{"class":122},"      -- Check sufficient balance.\n",[116,1360,1362,1365,1367,1369,1371,1373,1375,1377,1379,1381],{"class":118,"line":1361},40,[116,1363,1364],{"class":129},"      SELECT",[116,1366,152],{"class":133},[116,1368,1220],{"class":129},[116,1370,152],{"class":133},[116,1372,1225],{"class":129},[116,1374,146],{"class":133},[116,1376,256],{"class":129},[116,1378,170],{"class":133},[116,1380,155],{"class":129},[116,1382,1383],{"class":133}," p_from;\n",[116,1385,1387,1389,1391,1393,1396],{"class":118,"line":1386},41,[116,1388,1198],{"class":129},[116,1390,152],{"class":133},[116,1392,1204],{"class":129},[116,1394,1395],{"class":133}," p_amount ",[116,1397,1120],{"class":129},[116,1399,1401,1404,1407],{"class":118,"line":1400},42,[116,1402,1403],{"class":133},"        RAISE EXCEPTION ",[116,1405,1406],{"class":1129},"'insufficient balance: % \u003C %'",[116,1408,1409],{"class":133},", balance, p_amount;\n",[116,1411,1413,1415,1417],{"class":118,"line":1412},43,[116,1414,1343],{"class":129},[116,1416,1142],{"class":129},[116,1418,178],{"class":133},[116,1420,1422],{"class":118,"line":1421},44,[116,1423,228],{"emptyLinePlaceholder":227},[116,1425,1427],{"class":118,"line":1426},45,[116,1428,1429],{"class":122},"      -- Atomic read-modify-write — safe at any isolation level.\n",[116,1431,1433,1436,1438,1440,1442,1444,1446,1448,1450,1452,1454,1456],{"class":118,"line":1432},46,[116,1434,1435],{"class":129},"      UPDATE",[116,1437,146],{"class":133},[116,1439,149],{"class":129},[116,1441,152],{"class":133},[116,1443,155],{"class":129},[116,1445,152],{"class":133},[116,1447,160],{"class":129},[116,1449,1395],{"class":133},[116,1451,256],{"class":129},[116,1453,170],{"class":133},[116,1455,155],{"class":129},[116,1457,1383],{"class":133},[116,1459,1461,1463,1465,1467,1469,1471,1473,1475,1477,1479,1481,1483],{"class":118,"line":1460},47,[116,1462,1435],{"class":129},[116,1464,146],{"class":133},[116,1466,149],{"class":129},[116,1468,152],{"class":133},[116,1470,155],{"class":129},[116,1472,152],{"class":133},[116,1474,196],{"class":129},[116,1476,1395],{"class":133},[116,1478,256],{"class":129},[116,1480,170],{"class":133},[116,1482,155],{"class":129},[116,1484,1485],{"class":133}," p_to;\n",[116,1487,1489],{"class":118,"line":1488},48,[116,1490,228],{"emptyLinePlaceholder":227},[116,1492,1494,1497],{"class":118,"line":1493},49,[116,1495,1496],{"class":129},"      COMMIT",[116,1498,178],{"class":133},[116,1500,1502,1505,1508],{"class":118,"line":1501},50,[116,1503,1504],{"class":129},"      RETURN",[116,1506,1507],{"class":133},";  ",[116,1509,1510],{"class":122},"-- success, exit the retry loop\n",[116,1512,1514],{"class":118,"line":1513},51,[116,1515,228],{"emptyLinePlaceholder":227},[116,1517,1519],{"class":118,"line":1518},52,[116,1520,1521],{"class":133},"    EXCEPTION\n",[116,1523,1525,1528,1531,1534,1537],{"class":118,"line":1524},53,[116,1526,1527],{"class":129},"      WHEN",[116,1529,1530],{"class":133}," serialization_failure ",[116,1532,1533],{"class":129},"OR",[116,1535,1536],{"class":133}," deadlock_detected ",[116,1538,1120],{"class":129},[116,1540,1542,1545],{"class":118,"line":1541},54,[116,1543,1544],{"class":129},"        ROLLBACK",[116,1546,178],{"class":133},[116,1548,1550],{"class":118,"line":1549},55,[116,1551,1552],{"class":122},"        -- Retry the entire transaction — re-read, re-decide, re-write.\n",[116,1554,1556,1559],{"class":118,"line":1555},56,[116,1557,1558],{"class":129},"        CONTINUE",[116,1560,1561],{"class":133}," retry;\n",[116,1563,1565,1567],{"class":118,"line":1564},57,[116,1566,1139],{"class":129},[116,1568,178],{"class":133},[116,1570,1572,1575,1578],{"class":118,"line":1571},58,[116,1573,1574],{"class":129},"  END",[116,1576,1577],{"class":129}," LOOP",[116,1579,178],{"class":133},[116,1581,1583,1586],{"class":118,"line":1582},59,[116,1584,1585],{"class":129},"END",[116,1587,178],{"class":133},[116,1589,1591],{"class":118,"line":1590},60,[116,1592,1593],{"class":133},"$$;\n",[116,1595,1597],{"class":118,"line":1596},61,[116,1598,228],{"emptyLinePlaceholder":227},[116,1600,1602],{"class":118,"line":1601},62,[116,1603,1604],{"class":122},"-- Application-level pseudo-code (Python \u002F psycopg2):\n",[116,1606,1608],{"class":118,"line":1607},63,[116,1609,1610],{"class":122},"-- for attempt in range(max_retries):\n",[116,1612,1614],{"class":118,"line":1613},64,[116,1615,1616],{"class":122},"--     try:\n",[116,1618,1620],{"class":118,"line":1619},65,[116,1621,1622],{"class":122},"--         conn.execute(\"BEGIN ISOLATION LEVEL SERIALIZABLE\")\n",[116,1624,1626],{"class":118,"line":1625},66,[116,1627,1628],{"class":122},"--         conn.execute(\"SELECT balance FROM accounts WHERE id = %s FOR UPDATE\", (p_from,))\n",[116,1630,1632],{"class":118,"line":1631},67,[116,1633,1634],{"class":122},"--         ... check + update ...\n",[116,1636,1638],{"class":118,"line":1637},68,[116,1639,1640],{"class":122},"--         conn.execute(\"COMMIT\")\n",[116,1642,1644],{"class":118,"line":1643},69,[116,1645,1646],{"class":122},"--         break\n",[116,1648,1650],{"class":118,"line":1649},70,[116,1651,1652],{"class":122},"--     except psycopg2.errors.SerializationFailure:\n",[116,1654,1656],{"class":118,"line":1655},71,[116,1657,1658],{"class":122},"--         conn.execute(\"ROLLBACK\")\n",[116,1660,1662],{"class":118,"line":1661},72,[116,1663,1664],{"class":122},"--         continue\n",[116,1666,1668],{"class":118,"line":1667},73,[116,1669,1670],{"class":122},"--     except psycopg2.errors.DeadlockDetected:\n",[116,1672,1674],{"class":118,"line":1673},74,[116,1675,1658],{"class":122},[116,1677,1679],{"class":118,"line":1678},75,[116,1680,1664],{"class":122},[18,1682,1684],{"id":1683},"deadlocks","Deadlocks",[14,1686,1687,1688,1691,1692,1695],{},"Two transactions each hold a lock the other needs — both wait forever. The database detects the cycle and aborts one with ",[113,1689,1690],{},"ERROR: deadlock detected"," (SQLSTATE ",[113,1693,1694],{},"40P01",").",[103,1697,1698],{"language":676},[107,1699,1702],{"className":1700,"code":1701,"language":676,"meta":111},[680],"T1: UPDATE accounts SET balance = balance - 100 WHERE id = 1;   -- locks row 1\nT2: UPDATE accounts SET balance = balance - 50  WHERE id = 2;   -- locks row 2\nT1: UPDATE accounts SET balance = balance + 100 WHERE id = 2;   -- waits for T2's lock on row 2\nT2: UPDATE accounts SET balance = balance + 50  WHERE id = 1;   -- waits for T1's lock on row 1\n-- Deadlock — database detects the cycle and aborts one transaction.\n",[113,1703,1701],{"__ignoreMap":111},[14,1705,1706,1709,1710,1713,1714,1717,1718,1721],{},[50,1707,1708],{},"Prevention",": always lock rows in a consistent order. If both transactions lock ",[113,1711,1712],{},"id=1"," then ",[113,1715,1716],{},"id=2",", no deadlock. ",[113,1719,1720],{},"SELECT ... FOR UPDATE"," establishes locks in a deterministic order before the updates.",[103,1723,1724],{"language":105},[107,1725,1727],{"className":109,"code":1726,"language":105,"meta":111,"style":111},"-- Deadlock-safe: lock both rows in ascending id order before any updates.\nBEGIN;\nSELECT id FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;\n-- Both rows locked in the same order regardless of transfer direction.\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\nCOMMIT;\n",[113,1728,1729,1734,1740,1782,1787,1815,1843],{"__ignoreMap":111},[116,1730,1731],{"class":118,"line":119},[116,1732,1733],{"class":122},"-- Deadlock-safe: lock both rows in ascending id order before any updates.\n",[116,1735,1736,1738],{"class":118,"line":126},[116,1737,130],{"class":129},[116,1739,178],{"class":133},[116,1741,1742,1744,1746,1748,1750,1752,1754,1757,1760,1763,1766,1769,1771,1774,1776,1778,1780],{"class":118,"line":140},[116,1743,807],{"class":129},[116,1745,170],{"class":133},[116,1747,1225],{"class":129},[116,1749,146],{"class":133},[116,1751,256],{"class":129},[116,1753,170],{"class":133},[116,1755,1756],{"class":129},"IN",[116,1758,1759],{"class":133}," (",[116,1761,1762],{"class":163},"1",[116,1764,1765],{"class":133},", ",[116,1767,1768],{"class":163},"2",[116,1770,993],{"class":133},[116,1772,1773],{"class":129},"ORDER BY",[116,1775,170],{"class":133},[116,1777,1238],{"class":129},[116,1779,1241],{"class":129},[116,1781,178],{"class":133},[116,1783,1784],{"class":118,"line":181},[116,1785,1786],{"class":122},"-- Both rows locked in the same order regardless of transfer direction.\n",[116,1788,1789,1791,1793,1795,1797,1799,1801,1803,1805,1807,1809,1811,1813],{"class":118,"line":212},[116,1790,143],{"class":129},[116,1792,146],{"class":133},[116,1794,149],{"class":129},[116,1796,152],{"class":133},[116,1798,155],{"class":129},[116,1800,152],{"class":133},[116,1802,160],{"class":129},[116,1804,164],{"class":163},[116,1806,167],{"class":129},[116,1808,170],{"class":133},[116,1810,155],{"class":129},[116,1812,175],{"class":163},[116,1814,178],{"class":133},[116,1816,1817,1819,1821,1823,1825,1827,1829,1831,1833,1835,1837,1839,1841],{"class":118,"line":224},[116,1818,143],{"class":129},[116,1820,146],{"class":133},[116,1822,149],{"class":129},[116,1824,152],{"class":133},[116,1826,155],{"class":129},[116,1828,152],{"class":133},[116,1830,196],{"class":129},[116,1832,164],{"class":163},[116,1834,167],{"class":129},[116,1836,170],{"class":133},[116,1838,155],{"class":129},[116,1840,207],{"class":163},[116,1842,178],{"class":133},[116,1844,1845,1847],{"class":118,"line":231},[116,1846,215],{"class":129},[116,1848,178],{"class":133},[18,1850,1852],{"id":1851},"explicit-locking-select-for-update","Explicit Locking — SELECT FOR UPDATE",[14,1854,1855],{},"Locks rows for the duration of the transaction, preventing other transactions from updating\u002Fdeleting them. Needed when you read a value, make a decision based on it, and then update.",[103,1857,1858],{"language":105},[107,1859,1861],{"className":109,"code":1860,"language":105,"meta":111,"style":111},"-- FOR UPDATE: locks rows against UPDATE\u002FDELETE\u002Fother FOR UPDATE.\n-- Needed for read-decide-write patterns to prevent lost updates.\nBEGIN;\nSELECT balance FROM accounts WHERE id = 1 FOR UPDATE;   -- locks row 1\n-- ... compute decision based on the locked value ...\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;   -- atomic update\nCOMMIT;\n\n-- Variants:\n-- FOR SHARE: locks against UPDATE\u002FDELETE but allows other FOR SHARE.\n-- FOR NO KEY UPDATE: locks but allows other FOR NO KEY UPDATE \u002F FOR SHARE (finer-grained).\n-- FOR UPDATE SKIP LOCKED: skip rows already locked (queue patterns).\n-- FOR UPDATE NOWAIT: error immediately if locked, instead of waiting.\n\n-- Job queue: grab the next available job, skipping locked ones.\n-- Multiple workers can run this concurrently without blocking each other.\nSELECT id FROM jobs WHERE status = 'pending'\nORDER BY created_at\nFOR UPDATE SKIP LOCKED\nLIMIT 1;\n-- Worker A gets job 1, Worker B skips job 1 (locked) and gets job 2.\n\n-- Lock only specific tables in a JOIN:\nSELECT * FROM orders o JOIN customers c ON o.customer_id = c.id\nWHERE o.status = 'pending'\nFOR UPDATE OF o;   -- locks only orders rows, not customers rows\n",[113,1862,1863,1868,1873,1879,1907,1912,1943,1949,1953,1958,1963,1968,1973,1978,1982,1987,1992,2014,2021,2033,2042,2047,2051,2056,2095,2110],{"__ignoreMap":111},[116,1864,1865],{"class":118,"line":119},[116,1866,1867],{"class":122},"-- FOR UPDATE: locks rows against UPDATE\u002FDELETE\u002Fother FOR UPDATE.\n",[116,1869,1870],{"class":118,"line":126},[116,1871,1872],{"class":122},"-- Needed for read-decide-write patterns to prevent lost updates.\n",[116,1874,1875,1877],{"class":118,"line":140},[116,1876,130],{"class":129},[116,1878,178],{"class":133},[116,1880,1881,1883,1885,1887,1889,1891,1893,1895,1897,1900,1902,1904],{"class":118,"line":181},[116,1882,807],{"class":129},[116,1884,152],{"class":133},[116,1886,1225],{"class":129},[116,1888,146],{"class":133},[116,1890,256],{"class":129},[116,1892,170],{"class":133},[116,1894,155],{"class":129},[116,1896,175],{"class":163},[116,1898,1899],{"class":129}," FOR",[116,1901,1241],{"class":129},[116,1903,428],{"class":133},[116,1905,1906],{"class":122},"-- locks row 1\n",[116,1908,1909],{"class":118,"line":212},[116,1910,1911],{"class":122},"-- ... compute decision based on the locked value ...\n",[116,1913,1914,1916,1918,1920,1922,1924,1926,1928,1930,1932,1934,1936,1938,1940],{"class":118,"line":224},[116,1915,143],{"class":129},[116,1917,146],{"class":133},[116,1919,149],{"class":129},[116,1921,152],{"class":133},[116,1923,155],{"class":129},[116,1925,152],{"class":133},[116,1927,160],{"class":129},[116,1929,164],{"class":163},[116,1931,167],{"class":129},[116,1933,170],{"class":133},[116,1935,155],{"class":129},[116,1937,175],{"class":163},[116,1939,428],{"class":133},[116,1941,1942],{"class":122},"-- atomic update\n",[116,1944,1945,1947],{"class":118,"line":231},[116,1946,215],{"class":129},[116,1948,178],{"class":133},[116,1950,1951],{"class":118,"line":237},[116,1952,228],{"emptyLinePlaceholder":227},[116,1954,1955],{"class":118,"line":244},[116,1956,1957],{"class":122},"-- Variants:\n",[116,1959,1960],{"class":118,"line":268},[116,1961,1962],{"class":122},"-- FOR SHARE: locks against UPDATE\u002FDELETE but allows other FOR SHARE.\n",[116,1964,1965],{"class":118,"line":280},[116,1966,1967],{"class":122},"-- FOR NO KEY UPDATE: locks but allows other FOR NO KEY UPDATE \u002F FOR SHARE (finer-grained).\n",[116,1969,1970],{"class":118,"line":285},[116,1971,1972],{"class":122},"-- FOR UPDATE SKIP LOCKED: skip rows already locked (queue patterns).\n",[116,1974,1975],{"class":118,"line":291},[116,1976,1977],{"class":122},"-- FOR UPDATE NOWAIT: error immediately if locked, instead of waiting.\n",[116,1979,1980],{"class":118,"line":297},[116,1981,228],{"emptyLinePlaceholder":227},[116,1983,1984],{"class":118,"line":653},[116,1985,1986],{"class":122},"-- Job queue: grab the next available job, skipping locked ones.\n",[116,1988,1989],{"class":118,"line":659},[116,1990,1991],{"class":122},"-- Multiple workers can run this concurrently without blocking each other.\n",[116,1993,1994,1996,1998,2000,2003,2005,2008,2011],{"class":118,"line":1065},[116,1995,807],{"class":129},[116,1997,170],{"class":133},[116,1999,1225],{"class":129},[116,2001,2002],{"class":133}," jobs ",[116,2004,256],{"class":129},[116,2006,2007],{"class":129}," status",[116,2009,2010],{"class":129}," =",[116,2012,2013],{"class":1129}," 'pending'\n",[116,2015,2016,2018],{"class":118,"line":1071},[116,2017,1773],{"class":129},[116,2019,2020],{"class":133}," created_at\n",[116,2022,2023,2025,2027,2030],{"class":118,"line":1083},[116,2024,1238],{"class":129},[116,2026,1241],{"class":129},[116,2028,2029],{"class":129}," SKIP",[116,2031,2032],{"class":133}," LOCKED\n",[116,2034,2035,2038,2040],{"class":118,"line":1089},[116,2036,2037],{"class":129},"LIMIT",[116,2039,175],{"class":163},[116,2041,178],{"class":133},[116,2043,2044],{"class":118,"line":1106},[116,2045,2046],{"class":122},"-- Worker A gets job 1, Worker B skips job 1 (locked) and gets job 2.\n",[116,2048,2049],{"class":118,"line":1123},[116,2050,228],{"emptyLinePlaceholder":227},[116,2052,2053],{"class":118,"line":1136},[116,2054,2055],{"class":122},"-- Lock only specific tables in a JOIN:\n",[116,2057,2058,2060,2063,2065,2068,2071,2074,2077,2080,2082,2085,2087,2090,2092],{"class":118,"line":1147},[116,2059,807],{"class":129},[116,2061,2062],{"class":129}," *",[116,2064,250],{"class":129},[116,2066,2067],{"class":133}," orders o ",[116,2069,2070],{"class":129},"JOIN",[116,2072,2073],{"class":133}," customers c ",[116,2075,2076],{"class":129},"ON",[116,2078,2079],{"class":163}," o",[116,2081,848],{"class":133},[116,2083,2084],{"class":163},"customer_id",[116,2086,2010],{"class":129},[116,2088,2089],{"class":163}," c",[116,2091,848],{"class":133},[116,2093,2094],{"class":163},"id\n",[116,2096,2097,2099,2101,2103,2106,2108],{"class":118,"line":1152},[116,2098,256],{"class":129},[116,2100,2079],{"class":163},[116,2102,848],{"class":133},[116,2104,2105],{"class":163},"status",[116,2107,2010],{"class":129},[116,2109,2013],{"class":1129},[116,2111,2112,2114,2116,2119],{"class":118,"line":1158},[116,2113,1238],{"class":129},[116,2115,1241],{"class":129},[116,2117,2118],{"class":133}," OF o;   ",[116,2120,2121],{"class":122},"-- locks only orders rows, not customers rows\n",[14,2123,2124,2127],{},[113,2125,2126],{},"SKIP LOCKED"," is the foundation of safe job queues in the database. It's a PG\u002FMySQL\u002FOracle extension (not in the SQL standard; SQLite lacks it).",[18,2129,2131],{"id":2130},"advisory-locks","Advisory Locks",[14,2133,2134],{},"Application-level locks that aren't tied to table rows — for coordinating across transactions at a higher level.",[103,2136,2137],{"language":105},[107,2138,2140],{"className":109,"code":2139,"language":105,"meta":111,"style":111},"-- Session-level advisory lock: held until explicitly released or session ends.\nSELECT pg_advisory_lock(12345);   -- lock with key 12345\n-- ... critical section ...\nSELECT pg_advisory_unlock(12345);\n\n-- Transaction-level advisory lock: auto-released at COMMIT\u002FROLLBACK.\nBEGIN;\nSELECT pg_advisory_xact_lock(12345);  -- auto-released at transaction end\n-- ... critical section ...\nCOMMIT;  -- lock released automatically\n\n-- Use cases: migrate coordination (only one worker runs a migration),\n-- global rate limiting, serializing access to an external resource.\n",[113,2141,2142,2147,2163,2168,2180,2184,2189,2195,2210,2214,2223,2227,2232],{"__ignoreMap":111},[116,2143,2144],{"class":118,"line":119},[116,2145,2146],{"class":122},"-- Session-level advisory lock: held until explicitly released or session ends.\n",[116,2148,2149,2151,2154,2157,2160],{"class":118,"line":126},[116,2150,807],{"class":129},[116,2152,2153],{"class":133}," pg_advisory_lock(",[116,2155,2156],{"class":163},"12345",[116,2158,2159],{"class":133},");   ",[116,2161,2162],{"class":122},"-- lock with key 12345\n",[116,2164,2165],{"class":118,"line":140},[116,2166,2167],{"class":122},"-- ... critical section ...\n",[116,2169,2170,2172,2175,2177],{"class":118,"line":181},[116,2171,807],{"class":129},[116,2173,2174],{"class":133}," pg_advisory_unlock(",[116,2176,2156],{"class":163},[116,2178,2179],{"class":133},");\n",[116,2181,2182],{"class":118,"line":212},[116,2183,228],{"emptyLinePlaceholder":227},[116,2185,2186],{"class":118,"line":224},[116,2187,2188],{"class":122},"-- Transaction-level advisory lock: auto-released at COMMIT\u002FROLLBACK.\n",[116,2190,2191,2193],{"class":118,"line":231},[116,2192,130],{"class":129},[116,2194,178],{"class":133},[116,2196,2197,2199,2202,2204,2207],{"class":118,"line":237},[116,2198,807],{"class":129},[116,2200,2201],{"class":133}," pg_advisory_xact_lock(",[116,2203,2156],{"class":163},[116,2205,2206],{"class":133},");  ",[116,2208,2209],{"class":122},"-- auto-released at transaction end\n",[116,2211,2212],{"class":118,"line":244},[116,2213,2167],{"class":122},[116,2215,2216,2218,2220],{"class":118,"line":268},[116,2217,215],{"class":129},[116,2219,1507],{"class":133},[116,2221,2222],{"class":122},"-- lock released automatically\n",[116,2224,2225],{"class":118,"line":280},[116,2226,228],{"emptyLinePlaceholder":227},[116,2228,2229],{"class":118,"line":285},[116,2230,2231],{"class":122},"-- Use cases: migrate coordination (only one worker runs a migration),\n",[116,2233,2234],{"class":118,"line":291},[116,2235,2236],{"class":122},"-- global rate limiting, serializing access to an external resource.\n",[18,2238,2240],{"id":2239},"anti-pattern-long-running-transactions-holding-locks","Anti-Pattern: Long-Running Transactions Holding Locks",[103,2242,2243],{"language":105},[107,2244,2246],{"className":109,"code":2245,"language":105,"meta":111,"style":111},"-- ❌ WRONG: transaction spans an HTTP call to an external service.\n-- The external service's latency now determines your lock hold time.\n-- If it's slow, other transactions block, connections exhaust, app hangs.\n\n-- Pseudo-code (application layer):\n-- BEGIN;\n-- SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;  -- locks row\n-- response = http.post('https:\u002F\u002Fapi.payment.com\u002Fcharge', ...)  -- 30s timeout!\n-- UPDATE accounts SET balance = balance - 100 WHERE id = 1;\n-- COMMIT;  -- lock held for 30+ seconds\n\n-- ✅ CORRECT: keep the transaction tight — do the HTTP call outside.\n-- 1. Read + decide (no lock).\n-- 2. Call the external service (outside transaction).\n-- 3. BEGIN; lock; verify decision still holds; update; COMMIT (milliseconds).\n\nresponse = http.post('https:\u002F\u002Fapi.payment.com\u002Fcharge', ...)\nif response.success:\n    BEGIN;\n    SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;\n    -- verify balance still sufficient, then update\n    UPDATE accounts SET balance = balance - 100 WHERE id = 1;\n    COMMIT;  -- lock held for \u003C1ms\n",[113,2247,2248,2253,2258,2263,2267,2272,2277,2282,2287,2292,2297,2301,2306,2311,2316,2321,2325,2349,2365,2372,2397,2402,2431],{"__ignoreMap":111},[116,2249,2250],{"class":118,"line":119},[116,2251,2252],{"class":122},"-- ❌ WRONG: transaction spans an HTTP call to an external service.\n",[116,2254,2255],{"class":118,"line":126},[116,2256,2257],{"class":122},"-- The external service's latency now determines your lock hold time.\n",[116,2259,2260],{"class":118,"line":140},[116,2261,2262],{"class":122},"-- If it's slow, other transactions block, connections exhaust, app hangs.\n",[116,2264,2265],{"class":118,"line":181},[116,2266,228],{"emptyLinePlaceholder":227},[116,2268,2269],{"class":118,"line":212},[116,2270,2271],{"class":122},"-- Pseudo-code (application layer):\n",[116,2273,2274],{"class":118,"line":224},[116,2275,2276],{"class":122},"-- BEGIN;\n",[116,2278,2279],{"class":118,"line":231},[116,2280,2281],{"class":122},"-- SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;  -- locks row\n",[116,2283,2284],{"class":118,"line":237},[116,2285,2286],{"class":122},"-- response = http.post('https:\u002F\u002Fapi.payment.com\u002Fcharge', ...)  -- 30s timeout!\n",[116,2288,2289],{"class":118,"line":244},[116,2290,2291],{"class":122},"-- UPDATE accounts SET balance = balance - 100 WHERE id = 1;\n",[116,2293,2294],{"class":118,"line":268},[116,2295,2296],{"class":122},"-- COMMIT;  -- lock held for 30+ seconds\n",[116,2298,2299],{"class":118,"line":280},[116,2300,228],{"emptyLinePlaceholder":227},[116,2302,2303],{"class":118,"line":285},[116,2304,2305],{"class":122},"-- ✅ CORRECT: keep the transaction tight — do the HTTP call outside.\n",[116,2307,2308],{"class":118,"line":291},[116,2309,2310],{"class":122},"-- 1. Read + decide (no lock).\n",[116,2312,2313],{"class":118,"line":297},[116,2314,2315],{"class":122},"-- 2. Call the external service (outside transaction).\n",[116,2317,2318],{"class":118,"line":653},[116,2319,2320],{"class":122},"-- 3. BEGIN; lock; verify decision still holds; update; COMMIT (milliseconds).\n",[116,2322,2323],{"class":118,"line":659},[116,2324,228],{"emptyLinePlaceholder":227},[116,2326,2327,2330,2332,2335,2337,2340,2343,2346],{"class":118,"line":1065},[116,2328,2329],{"class":133},"response ",[116,2331,155],{"class":129},[116,2333,2334],{"class":163}," http",[116,2336,848],{"class":133},[116,2338,2339],{"class":163},"post",[116,2341,2342],{"class":133},"(",[116,2344,2345],{"class":1129},"'https:\u002F\u002Fapi.payment.com\u002Fcharge'",[116,2347,2348],{"class":133},", ...)\n",[116,2350,2351,2354,2357,2359,2362],{"class":118,"line":1071},[116,2352,2353],{"class":129},"if",[116,2355,2356],{"class":163}," response",[116,2358,848],{"class":133},[116,2360,2361],{"class":163},"success",[116,2363,2364],{"class":133},":\n",[116,2366,2367,2370],{"class":118,"line":1083},[116,2368,2369],{"class":129},"    BEGIN",[116,2371,178],{"class":133},[116,2373,2374,2377,2379,2381,2383,2385,2387,2389,2391,2393,2395],{"class":118,"line":1089},[116,2375,2376],{"class":129},"    SELECT",[116,2378,152],{"class":133},[116,2380,1225],{"class":129},[116,2382,146],{"class":133},[116,2384,256],{"class":129},[116,2386,170],{"class":133},[116,2388,155],{"class":129},[116,2390,175],{"class":163},[116,2392,1899],{"class":129},[116,2394,1241],{"class":129},[116,2396,178],{"class":133},[116,2398,2399],{"class":118,"line":1106},[116,2400,2401],{"class":122},"    -- verify balance still sufficient, then update\n",[116,2403,2404,2407,2409,2411,2413,2415,2417,2419,2421,2423,2425,2427,2429],{"class":118,"line":1123},[116,2405,2406],{"class":129},"    UPDATE",[116,2408,146],{"class":133},[116,2410,149],{"class":129},[116,2412,152],{"class":133},[116,2414,155],{"class":129},[116,2416,152],{"class":133},[116,2418,160],{"class":129},[116,2420,164],{"class":163},[116,2422,167],{"class":129},[116,2424,170],{"class":133},[116,2426,155],{"class":129},[116,2428,175],{"class":163},[116,2430,178],{"class":133},[116,2432,2433,2436,2438],{"class":118,"line":1136},[116,2434,2435],{"class":129},"    COMMIT",[116,2437,1507],{"class":133},[116,2439,2440],{"class":122},"-- lock held for \u003C1ms\n",[18,2442,2444],{"id":2443},"anti-pattern-ignoring-serialization-failures-without-retry","Anti-Pattern: Ignoring Serialization Failures Without Retry",[103,2446,2447],{"language":105},[107,2448,2450],{"className":109,"code":2449,"language":105,"meta":111,"style":111},"-- ❌ WRONG: using SERIALIZABLE but not retrying on serialization_failure.\n-- SSI will abort transactions it detects as anomalies — if you don't retry,\n-- the transaction just fails. You get all the throughput cost of SERIALIZABLE\n-- with none of the correctness benefit.\n\n-- Pseudo-code (application layer):\n-- try:\n--     BEGIN ISOLATION LEVEL SERIALIZABLE\n--     ... work ...\n--     COMMIT\n-- except:\n--     ROLLBACK\n--     raise  -- ❌ just propagates the error, no retry\n\n-- ✅ CORRECT: retry the entire transaction on SQLSTATE 40001.\n-- for attempt in range(max_retries):\n--     try:\n--         BEGIN ISOLATION LEVEL SERIALIZABLE\n--         ... work ...\n--         COMMIT\n--         break\n--     except sqlstate == '40001':  # serialization_failure\n--         ROLLBACK\n--         continue\n--     except sqlstate == '40P01':  # deadlock_detected\n--         ROLLBACK\n--         continue\n",[113,2451,2452,2457,2462,2467,2472,2476,2480,2485,2490,2495,2500,2505,2510,2515,2519,2524,2528,2532,2537,2542,2547,2551,2556,2561,2565,2570,2574],{"__ignoreMap":111},[116,2453,2454],{"class":118,"line":119},[116,2455,2456],{"class":122},"-- ❌ WRONG: using SERIALIZABLE but not retrying on serialization_failure.\n",[116,2458,2459],{"class":118,"line":126},[116,2460,2461],{"class":122},"-- SSI will abort transactions it detects as anomalies — if you don't retry,\n",[116,2463,2464],{"class":118,"line":140},[116,2465,2466],{"class":122},"-- the transaction just fails. You get all the throughput cost of SERIALIZABLE\n",[116,2468,2469],{"class":118,"line":181},[116,2470,2471],{"class":122},"-- with none of the correctness benefit.\n",[116,2473,2474],{"class":118,"line":212},[116,2475,228],{"emptyLinePlaceholder":227},[116,2477,2478],{"class":118,"line":224},[116,2479,2271],{"class":122},[116,2481,2482],{"class":118,"line":231},[116,2483,2484],{"class":122},"-- try:\n",[116,2486,2487],{"class":118,"line":237},[116,2488,2489],{"class":122},"--     BEGIN ISOLATION LEVEL SERIALIZABLE\n",[116,2491,2492],{"class":118,"line":244},[116,2493,2494],{"class":122},"--     ... work ...\n",[116,2496,2497],{"class":118,"line":268},[116,2498,2499],{"class":122},"--     COMMIT\n",[116,2501,2502],{"class":118,"line":280},[116,2503,2504],{"class":122},"-- except:\n",[116,2506,2507],{"class":118,"line":285},[116,2508,2509],{"class":122},"--     ROLLBACK\n",[116,2511,2512],{"class":118,"line":291},[116,2513,2514],{"class":122},"--     raise  -- ❌ just propagates the error, no retry\n",[116,2516,2517],{"class":118,"line":297},[116,2518,228],{"emptyLinePlaceholder":227},[116,2520,2521],{"class":118,"line":653},[116,2522,2523],{"class":122},"-- ✅ CORRECT: retry the entire transaction on SQLSTATE 40001.\n",[116,2525,2526],{"class":118,"line":659},[116,2527,1610],{"class":122},[116,2529,2530],{"class":118,"line":1065},[116,2531,1616],{"class":122},[116,2533,2534],{"class":118,"line":1071},[116,2535,2536],{"class":122},"--         BEGIN ISOLATION LEVEL SERIALIZABLE\n",[116,2538,2539],{"class":118,"line":1083},[116,2540,2541],{"class":122},"--         ... work ...\n",[116,2543,2544],{"class":118,"line":1089},[116,2545,2546],{"class":122},"--         COMMIT\n",[116,2548,2549],{"class":118,"line":1106},[116,2550,1646],{"class":122},[116,2552,2553],{"class":118,"line":1123},[116,2554,2555],{"class":122},"--     except sqlstate == '40001':  # serialization_failure\n",[116,2557,2558],{"class":118,"line":1136},[116,2559,2560],{"class":122},"--         ROLLBACK\n",[116,2562,2563],{"class":118,"line":1147},[116,2564,1664],{"class":122},[116,2566,2567],{"class":118,"line":1152},[116,2568,2569],{"class":122},"--     except sqlstate == '40P01':  # deadlock_detected\n",[116,2571,2572],{"class":118,"line":1158},[116,2573,2560],{"class":122},[116,2575,2576],{"class":118,"line":1164},[116,2577,1664],{"class":122},[18,2579,2581],{"id":2580},"tips-tricks","💡 Tips & Tricks",[758,2583,2584,2601,2607,2616,2628,2644,2659],{},[761,2585,2586,2592,2593,2596,2597,2600],{},[50,2587,2588,2589,2591],{},"Idiom — ",[113,2590,1720],{}," for read-decide-write",": without it, you have a lost-update race (T2's update overwrites T1's, even at REPEATABLE READ). At SERIALIZABLE the database detects the conflict and aborts, but at lower levels the lost update is ",[731,2594,2595],{},"silent",". Always use ",[113,2598,2599],{},"FOR UPDATE"," when you read a value, decide, then write.",[761,2602,2603,2606],{},[50,2604,2605],{},"Performance — keep transactions short",": long transactions hold locks, block VACUUM (dead tuples can't be reclaimed until all transactions that might see them finish), and increase bloat. A transaction spanning an HTTP call is a classic anti-pattern — the external service's latency determines your lock hold time.",[761,2608,2609,2615],{},[50,2610,2588,2611,2614],{},[113,2612,2613],{},"FOR UPDATE SKIP LOCKED"," for job queues",": workers grab rows without blocking each other; locked rows are skipped. Simplest correct queue pattern in the database. At high scale, use dedicated queues (RabbitMQ, SQS).",[761,2617,2618,2623,2624,2627],{},[50,2619,2620,2621],{},"Idiom — SERIALIZABLE with retry on ",[113,2622,854],{},": without retry, SERIALIZABLE just adds aborts with no benefit. Retry the ",[731,2625,2626],{},"entire"," transaction (re-read, re-decide, re-write), not just the failing statement.",[761,2629,2630,2633,2634,2636,2637,2639,2640,2643],{},[50,2631,2632],{},"Debug — deadlocks indicate inconsistent lock ordering",": audit the order in which transactions acquire locks (via ",[113,2635,2599],{}," or ",[113,2638,143],{},") and enforce a global order (e.g., always lock by ascending ",[113,2641,2642],{},"id","). Once locks are acquired in a consistent order, cycles can't form.",[761,2645,2646,2654,2655,2658],{},[50,2647,2588,2648,2651,2652],{},[113,2649,2650],{},"SET TRANSACTION ISOLATION LEVEL"," right after ",[113,2653,130],{},": the snapshot is taken at the first statement. Setting it after the first query is too late. Use ",[113,2656,2657],{},"BEGIN ISOLATION LEVEL SERIALIZABLE"," for clarity.",[761,2660,2661,2664,2665,2668],{},[50,2662,2663],{},"Idiom — advisory locks for application-level coordination",": ",[113,2666,2667],{},"pg_advisory_xact_lock(key)"," for migration coordination, global serialization, or external resource access — auto-released at COMMIT.",[18,2670,2672],{"id":2671},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[758,2674,2675,2690,2700,2706,2717,2745,2753,2767,2783,2792,2806,2822],{},[761,2676,2677,2664,2680,2683,2684,1765,2686,2689],{},[50,2678,2679],{},"Lost updates at READ COMMITTED",[113,2681,2682],{},"T1: read x=10; T2: read x=10; T1: write x=11; T2: write x=11"," — T2 overwrites T1's update, both think they incremented, result is 11 not 12. READ COMMITTED doesn't prevent this. Use ",[113,2685,2599],{},[113,2687,2688],{},"UPDATE ... SET x = x + 1"," (atomic), or SERIALIZABLE.",[761,2691,2692,2695,2696,2699],{},[50,2693,2694],{},"REPEATABLE READ \"first-updater-wins\"",": if T2 (REPEATABLE READ) tries to update a row T1 already updated and committed, T2 gets ",[113,2697,2698],{},"could not serialize access due to concurrent update"," — T2 must retry. Correct but surprises people expecting \"last writer wins.\"",[761,2701,2702,2705],{},[50,2703,2704],{},"SERIALIZABLE can abort transactions that would have been fine"," — SSI is conservative (false positives). This is the cost of serializability without global locks. Retry handles it.",[761,2707,2708,2713,2714,2716],{},[50,2709,2710,2711],{},"SERIALIZABLE serialization_failure SQLSTATE is ",[113,2712,854],{},": catch this specifically in your driver. Deadlock is ",[113,2715,1694],{},". Both require retry.",[761,2718,2719,2664,2726,2729,2730,2733,2734,2737,2738,2741,2742,2744],{},[50,2720,2721,2723,2724],{},[113,2722,2599],{}," on a ",[113,2725,2070],{},[113,2727,2728],{},"SELECT ... FROM a JOIN b ... FOR UPDATE"," locks all rows in ",[113,2731,2732],{},"a"," and ",[113,2735,2736],{},"b",". Use ",[113,2739,2740],{},"FOR UPDATE OF a"," to lock only ",[113,2743,2732],{},"'s rows.",[761,2746,2747,2752],{},[50,2748,2749,2751],{},[113,2750,2126],{}," is not in the SQL standard",": PG\u002FMySQL\u002FOracle extension (SQLite lacks it). The locked-row-skipping semantics are essential for queues but not portable.",[761,2754,2755,2758,2759,2762,2763,2766],{},[50,2756,2757],{},"Long transactions block VACUUM",": any transaction older than dead tuples prevents their cleanup — bloat grows. Monitor with ",[113,2760,2761],{},"pg_stat_activity"," (long-running transactions) and ",[113,2764,2765],{},"pg_stat_user_tables"," (dead tuple count).",[761,2768,2769,2664,2774,2777,2778,2780,2781,848],{},[50,2770,2771,2773],{},[113,2772,130],{}," doesn't set isolation immediately",[113,2775,2776],{},"SET TRANSACTION ISOLATION LEVEL ..."," after the first query is too late — the snapshot is taken at the first statement. Set it right after ",[113,2779,130],{},", or use ",[113,2782,2657],{},[761,2784,2785,2788,2789,2791],{},[50,2786,2787],{},"Autocommit and multi-statement transactions",": in MySQL (autocommit on), ",[113,2790,130],{}," starts a transaction; in PG, every statement is in a transaction (implicit if not explicit). Application drivers differ — know your driver's default.",[761,2793,2794,2799,2800,2802,2803,2805],{},[50,2795,2796,2798],{},[113,2797,215],{}," can fail",": a serialization failure or deferred constraint violation can surface at ",[113,2801,215],{},", not at the offending statement. Always check ",[113,2804,215],{},"'s result, not just the statements'.",[761,2807,2808,2811,2812,2814,2815,2817,2818,2821],{},[50,2809,2810],{},"Idle in transaction",": a transaction that's ",[113,2813,130],{}," but never ",[113,2816,215],{},"ed holds locks and blocks VACUUM. Common with connection pools that leak transactions. Set ",[113,2819,2820],{},"idle_in_transaction_session_timeout"," (PG) to auto-kill them.",[761,2823,2824,2827,2828,2831],{},[50,2825,2826],{},"REPEATABLE READ in MySQL vs PG",": MySQL's REPEATABLE READ does ",[731,2829,2830],{},"not"," prevent all phantoms (it uses gap locking, which has edge cases). PG's REPEATABLE READ (snapshot isolation) prevents phantoms completely but allows write skew.",[18,2833,2835],{"id":2834},"spot-the-bug","🧠 Spot the Bug",[14,2837,2838],{},"Two tellers transfer money between the same two accounts at the same time. After both transfers, the totals are wrong. What happened, and how do you fix it?",[103,2840,2841],{"language":105},[107,2842,2844],{"className":109,"code":2843,"language":105,"meta":111,"style":111},"-- T1: transfer $100 from account 1 to account 2\nBEGIN;\nSELECT balance FROM accounts WHERE id = 1;   -- 1000\nSELECT balance FROM accounts WHERE id = 2;   -- 500\nUPDATE accounts SET balance = 900 WHERE id = 1;  -- ← writes a computed value!\nUPDATE accounts SET balance = 600 WHERE id = 2;  -- ← writes a computed value!\nCOMMIT;\n\n-- T2 (concurrently): transfer $50 from account 1 to account 2\nBEGIN;\nSELECT balance FROM accounts WHERE id = 1;   -- 1000 (or 900)\nSELECT balance FROM accounts WHERE id = 2;   -- 500  (or 600)\nUPDATE accounts SET balance = 950 WHERE id = 1;  -- ← computed from the SELECT\nUPDATE accounts SET balance = 550 WHERE id = 2;  -- ← computed from the SELECT\nCOMMIT;\n",[113,2845,2846,2851,2857,2880,2903,2931,2958,2964,2968,2973,2979,3002,3025,3053,3080],{"__ignoreMap":111},[116,2847,2848],{"class":118,"line":119},[116,2849,2850],{"class":122},"-- T1: transfer $100 from account 1 to account 2\n",[116,2852,2853,2855],{"class":118,"line":126},[116,2854,130],{"class":129},[116,2856,178],{"class":133},[116,2858,2859,2861,2863,2865,2867,2869,2871,2873,2875,2877],{"class":118,"line":140},[116,2860,807],{"class":129},[116,2862,152],{"class":133},[116,2864,1225],{"class":129},[116,2866,146],{"class":133},[116,2868,256],{"class":129},[116,2870,170],{"class":133},[116,2872,155],{"class":129},[116,2874,175],{"class":163},[116,2876,428],{"class":133},[116,2878,2879],{"class":122},"-- 1000\n",[116,2881,2882,2884,2886,2888,2890,2892,2894,2896,2898,2900],{"class":118,"line":181},[116,2883,807],{"class":129},[116,2885,152],{"class":133},[116,2887,1225],{"class":129},[116,2889,146],{"class":133},[116,2891,256],{"class":129},[116,2893,170],{"class":133},[116,2895,155],{"class":129},[116,2897,207],{"class":163},[116,2899,428],{"class":133},[116,2901,2902],{"class":122},"-- 500\n",[116,2904,2905,2907,2909,2911,2913,2915,2918,2920,2922,2924,2926,2928],{"class":118,"line":212},[116,2906,143],{"class":129},[116,2908,146],{"class":133},[116,2910,149],{"class":129},[116,2912,152],{"class":133},[116,2914,155],{"class":129},[116,2916,2917],{"class":163}," 900",[116,2919,167],{"class":129},[116,2921,170],{"class":133},[116,2923,155],{"class":129},[116,2925,175],{"class":163},[116,2927,1507],{"class":133},[116,2929,2930],{"class":122},"-- ← writes a computed value!\n",[116,2932,2933,2935,2937,2939,2941,2943,2946,2948,2950,2952,2954,2956],{"class":118,"line":224},[116,2934,143],{"class":129},[116,2936,146],{"class":133},[116,2938,149],{"class":129},[116,2940,152],{"class":133},[116,2942,155],{"class":129},[116,2944,2945],{"class":163}," 600",[116,2947,167],{"class":129},[116,2949,170],{"class":133},[116,2951,155],{"class":129},[116,2953,207],{"class":163},[116,2955,1507],{"class":133},[116,2957,2930],{"class":122},[116,2959,2960,2962],{"class":118,"line":231},[116,2961,215],{"class":129},[116,2963,178],{"class":133},[116,2965,2966],{"class":118,"line":237},[116,2967,228],{"emptyLinePlaceholder":227},[116,2969,2970],{"class":118,"line":244},[116,2971,2972],{"class":122},"-- T2 (concurrently): transfer $50 from account 1 to account 2\n",[116,2974,2975,2977],{"class":118,"line":268},[116,2976,130],{"class":129},[116,2978,178],{"class":133},[116,2980,2981,2983,2985,2987,2989,2991,2993,2995,2997,2999],{"class":118,"line":280},[116,2982,807],{"class":129},[116,2984,152],{"class":133},[116,2986,1225],{"class":129},[116,2988,146],{"class":133},[116,2990,256],{"class":129},[116,2992,170],{"class":133},[116,2994,155],{"class":129},[116,2996,175],{"class":163},[116,2998,428],{"class":133},[116,3000,3001],{"class":122},"-- 1000 (or 900)\n",[116,3003,3004,3006,3008,3010,3012,3014,3016,3018,3020,3022],{"class":118,"line":285},[116,3005,807],{"class":129},[116,3007,152],{"class":133},[116,3009,1225],{"class":129},[116,3011,146],{"class":133},[116,3013,256],{"class":129},[116,3015,170],{"class":133},[116,3017,155],{"class":129},[116,3019,207],{"class":163},[116,3021,428],{"class":133},[116,3023,3024],{"class":122},"-- 500  (or 600)\n",[116,3026,3027,3029,3031,3033,3035,3037,3040,3042,3044,3046,3048,3050],{"class":118,"line":291},[116,3028,143],{"class":129},[116,3030,146],{"class":133},[116,3032,149],{"class":129},[116,3034,152],{"class":133},[116,3036,155],{"class":129},[116,3038,3039],{"class":163}," 950",[116,3041,167],{"class":129},[116,3043,170],{"class":133},[116,3045,155],{"class":129},[116,3047,175],{"class":163},[116,3049,1507],{"class":133},[116,3051,3052],{"class":122},"-- ← computed from the SELECT\n",[116,3054,3055,3057,3059,3061,3063,3065,3068,3070,3072,3074,3076,3078],{"class":118,"line":297},[116,3056,143],{"class":129},[116,3058,146],{"class":133},[116,3060,149],{"class":129},[116,3062,152],{"class":133},[116,3064,155],{"class":129},[116,3066,3067],{"class":163}," 550",[116,3069,167],{"class":129},[116,3071,170],{"class":133},[116,3073,155],{"class":129},[116,3075,207],{"class":163},[116,3077,1507],{"class":133},[116,3079,3052],{"class":122},[116,3081,3082,3084],{"class":118,"line":653},[116,3083,215],{"class":129},[116,3085,178],{"class":133},[3087,3088,3089,3093,3114,3128,3141,3155,3281,3295],"details",{},[3090,3091,3092],"summary",{},"Answer",[14,3094,3095,3096,3098,3099,3102,3103,3106,3107,3110,3111,3113],{},"The application reads the balance via ",[113,3097,807],{},", computes the new balance in application code (",[113,3100,3101],{},"1000 - 100 = 900","), and writes it back as a literal (",[113,3104,3105],{},"SET balance = 900","). This is a ",[50,3108,3109],{},"lost update"," — T2 overwrites T1's update because T2's ",[113,3112,807],{}," saw the pre-T1 value and wrote a computed result, not a delta.",[14,3115,3116,3117,3119,3120,3123,3124,3127],{},"If T1 commits before T2's ",[113,3118,143],{},", T2's ",[113,3121,3122],{},"SET balance = 950"," overwrites T1's ",[113,3125,3126],{},"balance = 900",". The $100 transfer is lost. Final balance: 950 instead of 850.",[14,3129,3130,3131,3133,3134,3137,3138,3140],{},"The ",[113,3132,807],{}," reads are ",[731,3135,3136],{},"not locked",", so they don't prevent the other transaction from modifying the rows between the read and the write. At READ COMMITTED, the lost update is ",[50,3139,2595],{}," — no error, just wrong data.",[14,3142,3143,3144,3147,3148,3151,3152,3154],{},"The fix — use atomic read-modify-write (",[113,3145,3146],{},"balance = balance - X",", not ",[113,3149,3150],{},"balance = \u003Ccomputed value>",") and ",[113,3153,2599],{}," if you need to read-decide-write:",[103,3156,3157],{"language":105},[107,3158,3160],{"className":109,"code":3159,"language":105,"meta":111,"style":111},"BEGIN;\n-- Lock both rows in deterministic order (ascending id) to prevent deadlocks.\nSELECT balance FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;\n-- Atomic read-modify-write: the DB reads the current value and writes the delta\n-- in one step — safe at any isolation level, no lost update possible.\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\nCOMMIT;\n",[113,3161,3162,3168,3173,3209,3214,3219,3247,3275],{"__ignoreMap":111},[116,3163,3164,3166],{"class":118,"line":119},[116,3165,130],{"class":129},[116,3167,178],{"class":133},[116,3169,3170],{"class":118,"line":126},[116,3171,3172],{"class":122},"-- Lock both rows in deterministic order (ascending id) to prevent deadlocks.\n",[116,3174,3175,3177,3179,3181,3183,3185,3187,3189,3191,3193,3195,3197,3199,3201,3203,3205,3207],{"class":118,"line":140},[116,3176,807],{"class":129},[116,3178,152],{"class":133},[116,3180,1225],{"class":129},[116,3182,146],{"class":133},[116,3184,256],{"class":129},[116,3186,170],{"class":133},[116,3188,1756],{"class":129},[116,3190,1759],{"class":133},[116,3192,1762],{"class":163},[116,3194,1765],{"class":133},[116,3196,1768],{"class":163},[116,3198,993],{"class":133},[116,3200,1773],{"class":129},[116,3202,170],{"class":133},[116,3204,1238],{"class":129},[116,3206,1241],{"class":129},[116,3208,178],{"class":133},[116,3210,3211],{"class":118,"line":181},[116,3212,3213],{"class":122},"-- Atomic read-modify-write: the DB reads the current value and writes the delta\n",[116,3215,3216],{"class":118,"line":212},[116,3217,3218],{"class":122},"-- in one step — safe at any isolation level, no lost update possible.\n",[116,3220,3221,3223,3225,3227,3229,3231,3233,3235,3237,3239,3241,3243,3245],{"class":118,"line":224},[116,3222,143],{"class":129},[116,3224,146],{"class":133},[116,3226,149],{"class":129},[116,3228,152],{"class":133},[116,3230,155],{"class":129},[116,3232,152],{"class":133},[116,3234,160],{"class":129},[116,3236,164],{"class":163},[116,3238,167],{"class":129},[116,3240,170],{"class":133},[116,3242,155],{"class":129},[116,3244,175],{"class":163},[116,3246,178],{"class":133},[116,3248,3249,3251,3253,3255,3257,3259,3261,3263,3265,3267,3269,3271,3273],{"class":118,"line":231},[116,3250,143],{"class":129},[116,3252,146],{"class":133},[116,3254,149],{"class":129},[116,3256,152],{"class":133},[116,3258,155],{"class":129},[116,3260,152],{"class":133},[116,3262,196],{"class":129},[116,3264,164],{"class":163},[116,3266,167],{"class":129},[116,3268,170],{"class":133},[116,3270,155],{"class":129},[116,3272,207],{"class":163},[116,3274,178],{"class":133},[116,3276,3277,3279],{"class":118,"line":237},[116,3278,215],{"class":129},[116,3280,178],{"class":133},[14,3282,3283,3285,3286,3288,3289,3291,3292,3294],{},[113,3284,2599],{}," serializes the transactions on the locked rows; ",[113,3287,3146],{}," is an atomic update that doesn't depend on the ",[113,3290,807],{},"ed value. At SERIALIZABLE, the database would detect the conflict and abort one — but ",[113,3293,3146],{}," is correct even at READ COMMITTED without explicit locking.",[14,3296,3297,2664,3300,3303,3304,3307,3308,3310],{},[50,3298,3299],{},"The lesson",[113,3301,3302],{},"UPDATE ... SET col = col - X"," is an atomic read-modify-write (safe at any isolation level). ",[113,3305,3306],{},"UPDATE ... SET col = \u003Cvalue computed in app from a SELECT>"," is a lost-update race. Never read a value, compute in the application, and write it back without ",[113,3309,2599],{}," or an atomic expression.",[3312,3313,3314],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}",{"title":111,"searchDepth":126,"depth":126,"links":3316},[3317,3318,3321,3322,3328,3333,3334,3335,3336,3337,3338,3339,3340,3341,3342],{"id":20,"depth":126,"text":21},{"id":100,"depth":126,"text":101,"children":3319},[3320],{"id":304,"depth":140,"text":305},{"id":458,"depth":126,"text":459},{"id":665,"depth":126,"text":666,"children":3323},[3324,3325,3326,3327],{"id":669,"depth":140,"text":670},{"id":689,"depth":140,"text":690},{"id":707,"depth":140,"text":708},{"id":725,"depth":140,"text":726},{"id":748,"depth":126,"text":749,"children":3329},[3330,3331,3332],{"id":788,"depth":140,"text":789},{"id":814,"depth":140,"text":815},{"id":839,"depth":140,"text":526},{"id":864,"depth":126,"text":865},{"id":913,"depth":126,"text":914},{"id":1683,"depth":126,"text":1684},{"id":1851,"depth":126,"text":1852},{"id":2130,"depth":126,"text":2131},{"id":2239,"depth":126,"text":2240},{"id":2443,"depth":126,"text":2444},{"id":2580,"depth":126,"text":2581},{"id":2671,"depth":126,"text":2672},{"id":2834,"depth":126,"text":2835},"md",{},"\u002Fsql\u002F15-transactions-and-isolation",{"title":5,"description":16},"sql\u002F15-transactions-and-isolation","J0V97b_KOKD1S61CKcnuFlGmcDIHcJfDOyDvEvH8UkM",1789924654672]