Transactions & Concurrency Control
How a relational engine keeps data correct when many transactions run at once: the ACID guarantees, the anomalies that appear when isolation is relaxed, and the MVCC and locking machinery underneath. Interviewers lean on this area hard because it separates people who type BEGIN/COMMIT from people who can predict what two concurrent sessions will actually see and do to each other.
part ofRelational database conceptsoverview, primer and where to startread it →on this pageshowhide
explore
- ACID Guarantees20 questions
- Atomicity5 questions
- Consistency5 questions
- Isolation5 questions
- Durability5 questions
- Concurrency Anomalies22 questions
- Dirty Read3 questions
- Non-Repeatable Read5 questions
- Phantom Read4 questions
- Lost Update6 questions
- Write Skew4 questions
- Isolation Levels21 questions
- READ UNCOMMITTED4 questions
- READ COMMITTED5 questions
- REPEATABLE READ4 questions
- SERIALIZABLE4 questions
- Snapshot Isolation vs True Serializability4 questions
- MVCC Mechanics16 questions
- Row Versions & Snapshot Visibility5 questions
- Why Readers Don't Block Writers5 questions
- Version Cleanup & Bloat6 questions
- Locking & Two-Phase Locking16 questions
- Two-Phase Locking (2PL)6 questions
- Lock Types & Granularity6 questions
- Deadlock Detection & Avoidance4 questions
- Transactions in Practice13 questions
- Optimistic vs Pessimistic Concurrency4 questions
- Savepoints & Transaction Scope4 questions
- The Cost of Long-Running Transactions5 questions
- PostgreSQL DBAroleanchors this topic
- SQLskillanchors this topic
- AI & Data Scientistrole
- AI Engineerrole
- Backend Developerrole
- Computer Scienceskill
- Cyber Security Expertrole
- Data Analystrole
- Data Engineerrole
- Forward Deployed Engineerrole
- Full Stack Developerrole
- Java Backend Developerrole
- Kotlin Backend Developerrole
- MLOps Engineerrole
- Machine Learning Engineerrole
- Server-Side Game Developerrole
- Software Architectrole
questions
108 · 6 sectionsAtomicity is one of the four ACID properties of a database transaction. What does it guarantee, and what happens to the work a transaction has already done if it fails halfway through?
basics
~20 sAtomicity means a transaction is all-or-nothing: either every change it made becomes visible together, or none does. If it fails partway, the engine reverses the work already done, leaving the database as if the transaction never ran.
In the ACID acronym for database transactions, what does the C (consistency) actually guarantee, and who decides what 'consistent' means?
basics
~20 sC means a transaction moves the database from one valid state to another: every declared integrity rule (primary key, unique, not null, check, foreign key) holds at commit. The engine enforces the declared rules; the developer decides which rules to declare.
When a database returns success for a COMMIT, what exactly has it promised the client, and what has it not promised?
basics
~20 sIt promises the committed changes survive a crash or power loss of that node: the data reached stable storage, not just memory. It does not promise the data survives losing that machine or its disk, nor that any replica already has it.
What does the isolation property of ACID promise about transactions that run at the same time, and why do real database engines let you weaken that promise?
basics
~20 sIsolation means concurrent transactions must not see each other's unfinished work: the outcome should match some order where they ran one at a time. Enforcing that fully costs locking, blocking and aborts, so engines offer weaker levels that trade specific anomalies for throughput.
When a transaction is rolled back, changes it already made to rows and indexes have to disappear. Mechanically, how does a relational engine achieve that?
basics
~20 sThe engine durably records how to reverse each change before making it — before-images or inverse operations — and rollback replays those backwards. Multi-version engines instead leave the old row version in place and simply never make the new one visible.
What is a dirty read in a relational database, and what can go wrong for the transaction that performed one if the writing transaction later rolls back?
basics
~20 sA dirty read is reading a row version written by a transaction that has not committed. If that writer rolls back, the value you read never existed in any committed state, so any decision, calculation or write you based on it is derived from data the database itself disowned.
What is the lost update anomaly in a database, and what sequence of operations produces it?
basics
~20 sTwo transactions read the same row, each computes a new value from what it read, then both write. The second write overwrites the first, so one committed change silently disappears. No error is raised — only the final value is wrong.
What is a non-repeatable read, and what sequence of events inside a transaction produces one?
basics
~20 sA transaction reads a row, another transaction updates that row and commits, and the first transaction reads the same row again and sees a different value. The re-read is not repeatable — the data changed underneath a transaction that is still running.
What is a phantom read in a database transaction, and how is it different from a non-repeatable read?
basics
~20 sA phantom read is when a transaction re-runs the same search condition and the set of matching rows changes, because another transaction committed inserts or deletes. A non-repeatable read is when a row you already read comes back with different values.
Why does an in-place update such as `UPDATE accounts SET balance = balance - 10 WHERE id = 42` avoid a lost update, while reading the balance into application code and writing back the computed number does not?
basics
~20 sThe in-place form reads and writes inside one statement while holding the row's write lock, so no other transaction can slip in between. The application version leaves a gap between the read and the write in which another transaction can commit a change you never see.
What does the READ COMMITTED transaction isolation level guarantee, and which concurrency anomalies can still occur under it?
basics
~20 sIt guarantees one thing: you never read data written by a transaction that has not committed — no dirty reads. Everything weaker still happens: non-repeatable reads, phantoms, read skew across statements, and lost updates in read-modify-write code.
What is a dirty read, and what does the READ UNCOMMITTED transaction isolation level permit that stronger levels do not?
basics
~20 sA dirty read is seeing a row written by a transaction that has not committed — data that may be rolled back and never have existed. READ UNCOMMITTED is the only standard level that allows it; it is the weakest level and permits every other anomaly too.
Inside one database transaction you run SELECT balance FROM accounts WHERE id = 7 twice and get two different values because another transaction committed a change in between. What is that anomaly called, and what does the REPEATABLE READ isolation level do about it?
basics
~20 sThat is a non-repeatable read. REPEATABLE READ gives a transaction a stable view of rows it has read — a fixed snapshot, or read locks held until commit — so the second SELECT returns the same value as the first.
Two concurrent transactions, both at the READ COMMITTED isolation level, each read an account's balance, compute a new value in application code, and write it back. Explain what goes wrong and what your options are to prevent it.
basics
~20 sOne update is silently lost: both read 100, both write their own result, the second overwrites the first and no error is raised. Fixes: do the arithmetic in one UPDATE statement, lock the row on read with SELECT ... FOR UPDATE, or use an optimistic version column and retry when zero rows change.
Under the READ COMMITTED isolation level, two identical queries inside one transaction can return different results. Explain the visibility rule that causes this, and contrast it with taking one view for the whole transaction.
basics
~20 sREAD COMMITTED establishes a new view of committed data at the start of every statement, not once per transaction. So any transaction that commits between your two queries becomes visible to the second one. A transaction-wide view would hide those commits instead.
In a database that uses multi-version concurrency control (MVCC), why can a long-running SELECT keep reading rows while other transactions update those same rows, with neither side waiting on the other?
basics
~20 sAn update writes a new version of the row instead of overwriting it in place. The reader keeps seeing the version that was current when its snapshot began, so it needs no lock on the row, and the writer has no reader lock to wait behind.
In a database engine that keeps multiple physical versions of each row so concurrent transactions read consistent snapshots, what is table and index bloat, and why does it accumulate?
basics
~20 sUpdates and deletes leave the old row version behind so older snapshots can still read it. Until a background cleanup process reclaims those dead versions, tables and indexes hold space no query needs. That waste is bloat.
In a database that keeps multiple versions of each row, what stamps are recorded on a row version when it is created or superseded, and how does the engine use those stamps to decide whether a particular transaction may read that version?
basics
~20 sEvery row version records the id of the transaction that created it and, once superseded or deleted, the id of the transaction that expired it. A reader sees a version only if its creator had committed as of the reader's snapshot and its expirer had not.
Two concurrent transactions each issue an UPDATE against the same row in an MVCC database. Walk through what each transaction experiences from the moment the second UPDATE is issued until both finish.
basics
~20 sThe first updater takes an exclusive row lock and creates a new version. The second UPDATE finds the row locked and blocks. When the first commits, the second either re-reads the new version and applies its change, or aborts with a serialization error, depending on isolation level.
Why can a single session that has held one transaction open for hours stop a database's background version cleanup from reclaiming dead rows across every table, not just the tables that session touched?
basics
~20 sCleanup may only remove versions that no live snapshot can see. The oldest active snapshot sets a single global horizon; anything that died after it must be kept. One ancient transaction holds that horizon back, so dead rows everywhere become unreclaimable.
Two database transactions are each waiting for a lock the other already holds, and neither can make progress. What is this situation called, how does a relational database typically get out of it, and what is the application expected to do?
basics
~20 sIt is a deadlock. The database detects the cycle of waits, picks one transaction as victim, and rolls it back with a deadlock error; the survivor proceeds. The application must catch that error and retry the whole transaction, ideally after a short backoff.
In the two-phase locking (2PL) concurrency-control protocol used by relational database engines, what are the two phases, what rule must a transaction obey in each, and what is the transaction's "lock point"?
basics
~20 sGrowing phase: the transaction may take locks but release none. Shrinking phase: it may release locks but take no new ones. The lock point is the instant between them, where it holds every lock it will ever hold.
A database engine can take locks at row, page, or whole-table granularity. What are the trade-offs, and what makes an engine choose a coarser or finer level?
basics
~20 sFine granularity (row) maximises concurrency but costs memory and CPU per lock; coarse granularity (table) is nearly free to track but serialises unrelated work. Engines choose by how many rows a statement touches — few rows, lock rows; whole table, lock the table.
What does `SELECT ... FOR UPDATE` give you that a plain `SELECT` does not, and how does `FOR SHARE` differ from it?
basics
~20 sFOR UPDATE takes exclusive row locks on the rows it reads, held until commit, so no one else can modify or lock them — it turns a read into a claim. FOR SHARE takes shared row locks: others may still read, but nobody may modify the rows until you commit.
A service opens a database transaction, calls an external payment API inside it, and commits only after that API responds. What costs does keeping the transaction open across the remote call impose on the database, and what would you do instead?
basics
~20 sFor the whole API call the transaction keeps its locks, keeps its read snapshot, and holds a pooled connection. Other writers queue behind those locks and old row versions cannot be cleaned up. Move the remote call outside the transaction.
Two users load the same customer record and both submit an edit. Compare optimistic concurrency control (a version column checked at write time) with pessimistic concurrency control (locking the row at read time via SELECT ... FOR UPDATE): how does each stop one edit from silently overwriting the other, and when would you choose each?
basics
~20 sPessimistic locks the row when you read it, so the second writer waits — safe, but it holds locks and blocks. Optimistic takes no lock: you remember a version, and the update applies only if the version is unchanged; otherwise you re-read and retry. Rare conflicts favor optimistic; hot contended rows favor pessimistic.
A service inserts an order row and then updates a stock row, running on a connection left in autocommit mode. Explain what autocommit does to those two statements, what can go wrong, and how the transaction scope should be set instead.
basics
~20 sIn autocommit each statement is its own transaction, committed the moment it succeeds. So the insert is durable even if the update then fails, leaving an order with no stock deducted, and nothing can roll it back. Both statements must run inside one explicit transaction on the same connection.
In a database that uses multi-version concurrency control (MVCC), why can one transaction that stays open for hours cause tables and undo/version storage to keep growing, even when that transaction only reads and never writes?
basics
~20 sAn open transaction pins a snapshot, and the engine may not reclaim any row version that the oldest live snapshot could still need. So while it sits there, superseded versions from every other transaction accumulate: table and index bloat, growing undo storage, slower scans.
Your write is an UPDATE guarded by a version column, and it sometimes reports zero affected rows. Walk through how you would build the retry loop around that failure: what must be redone, how many attempts, what backoff, and what would make a retry unsafe.
basics
~20 sZero affected rows means someone else wrote the row, so your in-memory copy is stale. Retry the whole unit of work — new transaction, fresh read, recompute, write again — not just the UPDATE. Cap attempts (3–5), use jittered backoff, and keep external side effects out of the retried block or make them idempotent.