[{"data":1,"prerenderedAt":103},["ShallowReactive",2],{"qa-\u002Fsql\u002Ftransactions\u002Fisolation-concurrency":3},{"page":4,"siblings":94,"blog":100},{"id":5,"title":6,"body":7,"description":11,"difficulty":14,"extension":15,"framework":16,"frameworkSlug":17,"meta":18,"navigation":19,"order":12,"path":20,"questions":21,"questionsCount":84,"related":85,"seo":86,"seoDescription":87,"stem":88,"subtopic":89,"topic":90,"topicSlug":91,"updated":92,"__hash__":93},"qa\u002Fsql\u002Ftransactions\u002Fisolation-concurrency.md","Isolation Concurrency",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"hard","md","SQL","sql",{},true,"\u002Fsql\u002Ftransactions\u002Fisolation-concurrency",[22,27,32,36,40,44,48,52,56,60,64,68,72,76,80],{"id":23,"difficulty":24,"q":25,"a":26},"what-is-isolation","easy","What is transaction isolation?","**Transaction isolation** controls how and when the changes made by one\ntransaction become visible to other concurrent transactions. Higher isolation\nprevents more anomalies but increases contention; lower isolation is faster\nbut allows more concurrency bugs.\n\nSQL defines four standard isolation levels ranked from weakest to strongest:\n`READ UNCOMMITTED` → `READ COMMITTED` → `REPEATABLE READ` → `SERIALIZABLE`.\n\n```sql\n-- Set isolation level for the current transaction (Postgres)\nBEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n\n-- Or set a session default (MySQL)\nSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;\n```\n\n**Rule of thumb:** most applications are fine with `READ COMMITTED` (the\nPostgres default). Upgrade to `REPEATABLE READ` or `SERIALIZABLE` only\nwhen your application logic requires a consistent snapshot across multiple\nreads in the same transaction.\n",{"id":28,"difficulty":29,"q":30,"a":31},"read-phenomena","medium","What are the three read phenomena isolation levels protect against?","The SQL standard defines three anomalies that can occur when transactions\nrun concurrently:\n\n1. **Dirty read** — reading uncommitted changes from another transaction.\n   If that transaction rolls back, you read data that never existed.\n2. **Non-repeatable read** — reading the same row twice in the same\n   transaction and getting different values because another transaction\n   committed an update between the two reads.\n3. **Phantom read** — running the same range query twice and getting\n   different *sets of rows* because another transaction inserted or deleted\n   rows between the two reads.\n\n```\n-- Dirty read example (requires READ UNCOMMITTED)\nTx A: UPDATE products SET price = 999 WHERE id = 1  (not committed)\nTx B: SELECT price FROM products WHERE id = 1  → 999  (dirty!)\nTx A: ROLLBACK\n-- Tx B acted on a price of 999 that never permanently existed.\n```\n\n**Rule of thumb:** map each anomaly to the isolation level that prevents\nit: `READ COMMITTED` prevents dirty reads; `REPEATABLE READ` also prevents\nnon-repeatable reads; `SERIALIZABLE` also prevents phantoms.\n",{"id":33,"difficulty":24,"q":34,"a":35},"read-uncommitted","What is READ UNCOMMITTED and when (if ever) is it useful?","**`READ UNCOMMITTED`** is the lowest isolation level. Transactions can read\n**uncommitted (\"dirty\") changes** from other transactions. This means you can\nread data that another transaction later rolls back — data that was never\npermanently committed.\n\n```sql\n-- SQL Server: allow dirty reads\nSET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;\nSELECT * FROM orders WHERE status = 'pending';\n-- May return rows that are being modified by another transaction\n-- and might disappear if that transaction rolls back.\n```\n\nIt is rarely appropriate. One accepted use case: very approximate counts or\nestimates on a large table where absolute accuracy is not required and locking\noverhead matters more than precision.\n\nIn **Postgres**, `READ UNCOMMITTED` is mapped to `READ COMMITTED` internally —\nPostgres's MVCC architecture never allows dirty reads regardless of the\nisolation level set.\n\n**Rule of thumb:** never use `READ UNCOMMITTED` for any business-logic query.\nIf you need a rough count on a large table, use `pg_class.reltuples` in\nPostgres instead.\n",{"id":37,"difficulty":24,"q":38,"a":39},"read-committed","What does READ COMMITTED guarantee and what can still go wrong?","**`READ COMMITTED`** is the default in Postgres and Oracle. Each statement\nwithin a transaction sees only rows that were committed *before that\nstatement started*. This prevents dirty reads.\n\nWhat can still go wrong:\n- **Non-repeatable reads** — two `SELECT`s in the same transaction can\n  return different values for the same row if another transaction commits\n  between them.\n- **Phantom reads** — a range query can return different row counts if\n  another transaction inserts\u002Fdeletes rows between queries.\n\n```sql\n-- Non-repeatable read under READ COMMITTED:\n-- Tx A (READ COMMITTED):\nBEGIN;\n  SELECT balance FROM accounts WHERE id = 1;  -- → 500\n  -- Tx B commits: UPDATE accounts SET balance = 0 WHERE id = 1;\n  SELECT balance FROM accounts WHERE id = 1;  -- → 0 (different!)\nCOMMIT;\n```\n\n**Rule of thumb:** `READ COMMITTED` is correct for most OLTP workloads\nwhere each query is self-contained. If your transaction reads a value and\nthen uses it in a later write, consider `REPEATABLE READ` to prevent the\nvalue from changing between the read and the write.\n",{"id":41,"difficulty":29,"q":42,"a":43},"repeatable-read","What does REPEATABLE READ guarantee?","**`REPEATABLE READ`** ensures that if a transaction reads a row, it will\nsee the same values for that row on every subsequent read within the same\ntransaction — even if another transaction commits updates to that row in\nbetween. This prevents both dirty reads and non-repeatable reads.\n\nIn **Postgres**, `REPEATABLE READ` uses a **snapshot** taken at the start\nof the transaction, so the transaction sees a consistent view of all data\nas it was when it began. Postgres also prevents phantom reads under this\nlevel (stronger than the SQL standard requires).\n\nIn **MySQL (InnoDB)**, `REPEATABLE READ` is the default and uses a\nconsistent read snapshot for `SELECT`s, but phantom rows can still appear\nin locking reads (`SELECT FOR UPDATE`).\n\n```sql\nBEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n  SELECT balance FROM accounts WHERE id = 1;  -- → 500\n  -- Another transaction commits: UPDATE accounts SET balance = 0 WHERE id = 1\n  SELECT balance FROM accounts WHERE id = 1;  -- → still 500 (snapshot)\nCOMMIT;\n```\n\n**Rule of thumb:** use `REPEATABLE READ` when a transaction reads the same\ndata multiple times and the business logic requires it to be consistent\nacross those reads (e.g., computing a report in multiple steps).\n",{"id":45,"difficulty":14,"q":46,"a":47},"serializable","What does SERIALIZABLE isolation guarantee?","**`SERIALIZABLE`** is the strongest isolation level. It guarantees that the\nresult of concurrent transactions is equivalent to running them one after\nanother in some serial order — as if there were no concurrency at all.\n\nPostgres implements this via **Serializable Snapshot Isolation (SSI)**, which\ntracks read\u002Fwrite dependencies and aborts transactions that would create a\ncycle (a non-serializable schedule). It avoids broad locking but can abort\ntransactions that need to be retried.\n\n```sql\n-- Classic serialization anomaly (write skew) — prevented by SERIALIZABLE:\n-- Two doctors both check \"at least one doctor is on call\" and both\n-- decide to go off call — ending with zero doctors on call.\n\n-- Under SERIALIZABLE, one of the two transactions is aborted and must retry.\nBEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;\n  SELECT COUNT(*) FROM doctors WHERE on_call = TRUE;  -- → 2\n  UPDATE doctors SET on_call = FALSE WHERE id = 42;\nCOMMIT; -- may fail with serialization failure → retry\n```\n\n**Rule of thumb:** use `SERIALIZABLE` for financial ledgers, inventory\nmanagement, or any domain where write skew would produce incorrect results.\nBuild retry logic for `SQLSTATE 40001` (serialization failure) into the\napplication.\n",{"id":49,"difficulty":14,"q":50,"a":51},"mvcc","What is MVCC and how does it enable concurrency without locking reads?","**MVCC** (Multi-Version Concurrency Control) allows readers and writers to\noperate concurrently without blocking each other by keeping **multiple\nversions of each row** (old and new) in the storage engine.\n\n- A **reader** sees the version of each row that was committed before its\n  transaction (or query) started — it never waits for a writer.\n- A **writer** creates a new row version alongside the old one. Other\n  readers still see the old version until the new one is committed.\n\n```\n-- Timeline (Postgres MVCC):\nt=1: INSERT INTO t VALUES (1, 'old')  -- row version v1\nt=2: Tx A begins (snapshot = v1)\nt=3: Tx B: UPDATE t SET val='new' WHERE id=1  -- creates v2\nt=4: Tx B: COMMIT\nt=5: Tx A: SELECT * FROM t  -- still sees v1 (its snapshot)\nt=6: Tx A: COMMIT\nt=7: VACUUM reclaims v1 (no transaction needs it anymore)\n```\n\nThe downside of MVCC is **dead row accumulation** — old versions must be\ncleaned up by `VACUUM` in Postgres. A long-running transaction prevents\nVACUUM from reclaiming any versions created after its snapshot.\n\n**Rule of thumb:** understand that Postgres reads never block writes and\nwrites never block reads — this is MVCC in action. Long-running transactions\nare the enemy of MVCC health because they pin old row versions in storage.\n",{"id":53,"difficulty":14,"q":54,"a":55},"write-skew","What is write skew and how does SERIALIZABLE prevent it?","**Write skew** is a concurrency anomaly where two transactions each read an\noverlapping set of rows, make a decision based on what they read, and then\neach write to a *different* row — producing a state that neither transaction\nwould have allowed if it had run alone.\n\n```sql\n-- Invariant: at least one doctor must be on call at all times.\n-- Both Tx A and Tx B read: 2 doctors on call → each decides to go off call.\n\n-- Tx A:\nBEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n  SELECT COUNT(*) FROM on_call WHERE shift_id = 1;  -- → 2, safe to go off\n  UPDATE on_call SET doctor_id = NULL WHERE doctor_id = 101 AND shift_id = 1;\nCOMMIT;\n\n-- Tx B (concurrent):\nBEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n  SELECT COUNT(*) FROM on_call WHERE shift_id = 1;  -- → 2, safe to go off\n  UPDATE on_call SET doctor_id = NULL WHERE doctor_id = 202 AND shift_id = 1;\nCOMMIT;\n\n-- Result: 0 doctors on call — invariant violated.\n-- Under SERIALIZABLE, one transaction is aborted and must retry.\n```\n\n`REPEATABLE READ` does NOT prevent write skew because each transaction\nwrites to a *different* row. Only `SERIALIZABLE` (SSI) detects the\nrw-dependency cycle and prevents it.\n\n**Rule of thumb:** write skew is subtle and hard to spot in code reviews.\nAudit any transaction that reads a set of rows and then writes based on an\naggregate of that set — it is a write-skew candidate.\n",{"id":57,"difficulty":29,"q":58,"a":59},"lost-update","What is a lost update and how do you prevent it?","A **lost update** occurs when two transactions both read a value, compute a\nnew value based on it, and then both write back — the second write overwrites\nthe first writer's change, effectively losing it.\n\n```sql\n-- Both sessions read stock = 10\n-- Session A: stock = 10 - 1 = 9  → UPDATE inventory SET stock = 9 ...\n-- Session B: stock = 10 - 1 = 9  → UPDATE inventory SET stock = 9 ...\n-- Result: stock = 9 instead of 8. One sale is lost.\n\n-- Fix 1: atomic UPDATE (no read-then-write race)\nUPDATE inventory SET stock = stock - 1 WHERE product_id = 42 AND stock > 0;\n\n-- Fix 2: SELECT FOR UPDATE (pessimistic lock)\nBEGIN;\n  SELECT stock FROM inventory WHERE product_id = 42 FOR UPDATE;\n  UPDATE inventory SET stock = stock - 1 WHERE product_id = 42;\nCOMMIT;\n\n-- Fix 3: optimistic locking with a version column\nUPDATE inventory SET stock = stock - 1, version = version + 1\nWHERE product_id = 42 AND version = 7;\n-- 0 rows affected → conflict → retry\n```\n\n**Rule of thumb:** the safest fix is an atomic `UPDATE col = col - delta`\n(the database computes the new value from the current one, no race window).\nUse `SELECT FOR UPDATE` when the read-compute-write logic is too complex to\nexpress in a single `UPDATE`.\n",{"id":61,"difficulty":29,"q":62,"a":63},"phantom-read","What is a phantom read and what isolation level prevents it?","A **phantom read** occurs when a transaction executes the same range query\ntwice and gets different rows because another transaction inserted or deleted\nqualifying rows between the two reads.\n\n```sql\n-- Tx A (REPEATABLE READ in standard SQL, not Postgres):\nBEGIN;\n  SELECT COUNT(*) FROM bookings WHERE room_id = 5 AND date = '2026-07-01';\n  -- → 0 (room is free)\n  -- Tx B inserts a booking for room 5, date 2026-07-01 and commits\n  SELECT COUNT(*) FROM bookings WHERE room_id = 5 AND date = '2026-07-01';\n  -- → 1 (phantom row appeared!)\nCOMMIT;\n```\n\n- `READ COMMITTED`: phantoms possible.\n- `REPEATABLE READ` (standard SQL): phantoms still possible for inserts;\n  prevented for updates on existing rows. Postgres's MVCC snapshot prevents\n  phantoms completely at this level.\n- `SERIALIZABLE`: prevents all phantoms.\n\n**Rule of thumb:** if your application logic checks \"does row X exist before\ninserting it,\" use `SERIALIZABLE` or an explicit lock (`SELECT FOR UPDATE \u002F\nFOR SHARE`) to prevent phantom inserts from racing with your check.\n",{"id":65,"difficulty":14,"q":66,"a":67},"gap-locks","What are gap locks and next-key locks in MySQL InnoDB?","**Gap locks** and **next-key locks** are MySQL InnoDB mechanisms that prevent\nphantom reads under `REPEATABLE READ` by locking *ranges* of index space,\nnot just existing rows.\n\n- **Gap lock**: locks the gap between two index values — prevents inserts\n  into that range by other transactions.\n- **Next-key lock**: a gap lock plus the index record at the upper boundary.\n  InnoDB uses next-key locks by default under `REPEATABLE READ`.\n\n```sql\n-- Under REPEATABLE READ in MySQL, this SELECT FOR UPDATE\n-- locks not just the rows where age BETWEEN 20 AND 30,\n-- but also the gaps so no new rows in that range can be inserted.\nSELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;\n```\n\nGap locks can cause unexpected lock contention: inserting a value in a\nrange scanned by another transaction will block even if the inserted row\ndoes not match the other transaction's WHERE clause exactly.\n\n**Rule of thumb:** if you see unexpected INSERT waits in MySQL, check\nwhether a concurrent transaction's range scan holds a gap lock covering\nyour insert position. Upgrading to `READ COMMITTED` disables gap locks\nif you don't need phantom prevention.\n",{"id":69,"difficulty":24,"q":70,"a":71},"default-isolation-levels","What is the default isolation level in Postgres, MySQL, and SQL Server?","| Database | Default isolation level | MVCC? |\n|---|---|---|\n| **Postgres** | `READ COMMITTED` | Yes — reads never block writes |\n| **MySQL InnoDB** | `REPEATABLE READ` | Yes — consistent read snapshots |\n| **SQL Server** | `READ COMMITTED` | Optional (`READ_COMMITTED_SNAPSHOT`) |\n| **Oracle** | `READ COMMITTED` | Yes |\n\n```sql\n-- Check current isolation level\n-- Postgres:\nSHOW transaction_isolation;\n\n-- MySQL:\nSELECT @@transaction_isolation;\n\n-- SQL Server:\nSELECT transaction_isolation_level\nFROM   sys.dm_exec_sessions\nWHERE  session_id = @@SPID;\n```\n\nSQL Server has `READ_COMMITTED_SNAPSHOT` (RCSI) — an opt-in mode that\ngives `READ COMMITTED` MVCC-style behaviour (readers do not block writers),\nsimilar to Postgres's default.\n\n**Rule of thumb:** know your database's default before assuming behaviour.\nCode written for Postgres (`READ COMMITTED`) may behave differently when\nported to MySQL (`REPEATABLE READ`) and vice versa.\n",{"id":73,"difficulty":14,"q":74,"a":75},"serialization-failure-retry","How should application code handle a serialization failure?","When the database aborts a `SERIALIZABLE` (or `REPEATABLE READ` in MySQL)\ntransaction due to a conflict, it raises **SQLSTATE `40001`** (serialization\nfailure). The correct response is to **retry the entire transaction** from\nthe beginning — not just the failed statement.\n\n```python\n# Python + psycopg2 example\nimport psycopg2\nfrom psycopg2 import errors\nimport time\n\nMAX_RETRIES = 5\n\ndef transfer(conn, from_id, to_id, amount):\n    for attempt in range(MAX_RETRIES):\n        try:\n            with conn.cursor() as cur:\n                conn.autocommit = False\n                cur.execute(\"SET TRANSACTION ISOLATION LEVEL SERIALIZABLE\")\n                cur.execute(\"UPDATE accounts SET balance = balance - %s WHERE id = %s\",\n                            (amount, from_id))\n                cur.execute(\"UPDATE accounts SET balance = balance + %s WHERE id = %s\",\n                            (amount, to_id))\n                conn.commit()\n                return  # success\n        except errors.SerializationFailure:\n            conn.rollback()\n            time.sleep(0.05 * (2 ** attempt))  # exponential back-off\n    raise RuntimeError(\"Transaction failed after max retries\")\n```\n\n**Rule of thumb:** serialization failures are expected and normal under\n`SERIALIZABLE` — design every transaction that uses this level with a retry\nloop and exponential back-off. Never surface the raw database error to the\nend user.\n",{"id":77,"difficulty":14,"q":78,"a":79},"snapshot-isolation","What is snapshot isolation and how does it differ from SERIALIZABLE?","**Snapshot isolation (SI)** gives each transaction a consistent snapshot of\nthe database as it was at transaction start. Reads always see committed data\nfrom that snapshot, and writes conflict only if two transactions write to the\n*same row* (first-committer-wins). This prevents dirty reads, non-repeatable\nreads, and most phantom reads.\n\nThe key difference from `SERIALIZABLE`: SI still allows **write skew** —\ntwo transactions can each read a set of rows and write to different rows\nbased on that read, producing a state that neither transaction would have\npermitted alone (see write-skew question).\n\n```\nIsolation guarantees comparison:\n┌─────────────────────┬──────┬──────┬─────────┬──────────────┐\n│                     │ R.U. │ R.C. │ Rep.Rd. │ Serializable │\n├─────────────────────┼──────┼──────┼─────────┼──────────────┤\n│ Dirty read          │  ✗   │  ✓   │   ✓     │      ✓       │\n│ Non-repeatable read │  ✗   │  ✗   │   ✓     │      ✓       │\n│ Phantom read        │  ✗   │  ✗   │  ✓*     │      ✓       │\n│ Write skew          │  ✗   │  ✗   │   ✗     │      ✓       │\n└─────────────────────┴──────┴──────┴─────────┴──────────────┘\n(* Postgres REPEATABLE READ prevents phantoms via MVCC snapshot)\n```\n\n**Rule of thumb:** \"snapshot isolation\" is what most databases actually\nimplement when you ask for `REPEATABLE READ`. It is safe for the vast\nmajority of workloads. Upgrade to `SERIALIZABLE` only when write skew is\na real risk in your domain.\n",{"id":81,"difficulty":14,"q":82,"a":83},"lock-modes","What lock modes do databases use and how do they interact?","Databases use a hierarchy of locks with compatibility rules that determine\nwhich locks can be held simultaneously by different transactions.\n\nCommon lock modes (Postgres naming):\n\n| Mode | Abbr | Conflicts with |\n|---|---|---|\n| Access Share | AS | Access Exclusive only |\n| Row Share | RS | Exclusive, Access Exclusive |\n| Row Exclusive | RX | Share, Share Row Exclusive, Exclusive, Access Exclusive |\n| Share | S | Row Exclusive, Share Row Exclusive, Exclusive, Access Exclusive |\n| Exclusive | X | Everything except Access Share |\n| Access Exclusive | AX | Everything |\n\n```sql\n-- SELECT acquires Access Share (compatible with almost everything)\nSELECT * FROM orders;\n\n-- INSERT\u002FUPDATE\u002FDELETE acquire Row Exclusive\nUPDATE orders SET status = 'shipped' WHERE id = 1;\n\n-- ALTER TABLE requires Access Exclusive — blocks ALL other operations\nALTER TABLE orders ADD COLUMN notes TEXT;\n\n-- Check current locks\nSELECT relation::regclass, mode, granted\nFROM   pg_locks\nWHERE  relation IS NOT NULL;\n```\n\n**Rule of thumb:** `ALTER TABLE` takes an `Access Exclusive` lock and blocks\nevery read and write on the table for its duration. On large tables, use\n`CREATE INDEX CONCURRENTLY` and multi-step migrations to minimise lock time.\n",15,null,{"description":11},"SQL isolation levels interview questions — READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE, dirty reads, phantom reads, lost updates, MVCC, and locking behaviour across Postgres, MySQL, and SQL Server.","sql\u002Ftransactions\u002Fisolation-concurrency","Isolation Levels & Concurrency","Transactions","transactions","2026-06-20","egB7GrHYIzUcvVVoWUXEw7Qtp0A6GWgHZzO7X2jjX2Q",[95,99],{"subtopic":96,"path":97,"order":98},"Transactions & ACID","\u002Fsql\u002Ftransactions\u002Ftransactions",1,{"subtopic":89,"path":20,"order":12},{"path":101,"title":102},"\u002Fblog\u002Fsql-isolation-levels-concurrency","SQL Isolation Levels — Dirty Reads, Phantom Reads, and MVCC",1782244107313]