[{"data":1,"prerenderedAt":186},["ShallowReactive",2],{"topic-sql-transactions":3},{"framework":4,"topic":16,"subtopics":25},{"id":5,"description":6,"extension":7,"icon":8,"meta":9,"name":10,"order":11,"slug":12,"stem":13,"tier":14,"__hash__":15},"frameworks\u002Fframeworks\u002Fsql.yml","SQL interview questions on queries, joins and aggregation — essential for every backend, data and analytics interview.","yml","database",{},"SQL",4,"sql","frameworks\u002Fsql",1,"lpzsOj2p9p9W0Tctwc61nP-ulZAA80R5gJiyaZS6ZeI",{"id":17,"description":18,"extension":7,"frameworkSlug":12,"meta":19,"name":20,"order":21,"slug":22,"stem":23,"__hash__":24},"topics\u002Ftopics\u002Fsql-transactions.yml","ACID, COMMIT\u002FROLLBACK\u002FSAVEPOINT, isolation levels and locking — keeping data correct when many things happen at once.",{},"Transactions",6,"transactions","topics\u002Fsql-transactions","pG-ozhwxxK2VCNSH9_EBgJnYZq0NZ5rreHakRMgLy-8",[26,111],{"id":27,"title":20,"body":28,"description":32,"difficulty":35,"extension":36,"framework":10,"frameworkSlug":12,"meta":37,"navigation":38,"order":14,"path":39,"questions":40,"questionsCount":103,"related":104,"seo":105,"seoDescription":106,"stem":107,"subtopic":108,"topic":20,"topicSlug":22,"updated":109,"__hash__":110},"qa\u002Fsql\u002Ftransactions\u002Ftransactions.md",{"type":29,"value":30,"toc":31},"minimark",[],{"title":32,"searchDepth":33,"depth":33,"links":34},"",2,[],"medium","md",{},true,"\u002Fsql\u002Ftransactions\u002Ftransactions",[41,46,50,54,58,62,66,70,74,78,82,87,91,95,99],{"id":42,"difficulty":43,"q":44,"a":45},"what-is-transaction","easy","What is a database transaction?","A **transaction** is a sequence of one or more SQL statements that the\ndatabase treats as a single unit of work — either **all succeed** or\n**none take effect**. Transactions ensure that partial failures never leave\nthe database in an inconsistent state.\n\n```sql\n-- Transfer $100 from account 1 to account 2 atomically\nBEGIN;\n  UPDATE accounts SET balance = balance - 100 WHERE id = 1;\n  UPDATE accounts SET balance = balance + 100 WHERE id = 2;\nCOMMIT;\n-- If either UPDATE fails, ROLLBACK is triggered and neither change persists.\n```\n\nWithout a transaction, a crash between the two UPDATEs would subtract $100\nfrom account 1 but never add it to account 2 — money disappears.\n\n**Rule of thumb:** wrap any sequence of writes that must succeed or fail\ntogether in a single transaction. A transaction that touches only one row\nis still valid — it gives you the crash-recovery guarantee.\n",{"id":47,"difficulty":43,"q":48,"a":49},"acid-properties","What are the ACID properties?","**ACID** is the set of guarantees a database must provide for transactions\nto be reliable:\n\n- **Atomicity** — the transaction is all-or-nothing. A failure at any point\n  rolls back every change made so far.\n- **Consistency** — a transaction can only bring the database from one valid\n  state to another. All constraints, triggers, and rules still hold after\n  the commit.\n- **Isolation** — concurrent transactions do not see each other's uncommitted\n  changes. The degree of isolation is configurable (see isolation levels).\n- **Durability** — once committed, the changes survive crashes. The database\n  writes them to persistent storage (WAL \u002F redo log) before acknowledging\n  the commit.\n\n```sql\n-- Atomicity: if the second UPDATE fails, the first is rolled back\nBEGIN;\n  UPDATE orders SET status = 'shipped' WHERE id = 42;\n  INSERT INTO shipments (order_id, shipped_at) VALUES (42, now()); -- fails?\nCOMMIT; -- only reaches here if both statements succeed\n```\n\n**Rule of thumb:** when someone asks \"how does your database prevent X?\" the\nanswer maps to one of the four ACID letters. Know which letter covers which\nclass of problem.\n",{"id":51,"difficulty":43,"q":52,"a":53},"begin-commit-rollback","What do BEGIN, COMMIT, and ROLLBACK do?","- **`BEGIN`** (or `START TRANSACTION`) opens a new transaction. All\n  subsequent statements are part of this transaction until it is ended.\n- **`COMMIT`** permanently saves all changes made in the transaction and\n  releases any locks held.\n- **`ROLLBACK`** discards all changes made since `BEGIN` and releases locks.\n  The database returns to the state it was in before the transaction started.\n\n```sql\nBEGIN;\n  INSERT INTO invoices (customer_id, total) VALUES (7, 299.99);\n  UPDATE accounts SET balance = balance - 299.99 WHERE customer_id = 7;\nCOMMIT;   -- both rows persist\n\n-- Error path\nBEGIN;\n  DELETE FROM orders WHERE id = 99;\nROLLBACK; -- deletion is undone\n```\n\nIn Postgres, a failed statement inside a transaction automatically puts the\ntransaction into an **error state** — further statements are rejected until\nyou issue `ROLLBACK` (or `ROLLBACK TO SAVEPOINT`).\n\n**Rule of thumb:** always pair every `BEGIN` with either a `COMMIT` or a\n`ROLLBACK`. An open transaction that is never closed holds locks and blocks\nother sessions.\n",{"id":55,"difficulty":43,"q":56,"a":57},"autocommit","What is autocommit and how does it affect transactions?","In **autocommit mode** (the default in most databases), every SQL statement\nthat is not inside an explicit `BEGIN` block is automatically wrapped in its\nown single-statement transaction and committed immediately.\n\n```sql\n-- Autocommit ON (default): each statement is its own transaction\nUPDATE users SET email = 'new@example.com' WHERE id = 1;\n-- ^ committed immediately, cannot be rolled back\n\n-- Explicit transaction: autocommit suspended until COMMIT\u002FROLLBACK\nBEGIN;\n  UPDATE users SET email = 'new@example.com' WHERE id = 1;\n  -- still uncommitted, can still ROLLBACK\nCOMMIT;\n```\n\n- **Postgres**: autocommit is on by default; `BEGIN` suspends it.\n- **MySQL**: autocommit is on by default; `SET autocommit = 0` or `BEGIN` turns it off.\n- **SQL Server**: autocommit is on by default; `BEGIN TRANSACTION` starts an explicit one.\n\n**Rule of thumb:** never rely on autocommit for multi-statement operations.\nAlways open an explicit transaction when two or more writes must be atomic.\n",{"id":59,"difficulty":35,"q":60,"a":61},"savepoint","What is a SAVEPOINT and when would you use one?","A **SAVEPOINT** marks a point within a transaction that you can roll back\nto without aborting the entire transaction. This lets you recover from a\npartial failure while keeping the work done before the savepoint.\n\n```sql\nBEGIN;\n  INSERT INTO orders (customer_id, total) VALUES (5, 100.00);\n\n  SAVEPOINT after_order;\n\n  INSERT INTO order_items (order_id, product_id, qty) VALUES (99, 1, 2);\n  -- ^ suppose this fails (e.g. FK violation)\n\n  ROLLBACK TO SAVEPOINT after_order;\n  -- the orders INSERT is still intact; only order_items is undone\n\n  -- Try a different fix or log the error, then commit what we have\nCOMMIT;\n```\n\nSavepoints are especially useful in ORMs and application frameworks that\nwrap nested operations in sub-transactions.\n\n**Rule of thumb:** use savepoints for \"nested transaction\" patterns — when\na library or service call may fail but you want to keep the outer transaction\nalive. Don't overuse them; a simpler design often avoids the need.\n",{"id":63,"difficulty":35,"q":64,"a":65},"release-savepoint","What does RELEASE SAVEPOINT do?","`RELEASE SAVEPOINT name` destroys the savepoint but **does not commit or\nroll back** the work done since it. The changes remain part of the enclosing\ntransaction and will be committed or rolled back with it.\n\n```sql\nBEGIN;\n  INSERT INTO audit_log (event) VALUES ('start');\n\n  SAVEPOINT sp1;\n  UPDATE config SET value = 'new' WHERE key = 'theme';\n  RELEASE SAVEPOINT sp1;   -- sp1 is gone; UPDATE is still pending\n\n  -- Cannot ROLLBACK TO SAVEPOINT sp1 anymore\nCOMMIT; -- both the INSERT and UPDATE persist\n```\n\nAfter `RELEASE`, you can no longer roll back to that savepoint name.\n`RELEASE` is useful to free memory when you are certain you will not need\nto roll back to a particular point.\n\n**Rule of thumb:** release savepoints once you are confident the work they\ncover is correct. This is a minor housekeeping step; most applications omit\nit since savepoints are released automatically on `COMMIT` or `ROLLBACK`.\n",{"id":67,"difficulty":35,"q":68,"a":69},"implicit-transaction","What is an implicit transaction in SQL Server?","SQL Server supports an **implicit transaction** mode (`SET IMPLICIT_TRANSACTIONS ON`)\nwhere the database automatically begins a transaction before each DML or DDL\nstatement without an explicit `BEGIN TRANSACTION`. The user must still\nissue `COMMIT` or `ROLLBACK` to end it.\n\n```sql\n-- SQL Server with implicit transactions enabled\nSET IMPLICIT_TRANSACTIONS ON;\n\nUPDATE products SET price = price * 1.1; -- transaction auto-started\n-- still uncommitted! Must explicitly end it:\nCOMMIT;\n```\n\nThis differs from autocommit (where each statement commits automatically)\nand from explicit transactions (where you write `BEGIN TRANSACTION`).\nImplicit transactions are an easy source of long-running uncommitted\ntransactions and should be used carefully.\n\n**Rule of thumb:** avoid `IMPLICIT_TRANSACTIONS ON` in SQL Server — it\nsurprises developers who expect autocommit behavior. Prefer explicit\n`BEGIN TRANSACTION … COMMIT` for clarity.\n",{"id":71,"difficulty":35,"q":72,"a":73},"transaction-log","What is the transaction log (WAL) and why does it matter?","The **transaction log** (called **WAL** — Write-Ahead Log — in Postgres)\nrecords every change before it is written to the main data files. On a\ncrash, the database replays the log to bring data back to a consistent\nstate (redo) and removes uncommitted changes (undo).\n\n```\nTimeline of a COMMIT:\n1. Changes are written to the WAL on disk  ← durability guaranteed here\n2. Database acknowledges COMMIT to the client\n3. Changes are eventually flushed from buffer pool to data files\n(crash between steps 2 and 3 is safe — WAL replay restores the data)\n```\n\nThe WAL also powers **replication** (streaming the log to replicas) and\n**point-in-time recovery** (replaying the log up to a specific timestamp).\n\n**Rule of thumb:** understand that `COMMIT` does NOT mean \"data is in the\ntable file\" — it means \"data is in the WAL and therefore durable.\" The\nactual table file update is asynchronous.\n",{"id":75,"difficulty":35,"q":76,"a":77},"long-running-transactions","Why are long-running transactions harmful?","A long-running transaction holds **row\u002Fpage locks** that block other writers,\naccumulates **undo\u002Frollback data** that inflates the database's version store\nor transaction log, and in Postgres prevents **VACUUM** from reclaiming dead\nrow versions (causing table bloat).\n\n```sql\n-- Postgres: find long-running transactions\nSELECT pid,\n       now() - pg_stat_activity.xact_start AS duration,\n       query,\n       state\nFROM   pg_stat_activity\nWHERE  xact_start IS NOT NULL\n  AND  now() - xact_start > INTERVAL '5 minutes'\nORDER  BY duration DESC;\n\n-- Terminate if necessary\nSELECT pg_terminate_backend(pid) FROM pg_stat_activity\nWHERE  now() - xact_start > INTERVAL '1 hour';\n```\n\n**Rule of thumb:** keep transactions as short as possible. Do not hold an\nopen transaction while waiting for user input, making HTTP calls, or doing\nslow computation. Open the transaction, write, commit — then do anything\nslow.\n",{"id":79,"difficulty":35,"q":80,"a":81},"deadlock","What is a deadlock and how does the database resolve it?","A **deadlock** occurs when two (or more) transactions each hold a lock that\nthe other needs, so neither can proceed.\n\n```\nSession A: locks row 1, waits for row 2\nSession B: locks row 2, waits for row 1\n→ circular wait → deadlock\n```\n\nDatabases automatically detect deadlocks via a cycle-detection algorithm\nand resolve them by choosing a **victim** (typically the transaction with\nthe least work done) and rolling it back with an error.\n\n```sql\n-- Avoid deadlocks by always locking rows in the same order\n-- BAD: Session A locks user 1 then order 5; Session B locks order 5 then user 1\n-- GOOD: both sessions always lock by (user_id, order_id) order\n\n-- Postgres: SELECT FOR UPDATE to acquire locks explicitly in order\nSELECT * FROM users WHERE id = 1 FOR UPDATE;\nSELECT * FROM orders WHERE id = 5 FOR UPDATE;\n```\n\n**Rule of thumb:** prevent deadlocks by always acquiring locks in a\n**consistent order** across all transactions. Keep transactions short.\nHandle deadlock errors in application code with a retry loop.\n",{"id":83,"difficulty":84,"q":85,"a":86},"optimistic-vs-pessimistic","hard","What is the difference between optimistic and pessimistic locking?","**Pessimistic locking** acquires a lock *before* reading the data and holds\nit until the transaction commits — preventing any other writer from touching\nthe row during that window.\n\n**Optimistic locking** reads the data without a lock, does work, then\nchecks at write time whether another writer has changed the data since it\nwas read. If yes, it retries rather than committing stale data.\n\n```sql\n-- Pessimistic: lock the row immediately on read\nBEGIN;\n  SELECT * FROM inventory WHERE product_id = 42 FOR UPDATE;\n  -- no other transaction can UPDATE this row until we COMMIT\n  UPDATE inventory SET stock = stock - 1 WHERE product_id = 42;\nCOMMIT;\n\n-- Optimistic: use a version column to detect conflicts\n-- Read phase (no lock):\nSELECT stock, version FROM inventory WHERE product_id = 42;\n-- → stock=10, version=7\n\n-- Write phase: only update if version hasn't changed\nUPDATE inventory\nSET    stock = 9, version = 8\nWHERE  product_id = 42 AND version = 7;\n-- If 0 rows affected → conflict detected → retry\n```\n\n**Rule of thumb:** use pessimistic locking for high-contention resources\n(inventory, seat reservations) where conflicts are frequent. Use optimistic\nlocking for low-contention resources where conflicts are rare — it scales\nbetter by avoiding lock waits.\n",{"id":88,"difficulty":35,"q":89,"a":90},"select-for-update","What does SELECT FOR UPDATE do?","`SELECT FOR UPDATE` reads rows and immediately acquires an **exclusive row\nlock** on each row returned, preventing other transactions from updating or\nlocking those rows until the current transaction commits or rolls back.\n\n```sql\nBEGIN;\n  -- Lock the row so no other session can change it before we write\n  SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;\n  -- → balance = 500\n\n  UPDATE accounts SET balance = balance - 100 WHERE id = 1;\nCOMMIT;\n```\n\nVariants:\n- `FOR SHARE` — shared lock; others can read but not write.\n- `FOR UPDATE SKIP LOCKED` (Postgres, MySQL 8+) — skip rows already locked;\n  useful for job queues where workers should not compete for the same row.\n- `FOR UPDATE NOWAIT` — fail immediately if the row is already locked.\n\n**Rule of thumb:** use `SELECT FOR UPDATE` when you read a value and\nimmediately use it to compute an update — it closes the time-of-check \u002F\ntime-of-use race condition that would exist if the read and write were\nunprotected.\n",{"id":92,"difficulty":84,"q":93,"a":94},"skip-locked","How do you implement a concurrent job queue with SKIP LOCKED?","`SKIP LOCKED` lets multiple workers pull jobs from a queue table without\ncompeting for the same row — each worker skips rows already locked by\nanother worker.\n\n```sql\n-- Schema\nCREATE TABLE job_queue (\n  id         BIGSERIAL PRIMARY KEY,\n  payload    JSONB NOT NULL,\n  status     TEXT NOT NULL DEFAULT 'pending',\n  created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\n-- Worker: claim one job atomically\nBEGIN;\n  SELECT id, payload\n  FROM   job_queue\n  WHERE  status = 'pending'\n  ORDER  BY created_at\n  LIMIT  1\n  FOR UPDATE SKIP LOCKED;   -- skip rows locked by other workers\n\n  UPDATE job_queue SET status = 'processing' WHERE id = \u003Cclaimed_id>;\nCOMMIT;\n```\n\nEach worker gets a different row. If a worker crashes, the transaction\nrolls back, returning the row to `pending` for another worker to claim.\n\n**Rule of thumb:** `SELECT … FOR UPDATE SKIP LOCKED` is the correct pattern\nfor building a reliable job queue in SQL. It is atomic, crash-safe, and\nscales to many concurrent workers without external coordination.\n",{"id":96,"difficulty":84,"q":97,"a":98},"two-phase-commit","What is two-phase commit (2PC) and when is it used?","**Two-phase commit** is a distributed coordination protocol that ensures\na transaction spanning **multiple independent databases** either commits\non all of them or rolls back on all.\n\n- **Phase 1 (Prepare)**: the coordinator asks each participant to prepare\n  (write to their WAL, acquire locks, but do not commit). Each replies\n  \"yes\" or \"no\".\n- **Phase 2 (Commit\u002FAbort)**: if all said \"yes\", the coordinator tells all\n  to commit. If any said \"no\", the coordinator tells all to abort.\n\n```sql\n-- Postgres prepared transactions (the participant side of 2PC)\nBEGIN;\n  UPDATE accounts SET balance = balance - 100 WHERE id = 1;\nPREPARE TRANSACTION 'txn-xyz-001'; -- phase 1: prepared, not committed\n\n-- Later, coordinator decides to commit or rollback:\nCOMMIT PREPARED 'txn-xyz-001';\n-- or\nROLLBACK PREPARED 'txn-xyz-001';\n```\n\n**Rule of thumb:** 2PC solves cross-database atomicity but adds latency\nand coordinator failure risk. In modern systems, the **Saga pattern**\n(compensating transactions) is often preferred over 2PC for microservice\narchitectures.\n",{"id":100,"difficulty":35,"q":101,"a":102},"transaction-best-practices","What are the most important best practices for writing transactions?","1. **Keep transactions short** — open, write, commit. Do not hold a\n   transaction open while making network calls, waiting for user input, or\n   running slow computations.\n2. **Access tables in a consistent order** — prevents deadlocks across\n   concurrent transactions that touch the same set of tables.\n3. **Handle errors explicitly** — always `ROLLBACK` on any exception; never\n   swallow errors and then commit.\n4. **Do not use transactions for reads alone** — unless you need a consistent\n   snapshot across multiple queries, a plain `SELECT` outside a transaction\n   is cheaper.\n5. **Retry on transient failures** — deadlocks and serialization failures\n   are expected; build retry logic with exponential back-off.\n\n```sql\n-- Pattern: wrap in try\u002Fcatch, always rollback on error (Python psycopg2)\ntry:\n    cur.execute(\"BEGIN\")\n    cur.execute(\"UPDATE ...\")\n    cur.execute(\"INSERT ...\")\n    cur.execute(\"COMMIT\")\nexcept Exception:\n    cur.execute(\"ROLLBACK\")\n    raise\n```\n\n**Rule of thumb:** a transaction should be as wide as necessary (all writes\nthat must be atomic) and no wider. Every extra statement inside a transaction\nis a longer lock hold and a bigger rollback payload.\n",15,null,{"description":32},"SQL transactions interview questions — ACID properties, BEGIN\u002FCOMMIT\u002FROLLBACK, SAVEPOINT, autocommit, implicit vs explicit transactions, and best practices across Postgres, MySQL, and SQL Server.","sql\u002Ftransactions\u002Ftransactions","Transactions & ACID","2026-06-20","OBbo4RNRAA_7U1YkzIh4CFLgUM6Qp7vn03xJXnQssNU",{"id":112,"title":113,"body":114,"description":32,"difficulty":84,"extension":36,"framework":10,"frameworkSlug":12,"meta":118,"navigation":38,"order":33,"path":119,"questions":120,"questionsCount":103,"related":104,"seo":181,"seoDescription":182,"stem":183,"subtopic":184,"topic":20,"topicSlug":22,"updated":109,"__hash__":185},"qa\u002Fsql\u002Ftransactions\u002Fisolation-concurrency.md","Isolation Concurrency",{"type":29,"value":115,"toc":116},[],{"title":32,"searchDepth":33,"depth":33,"links":117},[],{},"\u002Fsql\u002Ftransactions\u002Fisolation-concurrency",[121,125,129,133,137,141,145,149,153,157,161,165,169,173,177],{"id":122,"difficulty":43,"q":123,"a":124},"what-is-isolation","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":126,"difficulty":35,"q":127,"a":128},"read-phenomena","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":130,"difficulty":43,"q":131,"a":132},"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":134,"difficulty":43,"q":135,"a":136},"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":138,"difficulty":35,"q":139,"a":140},"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":142,"difficulty":84,"q":143,"a":144},"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":146,"difficulty":84,"q":147,"a":148},"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":150,"difficulty":84,"q":151,"a":152},"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":154,"difficulty":35,"q":155,"a":156},"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":158,"difficulty":35,"q":159,"a":160},"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":162,"difficulty":84,"q":163,"a":164},"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":166,"difficulty":43,"q":167,"a":168},"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":170,"difficulty":84,"q":171,"a":172},"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":174,"difficulty":84,"q":175,"a":176},"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":178,"difficulty":84,"q":179,"a":180},"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",{"description":32},"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","egB7GrHYIzUcvVVoWUXEw7Qtp0A6GWgHZzO7X2jjX2Q",1782244098006]