Practice Questions — Transactions and ACID Properties
← Back to NotesTopic-Specific Questions
Question 1
Easy
What does the A in ACID stand for, and what does it guarantee?
Think all-or-nothing.
Atomicity. Every statement inside a transaction succeeds together or none of them do. A partial completion is impossible — on ROLLBACK or crash, the entire transaction is undone.
Question 2
Easy
Which statement begins a transaction in MySQL?
Two equivalent keywords.
START TRANSACTION; (preferred) or BEGIN;. Both are equivalent in MySQL. They implicitly turn off autocommit for the duration of the transaction.Question 3
Easy
What is the final balance?
-- starts: 1000
START TRANSACTION;
UPDATE accounts SET balance = balance + 200 WHERE id = 1;
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
ROLLBACK;
SELECT balance FROM accounts WHERE id = 1;ROLLBACK undoes both UPDATEs.
1000Question 4
Easy
What is MySQL's default isolation level?
Four words.
REPEATABLE READ. Unlike some databases (PostgreSQL defaults to READ COMMITTED), MySQL/InnoDB chose REPEATABLE READ as its default because MVCC makes it cheap and it prevents most common anomalies.Question 5
Easy
What is a dirty read?
Think 'data that doesn't officially exist yet'.
Reading a row that has been modified by another transaction that has not yet committed. If the writer then ROLLBACKs, the reader has seen data that never officially existed. Only READ UNCOMMITTED permits dirty reads.
Question 6
Easy
What does COMMIT do?
Two guarantees: visibility and durability.
Makes all changes inside the transaction (a) visible to other transactions and (b) durably written to the redo log on disk, so they survive a crash. After COMMIT, ROLLBACK cannot undo them.
Question 7
Medium
Final balance?
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE id = 1; -- 1000 -> 1100
SAVEPOINT p1;
UPDATE accounts SET balance = balance + 200 WHERE id = 1; -- 1100 -> 1300
ROLLBACK TO SAVEPOINT p1;
COMMIT;Roll back to the savepoint, then commit.
1100Question 8
Medium
Which isolation level prevents dirty reads but still allows non-repeatable reads?
Read only committed data, but another transaction can commit mid-way through yours.
READ COMMITTED.Question 9
Medium
What does SELECT ... FOR UPDATE do?
Acquires an exclusive lock.
Acquires an exclusive (X) lock on the matching rows for the duration of the transaction. Other transactions that try to SELECT FOR UPDATE, LOCK IN SHARE MODE, or modify those rows block until this transaction commits or rolls back. Plain SELECTs still work (they read the MVCC snapshot).
Question 10
Medium
What is a deadlock, and how does InnoDB handle it?
Cycle of lock waits + detection.
A deadlock is a cycle in the wait-for graph: T1 waits for a lock T2 holds, T2 waits for a lock T1 holds. InnoDB's deadlock detector notices the cycle and aborts one of the transactions (the one that has done less work) with ERROR 1213 / SQLSTATE 40001. The aborted transaction must retry.
Question 11
Medium
Why does InnoDB use MVCC for REPEATABLE READ rather than taking read locks?
Concurrency.
MVCC lets readers and writers run concurrently without blocking each other. SELECT sees a snapshot of the database as of transaction start, regardless of concurrent writes. Taking read locks instead would force readers to queue behind writers and vice versa, crushing throughput.
Question 12
Medium
Is SERIALIZABLE always the right choice for safety?
Think cost.
No. SERIALIZABLE effectively upgrades every SELECT to LOCK IN SHARE MODE and blocks writers until readers commit. Under high concurrency, throughput plummets. Use SERIALIZABLE only for short, critical sections where the anomalies prevented are genuine problems.
Question 13
Hard
Explain why two concurrent withdrawals without FOR UPDATE can cause an overdraft even under REPEATABLE READ.
Think MVCC snapshots vs write locks.
Each transaction's SELECT reads the MVCC snapshot taken at transaction start — both see balance = 500. Each transaction's IF check decides 500 >= 300 is OK and issues an UPDATE. UPDATE takes a write lock, so the second one waits, but once it acquires it, it just executes
balance = balance - 300 on the current (already-debited) value. Final balance: -100. REPEATABLE READ prevents the read from changing, not the arithmetic from overshooting. FOR UPDATE forces the SELECT itself to lock, serialising the whole critical section.Question 14
Hard
Why must DDL statements be treated as permanent in MySQL transactions?
Implicit commits.
Statements like CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE, and RENAME cause an implicit COMMIT of the current transaction. A subsequent ROLLBACK cannot undo the DDL (the table is still created/dropped/altered). This is a MySQL-specific behaviour — some databases (PostgreSQL) have transactional DDL. Plan migrations accordingly.
Question 15
Hard
How does adding an index reduce lock contention?
Locks attach to what the engine scans.
InnoDB locks the index rows it reads to enforce isolation. Without an index on the WHERE column, a statement scans (and thus locks) far more rows than it needs — potentially the whole table. With an index, only the matching index entries are locked. Better indexes mean less locking, fewer deadlocks, and higher concurrency.
Question 16
Hard
Describe the 'transactional outbox' pattern and when to use it.
Reliable event emission from transactional systems.
Instead of calling an external system (email, message queue, webhook) from inside a DB transaction, INSERT a row into an
outbox table as part of the transaction. A separate background worker reads committed outbox rows and dispatches them, marking them sent. If the transaction rolls back, the outbox row is rolled back too, so no message is emitted for a cancelled transaction. If the worker crashes, unprocessed rows remain and are retried.Question 17
Hard
A long-running SELECT inside a transaction holds a snapshot for 10 minutes. What is the downside on a busy server?
Undo log growth.
InnoDB must keep old versions of every row modified during those 10 minutes, in case the long-running transaction reads them. The undo log (rollback segment) grows proportionally, filling disk and slowing every other transaction. This is the 'history list length' problem. Kill long-running read transactions with
KILL or split them into smaller chunks.Mixed & Application Questions
Question 1
Easy
What does this return?
-- balance starts at 100
START TRANSACTION;
UPDATE t SET balance = 500 WHERE id = 1;
ROLLBACK;
SELECT balance FROM t WHERE id = 1;Rollback undoes the update.
100Question 2
Easy
Is SELECT * FROM t inside a transaction blocking under MySQL REPEATABLE READ?
MVCC.
No. Plain SELECT reads the MVCC snapshot without taking any row lock, so it does not block and is not blocked by other writers.
Question 3
Medium
What is the final value?
-- x starts at 10
START TRANSACTION;
UPDATE t SET x = x + 1 WHERE id = 1; -- 11
SAVEPOINT a;
UPDATE t SET x = x + 1 WHERE id = 1; -- 12
SAVEPOINT b;
UPDATE t SET x = x + 1 WHERE id = 1; -- 13
ROLLBACK TO SAVEPOINT a;
COMMIT;
SELECT x FROM t;Roll back to savepoint 'a' removes changes after it.
11Question 4
Medium
Name three ways to cause a deadlock.
Cycle creation.
(1) Two transactions update the same rows in opposite orders (T1 locks 101 then 102; T2 locks 102 then 101). (2) Transactions taking locks from left to right on different indexes that overlap. (3) A transaction locking a gap (next-key lock) that another transaction tries to insert into. All three create cycles.
Question 5
Medium
Under REPEATABLE READ in InnoDB, can a phantom read occur on a plain SELECT?
InnoDB uses snapshots for reads.
No. MVCC snapshots make every SELECT inside the transaction see the same set of rows regardless of concurrent INSERTs that are committed after the transaction started. For plain reads, InnoDB REPEATABLE READ is effectively serializable.
Question 6
Medium
Two sessions run:
-- Session 1
START TRANSACTION;
UPDATE t SET x = 100 WHERE id = 1;
-- no commit yet
-- Session 2 (READ COMMITTED)
START TRANSACTION;
SELECT x FROM t WHERE id = 1;What does Session 2's SELECT return (assuming original x = 5)?READ COMMITTED reads the latest COMMITTED value.
5Question 7
Medium
What does LOCK IN SHARE MODE guarantee?
Shared lock semantics.
Takes a shared (S) lock on the read rows for the duration of the transaction. Other transactions can also take S locks, so concurrent readers coexist. But no one can take an X lock (no UPDATE, no DELETE, no FOR UPDATE) on those rows until this transaction commits. Use it for 'read now, I will need the value to stay consistent for the rest of my transaction' scenarios.
Question 8
Hard
A payment service sees 20 deadlocks per minute. What are the first three things to check?
Ordering, indexes, transaction length.
(1) Are transactions locking rows in a consistent order? Sort by id before updating multiple rows. (2) Are the WHERE clauses indexed? A missing index locks far more rows than necessary, amplifying contention. (3) How long are the transactions? External API calls, large scans, or slow queries inside transactions expand the lock window; shorten them.
Question 9
Hard
What happens?
-- Session 1 (autocommit OFF)
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- forgets to COMMIT and moves on
-- Session 2
SELECT balance FROM accounts WHERE id = 1; -- under REPEATABLE READThink about what Session 2 sees and whether Session 2 blocks.
Session 2's plain SELECT returns the old, pre-deduction balance and does not block. Session 1's uncommitted UPDATE is invisible under MVCC. But Session 1's lock is still held indefinitely, so any write to row id = 1 by another session will block until Session 1 commits, rolls back, or is killed.
Question 10
Hard
Why should you never call an external API inside a transaction?
Locks and latency.
Locks are held from the first write statement until COMMIT/ROLLBACK. An external call can take seconds and has unpredictable latency. Every row your transaction has touched is locked for the duration. Under load, a 3-second API call turns into a 3-second outage for anyone else touching those rows. Perform external calls before START TRANSACTION (to get inputs) or after COMMIT (to emit events via the outbox pattern).
Question 11
Hard
Final balances after:
-- accounts: 101:1000, 102:1000
SET autocommit = 0;
UPDATE accounts SET balance = balance - 300 WHERE id = 101;
-- connection drops here without COMMIT or ROLLBACKWhat happens to uncommitted work when the connection closes?
101:1000, 102:1000. When the client disconnects without COMMIT, MySQL rolls back any uncommitted work automatically. Aarav's balance is restored.
Question 12
Hard
Explain write skew and whether REPEATABLE READ prevents it.
Two transactions each read a consistent snapshot and write different rows.
Write skew: two transactions each read a consistent view of the data, then each writes to a different row. Individually, each write is valid — but the combined effect breaks an invariant. Example: two on-call engineers. Rule: at least one must be on call. Each transaction checks 'I am the only one off-call, but there is still one other' and then both go off-call simultaneously. REPEATABLE READ does not prevent this because neither transaction modified a row the other read. Only SERIALIZABLE or explicit FOR UPDATE on the invariant rows prevents it.
Question 13
Hard
What is innodb_lock_wait_timeout and when does it matter?
Default 50 seconds.
The number of seconds a transaction will wait for a row lock before giving up with error 1205 (Lock wait timeout exceeded). Default is 50 seconds. For OLTP workloads, 50 seconds is far too long — it amounts to an outage from the user's perspective. Setting it to 3-5 seconds surfaces contention problems quickly and keeps p99 latency in check.
Question 14
Hard
Would setting isolation level to READ COMMITTED fix a deadlock caused by next-key locks on a range UPDATE?
READ COMMITTED uses only row locks, no gap locks.
Often yes. Under READ COMMITTED, InnoDB takes only row locks (not next-key / gap locks), so range UPDATEs lock fewer slots and deadlocks caused by concurrent inserts into the gap disappear. The trade-off: you give up phantom-read prevention, which can matter for some reports. Measure before flipping the switch.
Question 15
Hard
Two ways to recover from a stuck long-running transaction.
Kill or wait.
(1)
SHOW PROCESSLIST to find the offending thread, then KILL <id>; to terminate it and roll it back. (2) Ride out the storm and alert on lock wait timeouts; the transaction eventually commits, rolls back, or the client disconnects. Option 1 is usually the right call in production.Multiple Choice Questions
MCQ 1
What does ROLLBACK do?
Answer: B
B is correct. ROLLBACK returns the database to the state it was in when the transaction started.
B is correct. ROLLBACK returns the database to the state it was in when the transaction started.
MCQ 2
Which letter in ACID stands for 'committed data survives crashes'?
Answer: D
D is correct. D = Durability. Backed by the write-ahead redo log being flushed to disk at COMMIT time.
D is correct. D = Durability. Backed by the write-ahead redo log being flushed to disk at COMMIT time.
MCQ 3
Which is MySQL's default isolation level?
Answer: C
C is correct. MySQL/InnoDB defaults to REPEATABLE READ, unlike PostgreSQL which defaults to READ COMMITTED.
C is correct. MySQL/InnoDB defaults to REPEATABLE READ, unlike PostgreSQL which defaults to READ COMMITTED.
MCQ 4
Which statement takes an exclusive lock on the matched rows?
Answer: C
C is correct. FOR UPDATE takes an X lock, blocking any other write or locking read until the transaction ends.
C is correct. FOR UPDATE takes an X lock, blocking any other write or locking read until the transaction ends.
MCQ 5
What is autocommit = 1?
Answer: A
A is correct. autocommit = 1 is MySQL's default. Each standalone statement is its own mini-transaction. START TRANSACTION suspends autocommit until COMMIT or ROLLBACK.
A is correct. autocommit = 1 is MySQL's default. Each standalone statement is its own mini-transaction. START TRANSACTION suspends autocommit until COMMIT or ROLLBACK.
MCQ 6
Which anomaly can still occur under READ COMMITTED?
Answer: B
B is correct. READ COMMITTED blocks dirty reads but still allows non-repeatable reads (a committed UPDATE from another transaction is visible on your next SELECT).
B is correct. READ COMMITTED blocks dirty reads but still allows non-repeatable reads (a committed UPDATE from another transaction is visible on your next SELECT).
MCQ 7
What does SAVEPOINT enable?
Answer: B
B is correct. ROLLBACK TO SAVEPOINT name undoes changes since that savepoint without ending the transaction.
B is correct. ROLLBACK TO SAVEPOINT name undoes changes since that savepoint without ending the transaction.
MCQ 8
What happens when InnoDB detects a deadlock?
Answer: B
B is correct. The deadlock detector kills the transaction that has done less work. The winner finishes, the loser must retry.
B is correct. The deadlock detector kills the transaction that has done less work. The winner finishes, the loser must retry.
MCQ 9
Which is NOT a way to reduce deadlocks?
Answer: D
D is correct. Removing primary keys hurts both performance and correctness. The other three are standard deadlock mitigations.
D is correct. Removing primary keys hurts both performance and correctness. The other three are standard deadlock mitigations.
MCQ 10
Under MySQL/InnoDB REPEATABLE READ, can phantom reads occur on plain SELECTs?
Answer: B
B is correct. Plain SELECTs read the transaction-start snapshot, so a concurrent committed INSERT does not appear.
B is correct. Plain SELECTs read the transaction-start snapshot, so a concurrent committed INSERT does not appear.
MCQ 11
What does DDL do to an open transaction in MySQL?
Answer: B
B is correct. CREATE, ALTER, DROP, TRUNCATE, and RENAME all cause implicit COMMIT. ROLLBACK after DDL cannot undo the DDL.
B is correct. CREATE, ALTER, DROP, TRUNCATE, and RENAME all cause implicit COMMIT. ROLLBACK after DDL cannot undo the DDL.
MCQ 12
Which lock does a plain UPDATE take on the rows it modifies?
Answer: B
B is correct. UPDATE and DELETE always take X locks on the affected index rows.
B is correct. UPDATE and DELETE always take X locks on the affected index rows.
MCQ 13
Under REPEATABLE READ, two concurrent withdrawals without FOR UPDATE can both see balance = 500 and both succeed. Why?
Answer: B
B is correct. The classic 'lost update' pattern. FOR UPDATE is required to serialise the critical section.
B is correct. The classic 'lost update' pattern. FOR UPDATE is required to serialise the critical section.
MCQ 14
Error 1213 (40001) means:
Answer: B
B is correct. Applications must catch 1213 and retry. Exponential backoff reduces thundering-herd retry storms.
B is correct. Applications must catch 1213 and retry. Exponential backoff reduces thundering-herd retry storms.
MCQ 15
Which isolation level is effectively achieved for plain reads by InnoDB REPEATABLE READ + MVCC?
Answer: C
C is correct. Plain reads under REPEATABLE READ see a consistent snapshot for the transaction, like SERIALIZABLE, but without the locking cost.
C is correct. Plain reads under REPEATABLE READ see a consistent snapshot for the transaction, like SERIALIZABLE, but without the locking cost.
MCQ 16
Which is TRUE about innodb_lock_wait_timeout?
Answer: B
B is correct. Default 50 seconds; most OLTP shops reduce it to 3-5 seconds so contention surfaces quickly.
B is correct. Default 50 seconds; most OLTP shops reduce it to 3-5 seconds so contention surfaces quickly.
MCQ 17
Why call external APIs outside transactions?
Answer: B
B is correct. Call external services before START TRANSACTION or after COMMIT (using an outbox if you need reliability).
B is correct. Call external services before START TRANSACTION or after COMMIT (using an outbox if you need reliability).
Coding Challenges
Challenge 1: Atomic Transfer
EasyWrite a transaction that moves Rs. 1500 from account 101 to account 102. Ensure both UPDATEs succeed or neither does.
Sample Input
accounts: (101,'Aarav',5000), (102,'Priya',2000)
Sample Output
(101,3500), (102,3500)
Use START TRANSACTION and COMMIT.
START TRANSACTION;
UPDATE accounts SET balance = balance - 1500 WHERE id = 101;
UPDATE accounts SET balance = balance + 1500 WHERE id = 102;
COMMIT;Challenge 2: Rollback on Insufficient Funds
EasyTransfer Rs. X from account A to account B only if A has enough balance. Use FOR UPDATE and ROLLBACK if balance is insufficient.
Sample Input
accounts: (101,500), (102,1000). Transfer 700 from 101 to 102.
Sample Output
Transaction rolled back. Balances unchanged.
Use FOR UPDATE. Check balance. Use IF / ROLLBACK.
START TRANSACTION;
SELECT balance INTO @bal FROM accounts WHERE id = 101 FOR UPDATE;
IF @bal < 700 THEN
ROLLBACK;
ELSE
UPDATE accounts SET balance = balance - 700 WHERE id = 101;
UPDATE accounts SET balance = balance + 700 WHERE id = 102;
COMMIT;
END IF;Challenge 3: Savepoint Retry
EasyInsert an order, then insert items. If any item insert fails, roll back to the savepoint after the order insert and try a different item. Finally COMMIT.
Sample Input
Order total 1500, first try item_id=42 (fails), then item_id=99 (works).
Sample Output
orders has 1 row. order_items has 1 row with product_id=99.
Use SAVEPOINT / ROLLBACK TO SAVEPOINT.
START TRANSACTION;
INSERT INTO orders(customer_id, total) VALUES (7, 1500);
SET @oid = LAST_INSERT_ID();
SAVEPOINT after_order;
-- pretend the next insert fails; handle in app, then:
ROLLBACK TO SAVEPOINT after_order;
INSERT INTO order_items(order_id, product_id, qty) VALUES (@oid, 99, 1);
COMMIT;Challenge 4: Prevent Double-Booking of a Seat
MediumTwo users try to book the same seat concurrently. Write the SQL (for one session) that correctly books the seat only if it is currently NULL.
Sample Input
seats: (1, 'AI-101', NULL). User 1001 tries to book.
Sample Output
(1, 'AI-101', 1001) after the transaction; a concurrent session sees the seat already booked and does not overwrite.
Use SELECT ... FOR UPDATE. Check NULL. UPDATE only if NULL.
START TRANSACTION;
SELECT booked_by INTO @who FROM seats WHERE id = 1 FOR UPDATE;
IF @who IS NULL THEN
UPDATE seats SET booked_by = 1001 WHERE id = 1;
END IF;
COMMIT;Challenge 5: Simulate a Dirty Read
MediumUsing two MySQL sessions, demonstrate that READ UNCOMMITTED allows a dirty read. Provide the exact SQL for both sessions as comments.
Sample Input
accounts: (101, 5000). Session 2 UPDATEs without committing; Session 1 SELECTs.
Sample Output
Session 1 sees the uncommitted value; Session 2 rolls back; Session 1 acted on phantom data.
Set SESSION ISOLATION LEVEL READ UNCOMMITTED in Session 1.
-- Session 1
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 101; -- 5000
-- <wait>
SELECT balance FROM accounts WHERE id = 101; -- 9999 (dirty)
COMMIT;
-- Session 2
START TRANSACTION;
UPDATE accounts SET balance = 9999 WHERE id = 101;
-- do NOT commit
ROLLBACK;
-- Session 1's second SELECT had seen 9999, a value that never officially existed.Challenge 6: Isolation Level Proof
MediumShow two sessions where Session 1 under REPEATABLE READ does NOT see Session 2's committed UPDATE. Provide the SQL.
Sample Input
accounts: (101, 5000).
Sample Output
Session 1's second SELECT still returns 5000 even though Session 2 committed an UPDATE to 6000.
Session 1 must START TRANSACTION under REPEATABLE READ before Session 2 updates.
-- Session 1
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 101; -- 5000 (snapshot taken)
-- <wait for Session 2 to commit>
SELECT balance FROM accounts WHERE id = 101; -- still 5000
COMMIT;
-- Session 2
START TRANSACTION;
UPDATE accounts SET balance = 6000 WHERE id = 101;
COMMIT;
-- Session 2's new value is invisible to Session 1 until Session 1 commits and starts a fresh transaction.Challenge 7: Retry on Deadlock
HardWrite pseudocode (or Python) that runs a transaction with up to 3 retries on error 1213. Use exponential backoff: 10ms, 20ms, 40ms.
Sample Input
Under contention, occasionally the transaction errors with 1213.
Sample Output
Transaction succeeds on a later attempt; on 3 failures, a RuntimeError is raised.
Must ROLLBACK on deadlock. Must sleep between retries.
import time
from pymysql.err import OperationalError # or the equivalent in your driver
def run_with_retry(conn, body_fn, max_attempts=3):
for attempt in range(max_attempts):
try:
conn.begin()
body_fn(conn)
conn.commit()
return
except OperationalError as e:
if e.args and e.args[0] == 1213:
conn.rollback()
time.sleep(0.01 * (2 ** attempt))
continue
raise
raise RuntimeError('Gave up after %d deadlock retries' % max_attempts)Challenge 8: Consistent Lock Ordering
HardRewrite this two-account transfer so that both transactions always lock the rows in the same order (lowest id first), preventing deadlocks.
Sample Input
-- Current naive version
START TRANSACTION;
UPDATE accounts SET balance = balance - amt WHERE id = from_id;
UPDATE accounts SET balance = balance + amt WHERE id = to_id;
COMMIT;
Sample Output
Both transactions always lock the smaller id first, preventing a deadlock cycle.
Use LEAST / GREATEST or an IF. Still debit/credit correctly.
-- Always SELECT ... FOR UPDATE both rows in (smaller id, larger id) order,
-- then apply the debit/credit based on which id is the source.
START TRANSACTION;
SELECT id INTO @lo FROM accounts WHERE id = LEAST(:from_id, :to_id) FOR UPDATE;
SELECT id INTO @hi FROM accounts WHERE id = GREATEST(:from_id, :to_id) FOR UPDATE;
UPDATE accounts SET balance = balance - :amt WHERE id = :from_id;
UPDATE accounts SET balance = balance + :amt WHERE id = :to_id;
COMMIT;Need to Review the Concepts?
Go back to the detailed notes for this chapter.
Read Chapter NotesWant to master SQL and databases with a mentor?
Explore our MySQL Masterclass