Understanding Transactions and ACID Properties: The Complete Guide
A database transaction groups one or more SQL operations so they can be managed as a single unit
of work. The goal is to keep the database in a valid state even when an operation fails or
multiple users are working with the same data at the same time. The four ACID properties —
Atomicity, Consistency, Isolation, and
Durability — describe the key guarantees that make transactions reliable.
This guide covers SQL transactions, ACID properties, COMMIT,
ROLLBACK, isolation levels, savepoints, deadlocks, and practical concurrency
scenarios, followed by twenty questions commonly asked in SQL interviews. The examples use the
same customer and order tables as the rest of the series.
🎯 Key Takeaways
Transactions group statements into one unit
ACID — atomic, consistent, isolated, durable
COMMIT makes changes permanent; ROLLBACK undoes them
Isolation levels trade correctness for concurrency
Deadlocks are detected and one transaction is rolled back
20 Interview QuestionsBeginner to AdvancedReal ExamplesUpdated Oct 7, 2026
KW
Kailas Warade
✓ Verified Author
Data Engineer · Oracle Certified · SQL & PL/SQL
Kailas works with SQL, PL/SQL, Python, and PySpark to build data pipelines.
He writes the SQL interview guides at Sankalan Data Tech, focused on
practical, real-world answers.
🏆 Oracle Certified💼 Data Engineer📝 SQL & PL/SQL Guides📍 Pune, India
A transaction groups related SQL operations into a single unit of work. The goal is to
make sure the database does not end up in an unwanted partial state when one of the
operations fails. Depending on the database system, the transaction can be completed with
COMMIT or undone with ROLLBACK.
A bank transfer is a simple example. Suppose one statement subtracts money from account A
and another adds the same amount to account B. If the first operation succeeds but the
second fails, the accounts are no longer correct. A transaction allows both operations to
be treated as one unit, so the application can commit the transfer when everything
succeeds or roll it back when something goes wrong.
Database systems use mechanisms such as logging, locking, and multi-version concurrency
control to support transactions and concurrent access. The exact implementation depends
on the database engine. From the application's point of view, the usual flow is to start
or enter a transaction, execute the required SQL statements, and then use
COMMIT or ROLLBACK as appropriate. Some databases and drivers
manage transaction boundaries differently, so the exact syntax can vary.
Transactions are important in many real systems, including order processing, payments,
inventory updates, and user registration. Whenever several database changes belong
together and a partial result could cause incorrect data, transaction management becomes
an important part of application and database design.
The Sample Data We Use in Every Example
Same customers and orders tables as the rest of the series. For the transaction examples,
imagine a simpler accounts table that models a bank-style ledger.
-- customers and orders (as used throughout the series)
-- customers: customer_id | customer_name | region | credit_limit
-- orders: order_id | customer_id | order_amount | order_status
-- accounts (used in the transaction examples below)
-- account_id | holder_name | balance
-- 1 | Alice Corp | 10000
-- 2 | Bob Traders | 5000
The accounts table is small and easy to reason about. Every transaction
example below either transfers money between accounts or updates related rows, and you
can run the same examples against any ledger-style table in your own database.
The Four ACID Properties
ACID is the standard name for the four properties a transaction must guarantee. Every
relational database worth trusting implements all four, and every transaction interview
starts here.
Atomicity — all or nothing
Every statement in a transaction either succeeds or the whole transaction fails. If any
single statement fails — a constraint violation, a runtime error, a power outage — the
database undoes everything back to the start of the transaction.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
That is a transfer of 500 from account 1 to account 2. If the second UPDATE fails, the
first is undone automatically when the transaction rolls back. Account 1 does not lose
the money with nothing to show for it.
Consistency — valid states only
A transaction moves the database from one valid state to another. Every constraint —
foreign keys, unique keys, check constraints — must hold at the end. A transaction that
would violate a constraint fails and rolls back.
Consistency in the ACID sense is narrower than people sometimes assume. It means
the database's own rules are not violated. If your business rule says "an order
must have a valid customer", that rule has to be enforced by a foreign key or a trigger
for consistency to hold. The database does not read your business rules from a document —
it enforces whatever constraints you have declared.
Isolation — transactions do not interfere
Concurrent transactions behave as if they ran one after the other. Alice's transfer does
not see Bob's uncommitted work, and Bob's does not see Alice's. Whatever order the
database actually executes them in, the visible outcome is the same as some serial order.
Isolation is the property that is easiest to loosen. Strong isolation comes at the cost
of concurrency — transactions block each other more, throughput drops. The four isolation
levels (READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE) trade correctness
for speed.
Durability — once committed, it stays
After COMMIT returns, the changes are permanent. Even if the server crashes the next
millisecond, the changes survive. The database achieves this by writing the changes to a
transaction log and flushing it to disk before COMMIT returns.
That flush is the reason COMMIT is not instant. Fast storage (SSD, NVMe) makes it much
faster than spinning disk, but the fundamental cost is real. Systems that promise "no
data loss" pay for durability with that small delay on every commit.
How the four fit together
Property
Guarantee
What it protects against
Atomicity
All statements succeed or none do
Partial application of a multi-step change
Consistency
Constraints hold before and after
Invalid data entering the database
Isolation
Transactions do not see uncommitted work
Reading or writing dirty data
Durability
Committed changes survive failure
Data loss on crash or power failure
The Transaction Lifecycle
Every transaction follows the same sequence. Knowing the sequence makes the individual
statements easier to reason about.
Step 1 — BEGIN
BEGIN TRANSACTION; -- SQL Server, PostgreSQL
START TRANSACTION; -- MySQL
SET TRANSACTION; -- Oracle (implicitly starts on first DML)
The BEGIN statement marks the start of the transaction. Every DML statement after it
becomes part of the same unit of work until you commit or roll back. Some databases
(Oracle) start a transaction implicitly when the first DML statement runs, so the BEGIN
is not strictly required.
Step 2 — Perform the work
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
Any number of INSERT, UPDATE, DELETE, or SELECT statements. Each runs inside the
transaction. Other sessions do not see the changes until you commit.
Step 3 — Commit or roll back
COMMIT; -- make the changes permanent
-- or
ROLLBACK; -- discard the changes entirely
COMMIT ends the transaction successfully. ROLLBACK discards the work. After either, the
session is back in autocommit mode (or ready to start the next transaction explicitly).
What autocommit means
By default, most databases run in autocommit mode. Each statement is its own transaction —
it commits the moment it succeeds. There is no window where ROLLBACK could undo it. To
get transactional behaviour, you have to explicitly start a transaction.
-- Autocommit: this update is committed immediately
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
-- Explicit transaction: this update is not committed until you say so
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
ROLLBACK; -- the update is undone
Oracle is different — autocommit is off by default, and every DML statement starts an
implicit transaction. MySQL, PostgreSQL, and SQL Server default to autocommit on.
COMMIT and ROLLBACK
Two statements end a transaction. COMMIT keeps the work. ROLLBACK discards it.
COMMIT — make it permanent
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
After COMMIT, the two updates are part of the database. Other sessions see them. The
transaction log has the changes on disk. There is no undo.
Before COMMIT, the changes exist only in your session. Other users see the old values.
You can still ROLLBACK to undo everything. Once you commit, that window closes.
ROLLBACK — discard the work
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
ROLLBACK;
The two updates are undone. Account 1 has its original balance. Account 2 has its
original balance. Nothing changed.
The typical use is error handling. If a statement fails or a business rule is violated,
you roll back and start over. The database returns to the state it was in before the
transaction started.
What gets rolled back and what does not
DML statements — INSERT, UPDATE, DELETE, MERGE. All rolled back.
SELECT statements — no effect to roll back. A query does not change data.
DDL statements — CREATE, ALTER, DROP. These auto-commit on most databases, so they cannot be rolled back as part of the same transaction.
Sequence values — once NEXTVAL is called, the value is consumed even if you roll back. Sequences are non-transactional.
When to commit and when to roll back
Commit when a logical unit of work has completed successfully and every constraint has
been satisfied. Roll back when any step failed, when a business rule was violated, or
when the user cancelled the operation.
The one anti-pattern to avoid: committing inside a loop. Every COMMIT has real cost —
flushing to disk, updating the log. A loop that commits after every row on a million-row
operation runs a million commits and takes hours. Batch the changes and commit once.
SAVEPOINT and Partial Rollback
A SAVEPOINT marks a point inside a transaction you can return to without discarding the
whole transaction. It is useful when a long transaction has several distinct phases and
you want to undo one phase without undoing the earlier ones.
The pattern
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
SAVEPOINT after_debit;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
SAVEPOINT after_credit;
UPDATE audit_log SET note = 'Transfer complete' WHERE transfer_id = 100;
-- Realise the audit update was wrong
ROLLBACK TO SAVEPOINT after_credit;
-- Fix the audit update
UPDATE audit_log SET note = 'Transfer of 500 complete' WHERE transfer_id = 100;
COMMIT;
The final COMMIT keeps the two account updates and the corrected audit update. The
original audit update — the one that was wrong — was rolled back when the
ROLLBACK TO SAVEPOINT ran.
What SAVEPOINT does not do
It does not commit the work before it. The earlier changes still need a final COMMIT to become permanent.
It does not release locks. The locks taken by the transaction are held until the transaction ends, savepoint or not.
It is not a nested transaction. Some databases support nested transactions as a separate feature, but SAVEPOINT is a lighter mechanism — a marker, not a full sub-transaction.
When to use SAVEPOINTs
Savepoints shine in long operations with independent phases. A batch import that processes
rows in chunks — save a SAVEPOINT per chunk so a single bad chunk can be rolled back
without losing the earlier chunks. A multi-step workflow where one step is optional and
might fail without aborting the rest.
They should not be used as a substitute for proper error handling. If a step fails, the
right response is usually to roll back the whole transaction and retry. Savepoints are for
the cases where a partial rollback is genuinely the correct behaviour.
Support varies. Oracle, PostgreSQL, MySQL, and SQL Server all support SAVEPOINT. Syntax is
standard across them — SAVEPOINT name and
ROLLBACK TO SAVEPOINT name. SQL Server uses slightly different wording —
SAVE TRANSACTION name and ROLLBACK TRANSACTION name — but the
behaviour is the same.
Isolation Levels and Anomalies
Isolation is the property that decides how much one transaction can see of another's
uncommitted work. Strong isolation gives you more correctness; weaker isolation gives you
more concurrency. Every serious database lets you choose which trade-off you want.
The three anomalies
Three specific problems are what the isolation levels exist to prevent.
Dirty read. Transaction A reads data that Transaction B has written but
not committed. If B then rolls back, A has read data that never existed. This is the
worst anomaly because it produces data that is simply wrong.
-- Transaction B
BEGIN TRANSACTION;
UPDATE accounts SET balance = 6000 WHERE account_id = 1;
-- not yet committed
-- Transaction A
SELECT balance FROM accounts WHERE account_id = 1;
-- If isolation is low, A sees 6000 (dirty read)
-- Transaction B
ROLLBACK;
-- The 6000 never existed, but A already read it
Non-repeatable read. Transaction A reads a row, Transaction B updates
it and commits, Transaction A reads the same row again and gets different data. The two
reads in the same transaction disagree.
Phantom read. Transaction A runs a query that returns five rows,
Transaction B inserts a row that matches the same filter and commits, Transaction A runs
the query again and gets six rows. A new row has appeared "out of nowhere".
The four isolation levels
Level
Dirty read
Non-repeatable read
Phantom read
READ UNCOMMITTED
Possible
Possible
Possible
READ COMMITTED
Prevented
Possible
Possible
REPEATABLE READ
Prevented
Prevented
Possible
SERIALIZABLE
Prevented
Prevented
Prevented
How to set the isolation level
-- SQL Server, PostgreSQL
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
-- ...
COMMIT;
-- MySQL
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
-- ...
COMMIT;
Choosing a level
READ UNCOMMITTED — almost never the right answer for production. Useful only for rough analytics on data that is being written to constantly, where being wrong occasionally is acceptable.
READ COMMITTED — the default in most databases (PostgreSQL, SQL Server, Oracle). Good balance for most applications.
REPEATABLE READ — the MySQL default. Stricter, more blocking, useful when the transaction reads the same rows multiple times.
SERIALIZABLE — the strictest. Use for financial calculations, inventory checks, or anywhere a phantom read would cause real problems. Costs concurrency.
Most applications run with READ COMMITTED and never think about it. That is fine — it
handles the vast majority of cases. But when a bug appears that only happens under
concurrency, isolation level is one of the first things to check.
Locks and Locking Modes
A lock is a mechanism the database uses to enforce isolation. When Transaction A reads or
writes a row, the database takes a lock that decides what other transactions can do with
that row until A finishes.
Shared locks vs exclusive locks
Shared lock (S) — taken for reads. Multiple transactions can hold shared locks on the same row. Any of them can read.
Exclusive lock (X) — taken for writes. Only one transaction can hold it, and it blocks both reads and writes from others.
Update lock (U) — an intermediate mode used when a transaction intends to update a row but has not yet. Prevents two transactions from both upgrading a shared lock to exclusive and deadlocking.
Lock granularity
Locks can be taken at different levels of the database — row, page, table, database. The
database chooses automatically based on the operation.
Row-level locks — the most granular. Only the specific row is locked. Best concurrency, more memory to track locks.
Page-level locks — a fixed-size block of rows. Less memory, more blocking.
Table-level locks — the whole table. Highest concurrency cost but very cheap to track.
Explicit locking in queries
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE;
-- Update, then commit
COMMIT;
The FOR UPDATE clause takes an exclusive lock on the selected rows. Other
transactions trying to update them will block until this transaction commits or rolls
back. Useful for the read-then-write pattern where you want to make sure the value does
not change between the read and the write.
Lock duration
Locks are held until the end of the transaction. Commit or rollback releases them. That
is why short transactions are important — a long-running transaction holds locks that
block everyone else.
Some databases also release shared locks early when using read-committed isolation. This
"read-committed snapshot" behaviour is why a query can see the same data across multiple
statements within a transaction — the snapshot is taken at the start of the transaction,
not at the start of each statement.
In practice, you rarely write locks explicitly. The database takes them for you as part
of the isolation level. But when you see a query blocked for a long time, the reason is
almost always a lock held by another session.
Deadlocks and How They Are Resolved
A deadlock happens when two transactions each hold a lock the other needs. Neither can
proceed, and both wait forever. No timeout, no progress. Something has to break the
cycle, and the database handles it.
The classic deadlock
-- Transaction A
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
-- Now waiting for a lock on account 2
-- Transaction B (running at the same time)
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
-- Now waiting for a lock on account 1
-- A holds account 1, wants account 2. B holds account 2, wants account 1.
-- Deadlock.
How the database resolves it
The database runs a deadlock detector — a background process that scans the lock graph
looking for cycles. When it finds one, it picks a victim — usually the transaction with
the least work done — and rolls it back. That transaction gets an error and can retry.
The other transaction proceeds as if nothing happened.
The victim transaction usually sees an error like "deadlock detected" (PostgreSQL)
or "transaction was deadlocked on lock resources and has been chosen as the deadlock
victim" (SQL Server). Application code should catch that error and retry.
Preventing deadlocks
Three strategies reduce the chance of deadlocks. First, always access resources in the
same order across transactions — if every transaction updates account 1 before account 2,
the deadlock above cannot happen.
Second, keep transactions short. The longer a transaction holds a lock, the more chance
another transaction will want that lock while the first is still holding it.
Third, use lower isolation levels where possible. Weaker isolation takes fewer locks, and
fewer locks mean fewer opportunities for deadlock.
Retry logic in applications
-- Pseudocode for a retry loop
attempt = 0
while attempt < 3:
try:
BEGIN TRANSACTION;
-- do work
COMMIT;
break;
except DeadlockError:
ROLLBACK;
attempt += 1
if attempt == 3:
raise
sleep(random_delay)
A small random delay between retries prevents two transactions from deadlocking again
the same way. Retrying immediately usually runs into the same lock pattern.
Deadlocks are not a bug in your query — they are a fact of concurrent access. Every
production database has them. What matters is that the application knows how to handle
them gracefully.
Durability in Practice
Durability is the ACID property that guarantees committed changes survive a crash. It is
the reason COMMIT takes a small but real amount of time.
The write-ahead log
When you commit, the database writes the changes to a transaction log
(also called a write-ahead log or WAL) and flushes it to disk before COMMIT returns.
The actual data files may not be updated yet — that happens later, in the background.
But the log has everything needed to reconstruct the changes.
If the server crashes right after COMMIT, the log is on disk. On restart, the database
replays the log, applying every committed transaction that had not yet made it into the
data files. The changes are not lost.
Why COMMIT is not instant
The flush to disk is the reason. Fast SSDs make it fast — often under a millisecond —
but "fast" is not "zero". On a system committing thousands of transactions per second,
the cost of that flush matters.
-- In many databases, you can loosen durability for speed
SET synchronous_commit = off; -- PostgreSQL
SET innodb_flush_log_at_trx_commit = 2; -- MySQL
Those settings trade durability for throughput. Committed changes might be lost if the
server crashes in the wrong window — usually a few milliseconds. Acceptable for logs,
metrics, session data. Not acceptable for financial transactions.
Redo and undo logs
Most databases maintain two logs. The redo log records changes that have
been committed but not yet written to the data files. On crash recovery, the redo log is
applied forward to bring the data files up to date.
The undo log records the previous version of each changed row, used to
support rollback and to show older versions of a row to transactions that need to see
them (for consistent reads under MVCC).
MVCC — multiversion concurrency control
Modern databases (PostgreSQL, Oracle, MySQL with InnoDB, SQL Server with snapshot
isolation) use a technique called MVCC. Instead of locking rows for reading, they keep
multiple versions of each row. When a transaction reads, it sees the version that was
current when the transaction started. When it writes, it creates a new version.
The benefit is that readers and writers do not block each other. A long-running read
query does not stop concurrent writes, and a write does not force reads to wait. That
was a major source of blocking in older database designs and is one of the reasons modern
databases handle more concurrency with the same hardware.
Best Practices for Transactional Consistency
Six habits make the difference between transactions that behave well under load and
transactions that become a source of bugs.
1. Keep transactions short
The longer a transaction runs, the longer it holds locks and the more likely it will
conflict with another transaction. Anything that does not need to be inside the
transaction should be outside it. Reading input files, calling external APIs,
formatting output — do that before you BEGIN.
2. Choose the right isolation level
Default to the database's default. Override only when you have a specific reason.
Serializable when correctness demands it, read-committed for most workloads,
repeatable-read when a transaction reads the same rows multiple times.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
3. Always handle errors and roll back on failure
BEGIN TRANSACTION;
BEGIN TRY
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
END TRY
BEGIN CATCH
ROLLBACK;
-- Log or re-raise the error
END CATCH;
Without the CATCH block, a failed statement leaves the transaction open, holding locks.
Any code path that might fail should have a rollback.
4. Do not wait for user input inside a transaction
A transaction waiting on a user click holds locks the whole time. If the user goes to
lunch, the transaction stays open, blocking other work. Collect all the input first,
then start the transaction, then commit immediately.
5. Use SAVEPOINTs for long operations with independent phases
BEGIN TRANSACTION;
-- Step 1
INSERT INTO orders (...) VALUES (...);
SAVEPOINT after_order;
-- Step 2
INSERT INTO order_items (...) VALUES (...);
SAVEPOINT after_items;
-- Step 3 — may fail without invalidating the earlier work
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 42;
-- If this fails, roll back to after_items and retry, keeping the order
COMMIT;
6. Batch commits where possible
One transaction per row is very inefficient. Group related changes into one transaction
and commit once. A batch load of ten thousand rows should be one transaction, not ten
thousand. The exception is very large batches — those should be split into chunks of a
few thousand rows each, so a failure does not roll back everything.
7. Monitor long-running transactions
Every production database has a way to see open transactions. Check regularly for
transactions that have been open for minutes or hours — those are usually bugs where a
transaction was started and never committed or rolled back.
-- PostgreSQL
SELECT pid, state, query, now() - xact_start AS duration
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;
20 Interview Questions and Answers
These are the transaction and ACID questions that come up most often. Every answer stands
on its own — read them in any order, or work through them in sequence to build up the
full picture.
/
🔍
No matching questions found
Try a keyword like COMMIT, ROLLBACK, or ACID.
A transaction is a group of SQL statements that succeed or fail together. Either
every statement takes effect, or none do. It is the mechanism that keeps a
multi-step change from leaving the database in an inconsistent state.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
That is a transfer of 500 from account 1 to account 2. Both updates happen, or
neither does. If the second UPDATE fails — say, because account 2 does not exist
— the first update is undone automatically. Account 1 does not lose the money
with nothing to show for it.
That is why transactions matter. Real systems have thousands of multi-step
operations — order placement, payment processing, inventory updates — where a
partial result would be worse than no result at all. Transactions give you the
guarantee that a step either fully completed or did not happen.
Under the hood, the database achieves this with logging and locking. Every change
is written to a log before it is applied. If the transaction rolls back, the log
is used to undo the changes. If it commits, the log is used to ensure the changes
survive a crash.
ACID is the acronym for the four properties a transaction must guarantee. Every
relational database worth trusting implements all four.
Atomicity — all statements in a transaction succeed or none do.
If any single statement fails, the whole transaction rolls back. This is the "all
or nothing" guarantee.
Consistency — a transaction moves the database from one valid
state to another. Constraints — primary keys, foreign keys, unique constraints,
check constraints — must hold before and after. A transaction that would violate
any rule fails and rolls back.
Isolation — concurrent transactions do not see each other's
uncommitted work. Whatever order they actually execute in, the visible outcome is
the same as if they had run one after the other.
Durability — once a transaction commits, the changes are
permanent. Even a crash immediately after COMMIT does not lose the data. The
database achieves this by writing changes to a log on disk before COMMIT returns.
Together they make a database reliable. Without atomicity, a crash mid-transaction
leaves partial changes. Without isolation, concurrent transactions see each
other's dirty data. Without durability, committed changes vanish on power loss.
The four properties cover the essential failure modes.
BEGIN TRANSACTION marks the start of a transaction. Every DML
statement after it becomes part of the same unit of work until you either COMMIT
or ROLLBACK.
BEGIN TRANSACTION;
INSERT INTO orders (order_id, customer_id, order_amount, order_status)
VALUES (1008, 'C001', 15000, 'Pending');
COMMIT;
That inserts one order. Without the BEGIN/COMMIT pair, the insert happens in
autocommit mode — the statement commits the moment it succeeds, and there is no
window to roll it back.
The exact keyword varies by database. SQL Server and PostgreSQL use
BEGIN TRANSACTION or just BEGIN. MySQL uses
START TRANSACTION. Oracle does not require an explicit BEGIN at all
— the first DML statement starts a transaction automatically.
Most databases default to autocommit on, which is why explicit transactions
matter. If you want multiple statements to succeed or fail together, you have to
wrap them in a transaction. Without the wrapper, each statement commits on its
own and there is no way to undo an earlier one when a later one fails.
COMMIT makes every change in the current transaction permanent.
After it runs, the changes are visible to other sessions and cannot be rolled
back.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
The two updates are now part of the database. Other users see the new balances.
The transaction log has the changes on disk. There is no undo.
Before COMMIT, the changes exist only in your session. If another user queries
accounts, they see the old values. You can still ROLLBACK. Once COMMIT runs, that
window closes.
When to commit: at the end of a logical unit of work, when everything has
succeeded and every constraint is satisfied. When not to commit: mid-way through a
multi-step operation that could still fail. If a downstream step fails, rolling
back is only possible if you have not already committed the earlier steps.
The one anti-pattern to avoid: committing inside a loop. Every COMMIT has real
cost — flushing to disk, updating the log. A loop that commits after every row
on a million-row operation runs a million commits. Batch the changes and commit
once at the end, or in chunks if the batch is very large.
ROLLBACK undoes every uncommitted change in the current transaction.
It is the undo button for the duration of the transaction — everything goes back
to how it was before BEGIN.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
ROLLBACK;
The two updates are undone. Account 1 has its original balance. Account 2 has its
original balance. Nothing changed.
The typical use is error handling. If a statement fails, or a business rule is
violated, or the user cancels, you roll back. The database returns to the state
it was in before the transaction started.
What gets rolled back: INSERT, UPDATE, DELETE, and MERGE. What does not: DDL
statements (they auto-commit on most databases), and sequence values (once
NEXTVAL is called, the value is consumed even if you roll back).
One important caveat: autocommit mode makes ROLLBACK useless. If every statement
commits itself immediately, there is nothing to roll back. This is why you have
to explicitly start a transaction — to have a window where ROLLBACK is meaningful.
SAVEPOINT marks a point inside a transaction you can return to with
ROLLBACK TO SAVEPOINT. It lets you undo part of a transaction without
discarding the whole thing.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
SAVEPOINT after_debit;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
SAVEPOINT after_credit;
UPDATE audit_log SET note = 'Transfer complete' WHERE transfer_id = 100;
-- Realise the audit update is wrong
ROLLBACK TO SAVEPOINT after_credit;
-- Fix it
UPDATE audit_log SET note = 'Transfer of 500 complete' WHERE transfer_id = 100;
COMMIT;
The final COMMIT keeps the two account updates and the corrected audit update.
The first audit update was discarded by the partial rollback.
What SAVEPOINT does not do: it does not commit the work before it (a final COMMIT
is still required), and it does not release locks (locks are held until the
transaction ends).
When to use SAVEPOINTs: long operations with independent phases. A batch import
that processes rows in chunks — one SAVEPOINT per chunk so a bad chunk can be
rolled back without losing the earlier chunks. A multi-step workflow where one
step is optional and might fail without aborting the rest.
Support is universal — Oracle, PostgreSQL, MySQL, and SQL Server all support
SAVEPOINT. SQL Server uses slightly different wording (SAVE TRANSACTION)
but the behaviour is the same.
Isolation levels control how much one transaction can see of another's
uncommitted work. Stronger isolation means fewer concurrency anomalies but more
blocking. Weaker isolation means faster concurrency but more chances of reading
inconsistent data.
The four levels are:
READ UNCOMMITTED — a transaction can see another transaction's uncommitted changes. Called "dirty reads".
READ COMMITTED — a transaction only sees committed changes. Prevents dirty reads. But the same query run twice can return different values if another transaction committed in between (non-repeatable read).
REPEATABLE READ — a transaction sees a consistent snapshot of the data from the start of the transaction. The same query returns the same rows every time. But new rows can still appear (phantom read).
SERIALIZABLE — the strictest level. Every transaction behaves as if it ran completely before or after every other transaction. Prevents all three anomalies, at the cost of concurrency.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
-- ...
COMMIT;
The default in most databases is READ COMMITTED. Oracle, PostgreSQL, and SQL
Server use it by default. MySQL defaults to REPEATABLE READ.
The choice matters most in multi-user environments. A financial application that
checks balances before making transfers usually wants REPEATABLE READ or
SERIALIZABLE. A read-heavy analytics workload might be fine with READ COMMITTED
or even READ UNCOMMITTED.
A lock is a mechanism — a specific resource the database prevents other
transactions from touching. An isolation level is a policy — a rule that decides
which locks the database takes on your behalf.
When you set an isolation level, the database then takes locks automatically to
enforce that level. You rarely write locks explicitly. The database decides when
to take a shared lock for a read or an exclusive lock for a write, based on the
isolation level and the operation.
-- Setting the isolation level
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Explicit lock on specific rows
SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE;
The first statement sets the policy. The second takes a specific lock. Both are
parts of the same toolkit — one is the automatic level, the other is manual
control.
When you actually need to write a lock explicitly: read-then-write patterns where
you want to make sure the value does not change between the read and the write.
SELECT ... FOR UPDATE takes an exclusive lock and prevents another
transaction from updating those rows until you commit.
Most applications never set an isolation level or write a lock. They use the
database default and let the automatic locking do its job. When they do need
more control, understanding the two concepts separately makes the choice easier.
Durability guarantees that once a transaction commits, the changes are permanent
even if the system crashes immediately after. The database achieves this by
writing the changes to a transaction log on disk before COMMIT returns.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
COMMIT; -- returns only after the change is safely on disk
The sequence inside COMMIT: the changes are written to the log, the log is
flushed to disk, and only then does COMMIT return success. The actual data files
may not be updated yet — that happens later in the background. But the log has
everything needed to reconstruct the changes.
If the server crashes right after COMMIT, the log is on disk. On restart, the
database reads the log and replays the committed transactions that had not yet
made it into the data files. The changes are not lost.
The flush is the reason COMMIT is not instant. Fast SSDs make it fast — often
under a millisecond — but the cost is real. On a system committing thousands of
transactions per second, the cost adds up.
Some databases let you loosen durability for speed. PostgreSQL has
synchronous_commit = off. MySQL has
innodb_flush_log_at_trx_commit = 2. Those settings trade durability
for throughput — committed changes might be lost if the server crashes in a small
window. Acceptable for logs and metrics; not for financial data.
Six habits cover most of what matters. None of them are complicated, and together
they prevent the majority of transactional bugs.
Keep transactions short. The longer a transaction runs, the
longer it holds locks and the more it blocks other work. Anything that does not
need to be inside the transaction — file reads, API calls, user interaction —
should happen before BEGIN.
Choose the right isolation level. Default to the database
default. Override only when you have a specific reason. Serializable for
financial correctness, read-committed for most workloads.
Always handle errors and roll back on failure. A failed statement
leaves the transaction open and holding locks. Any code path that might fail needs
a rollback.
BEGIN TRANSACTION;
BEGIN TRY
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
END TRY
BEGIN CATCH
ROLLBACK;
-- Log the error
END CATCH;
Do not wait for user input inside a transaction. A transaction
waiting on a user click holds locks the whole time. If the user goes to lunch,
the transaction stays open, blocking other work. Collect input first, then start
the transaction.
Use SAVEPOINTs for long operations with independent phases. When
a long transaction has several distinct steps and one might fail, SAVEPOINTs let
you roll back just the failed part without losing the earlier work.
Monitor long-running transactions. Every production database
exposes open transactions through a system view. Check regularly for transactions
that have been open for minutes or hours — those are usually bugs where a
transaction was started and never committed or rolled back.
A deadlock happens when two transactions each hold a lock the other needs. Neither
can proceed, and both wait forever. Something has to break the cycle, and the
database does it automatically.
-- Transaction A
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
-- Now waiting for account 2
-- Transaction B (concurrent)
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
-- Now waiting for account 1
-- A holds 1, wants 2. B holds 2, wants 1. Deadlock.
The database runs a deadlock detector — a background process that scans the lock
graph looking for cycles. When it finds one, it picks a victim transaction
(usually the one with the least work done) and rolls it back. The victim gets an
error; the other transaction proceeds as if nothing happened.
Common error messages: "deadlock detected" (PostgreSQL),
"transaction was deadlocked" (SQL Server), "ORA-00060: deadlock
detected while waiting for resource" (Oracle).
Preventing deadlocks: access resources in the same order across transactions
(always update account 1 before account 2), keep transactions short, use lower
isolation levels where possible.
Handling deadlocks in application code: catch the error and retry. A small random
delay between retries prevents two transactions from deadlocking again the same
way.
-- Pseudocode for a retry loop
attempt = 0
while attempt < 3:
try:
BEGIN TRANSACTION;
-- do work
COMMIT;
break;
except DeadlockError:
ROLLBACK;
attempt += 1
if attempt == 3: raise
sleep(random_delay)
Deadlocks are not a bug in your query — they are a fact of concurrent access.
Every production database has them. What matters is that the application knows
how to handle them gracefully.
Three specific anomalies the isolation levels are designed to prevent. Every
interview that covers transactions asks about them.
Dirty read. Transaction A reads data that Transaction B has
written but not committed. If B then rolls back, A has read data that never
existed. This is the worst anomaly because it produces data that is simply
wrong.
-- Transaction B
BEGIN TRANSACTION;
UPDATE accounts SET balance = 6000 WHERE account_id = 1;
-- not committed yet
-- Transaction A (concurrent)
SELECT balance FROM accounts WHERE account_id = 1;
-- With READ UNCOMMITTED: sees 6000 (dirty read)
-- With READ COMMITTED or higher: sees the old value
-- Transaction B
ROLLBACK;
-- The 6000 never existed
Non-repeatable read. Transaction A reads a row, Transaction B
updates and commits it, Transaction A reads the same row again and gets a
different value. The two reads in the same transaction disagree.
-- Transaction A
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 1; -- reads 10000
-- Transaction B (concurrent)
BEGIN TRANSACTION;
UPDATE accounts SET balance = 6000 WHERE account_id = 1;
COMMIT;
-- Transaction A
SELECT balance FROM accounts WHERE account_id = 1; -- reads 6000
-- Two reads, different values — non-repeatable read
Phantom read. Transaction A runs a query that returns five rows,
Transaction B inserts a row that matches the same filter and commits, Transaction
A runs the query again and gets six rows. A new row has appeared "out of nowhere".
-- Transaction A
BEGIN TRANSACTION;
SELECT COUNT(*) FROM orders WHERE order_status = 'Pending'; -- returns 5
-- Transaction B (concurrent)
INSERT INTO orders (order_id, order_status) VALUES (9999, 'Pending');
COMMIT;
-- Transaction A
SELECT COUNT(*) FROM orders WHERE order_status = 'Pending'; -- returns 6
-- A new row appeared — phantom read
The isolation levels prevent these anomalies in a specific order. READ COMMITTED
prevents dirty reads. REPEATABLE READ prevents dirty reads and non-repeatable
reads. SERIALIZABLE prevents all three. Higher isolation means fewer anomalies but
more blocking.
A lock is a mechanism the database uses to enforce isolation. Different types of
locks allow different levels of concurrent access.
Shared lock (S) — taken for reads. Multiple transactions can hold
shared locks on the same row, and any of them can read. But a shared lock blocks
any exclusive lock on the same row, which means it prevents writes.
Exclusive lock (X) — taken for writes. Only one transaction can
hold it, and it blocks both reads and writes from other transactions. When you
UPDATE a row, the database takes an exclusive lock on that row until you commit or
roll back.
Update lock (U) — an intermediate mode used when a transaction
intends to update a row but has not yet. It is a common in-between state to
prevent two transactions from both holding a shared lock and then both trying to
upgrade to exclusive, which would deadlock.
Locks can be taken at different granularities. Row-level locks are the most
granular and give the best concurrency. Page-level and table-level locks cover
more data but are cheaper to track. The database chooses based on the operation.
-- Explicit exclusive lock for a specific row
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE;
-- Now no other transaction can modify account 1
-- Update, then commit
COMMIT;
Locks are held until the transaction ends. Commit or rollback releases them. That
is why short transactions are important — a long-running transaction holds locks
that block everyone else.
When you see a query blocked for a long time, the reason is almost always a lock
held by another session. Finding and resolving that session is the standard
troubleshooting step.
MVCC stands for Multiversion Concurrency Control. It is the technique modern
databases use to let readers and writers work without blocking each other.
Without MVCC, a reader would need to take a shared lock on every row it reads.
That lock would block any writer trying to update the row, and the reader would
hold the lock for the duration of the transaction. Long reads block writes.
MVCC solves this by keeping multiple versions of each row. When a transaction
reads, it sees the version that was current when the transaction started. When a
writer updates a row, it creates a new version rather than overwriting the old
one. The old version remains visible to transactions that started before the
update.
-- Transaction A (started at time T1)
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 1; -- sees version at T1
-- Transaction B (started at time T2 > T1)
BEGIN TRANSACTION;
UPDATE accounts SET balance = 6000 WHERE account_id = 1;
COMMIT;
-- Transaction A
SELECT balance FROM accounts WHERE account_id = 1; -- still sees version at T1
COMMIT;
Transaction A sees the same value twice because MVCC gives it a consistent
snapshot. Transaction B's update does not block A's read, and A's read does not
block B's write. Both run at the same time without interfering.
MVCC is used by PostgreSQL, Oracle, MySQL (InnoDB storage engine), and SQL Server
(with snapshot isolation enabled). It is the main reason modern databases handle
concurrency much better than older designs.
The cost is storage. Old versions of rows have to be kept until no transaction
needs to see them. Databases run background "vacuum" processes to clean up old
versions. On a heavily-updated table, the cleanup has to keep up or the storage
grows.
Usually not. Most databases auto-commit DDL statements — CREATE, ALTER, DROP,
TRUNCATE. Once they run, they are permanent and cannot be rolled back as part of
the transaction that issued them.
BEGIN TRANSACTION;
UPDATE accounts SET balance = 0 WHERE account_id = 1; -- DML, can be rolled back
DROP TABLE accounts; -- DDL, auto-committed
ROLLBACK; -- undoes the UPDATE, but the DROP already happened
After that transaction, the UPDATE is rolled back, but the table is still
dropped. The ROLLBACK does not undo the DDL.
The exceptions are PostgreSQL and some configurations of other databases.
PostgreSQL supports transactional DDL — you can wrap CREATE TABLE, ALTER TABLE,
and DROP TABLE in a transaction and roll them back. That is a real advantage for
migrations, where a failed DDL can leave the schema in an inconsistent state on
other databases.
-- PostgreSQL: this works
BEGIN;
CREATE TABLE new_feature (...);
-- Something goes wrong
ROLLBACK;
-- The new table is gone
On Oracle, SQL Server, and MySQL, the same thing does not work. The CREATE TABLE
commits immediately, and the ROLLBACK only undoes the DML. This is one of the
reasons database migration tools are careful about DDL — they know it cannot be
undone and handle it accordingly.
The practical implication for developers: be extra careful with DDL statements in
production. A DROP TABLE inside a transaction on Oracle does not become safely
reversible just because a transaction is open. The drop commits itself.
A distributed transaction spans multiple databases or multiple resources — say
updating a row in database A and a row in database B as one atomic unit. It is
harder than a local transaction because the resources are independent and no
single process owns both.
The standard protocol is two-phase commit (2PC). One coordinator
process manages the transaction across resources. It works in two phases:
Prepare phase. The coordinator asks each resource to prepare to commit. Each resource locks the resources it needs and writes the changes to its log but does not commit. It responds with "ready" or "abort".
Commit phase. If every resource responded "ready", the coordinator tells all of them to commit. If any resource responded "abort", the coordinator tells all of them to roll back.
-- Conceptual two-phase commit
Coordinator: PREPARE to Database A
Database A: READY (locked, logged, not committed)
Coordinator: PREPARE to Database B
Database B: READY (locked, logged, not committed)
Coordinator: COMMIT to Database A
Database A: committed
Coordinator: COMMIT to Database B
Database B: committed
Two-phase commit gives distributed transactions ACID properties in the normal
case. The problem is when the coordinator crashes after asking for prepare but
before sending commit. The resources are stuck — they have the work logged and
locked but do not know whether to commit or abort. The transaction waits until
the coordinator recovers.
That is the practical cost of distributed transactions. They are slower, more
fragile, and often avoided in modern architectures in favour of sagas — a
pattern where each step has a compensating action that undoes it if a later step
fails.
Real-world distributed transactions appear in Oracle's distributed database
features, XA transactions in Java, and MSDTC on Windows. Modern microservices
architectures usually avoid them and use eventual consistency with sagas instead.
When a session disconnects — network drop, application crash, connection killed by
an administrator — the transaction is rolled back automatically. Whatever work the
session had done but not committed is discarded.
The database detects the disconnect, rolls back any open transactions that session
had, and releases the locks. Other sessions that were waiting on those locks
proceed as if the transaction had never started.
That behaviour is what makes long transactions dangerous on unreliable networks. A
transaction that has been running for thirty minutes and holds thousands of locks
can lose all that work with a single dropped connection. On a network that
occasionally drops connections, this is a real risk.
Practical mitigations. First, keep transactions short — the shorter they are, the
less work is lost on disconnect. Second, use SAVEPOINTs for long operations to
reduce the blast radius of a failure. Third, use explicit connection management
with reconnection logic so a transient network issue does not kill a large batch.
Two-phase commit and distributed transactions have similar issues. If the
coordinator disconnects mid-transaction, resources can be left in a prepared but
not committed state until the coordinator recovers.
From the application's perspective, a disconnect that rolls back work looks the
same as an explicit ROLLBACK. The client sees an error, the database is back to
its pre-transaction state, and the application can retry.
A session is a connection to the database. A transaction is a unit of work inside
a session. One session can have many transactions over its lifetime — one after
another — but a session has at most one open transaction at a time.
-- One session, three transactions
BEGIN TRANSACTION;
INSERT INTO customers (...) VALUES (...);
COMMIT;
BEGIN TRANSACTION;
UPDATE customers SET ... WHERE ...;
COMMIT;
BEGIN TRANSACTION;
DELETE FROM customers WHERE ...;
COMMIT;
That is one session running three transactions sequentially. The session holds the
connection; the transactions are the work done through it.
Session-level settings apply for the life of the connection. If you set the
isolation level in a session, it applies to every transaction in that session. If
you set a session variable, it persists until the connection closes.
Transaction-level settings apply only for that one transaction. You can override
the session's isolation level for a single transaction without affecting the rest
of the session.
-- Set session default
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Override for one transaction
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
-- ...
COMMIT;
The distinction matters when tuning. Session-level settings affect every query and
transaction that runs through the connection. Transaction-level settings affect
one unit of work. For a connection pool where sessions are reused across many
operations, session-level settings should be set carefully — they can affect
unrelated code that happens to use the same connection.
A long-running transaction is any transaction that stays open for minutes or
hours. It usually indicates a bug — a transaction was started and never committed
or rolled back. Sometimes it is intentional — a long batch job. Either way, it
causes problems.
The main problem is locks. Every write in a transaction holds an exclusive lock
until the transaction ends. A long-running transaction that has updated a few
thousand rows holds locks on all of them for the whole duration. Any other
session trying to update those rows blocks. The blocking cascades — session B
blocks on A's lock, session C blocks on B, and soon the whole system is waiting.
The second problem is undo storage. In databases that use MVCC, old versions of
rows have to be kept until no transaction needs to see them. A long-running
transaction forces old versions to be retained for the entire duration. On a
high-churn table, that can grow the storage dramatically.
Third, MVCC cleanup stalls. PostgreSQL's VACUUM process, MySQL's purge thread,
and equivalent processes in other databases cannot clean up old row versions
while a long-running transaction still needs them. The database accumulates
garbage that would normally be cleaned up.
To find long-running transactions, every database exposes them through a system
view:
-- PostgreSQL
SELECT pid, state, query, now() - xact_start AS duration
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;
When you find one that should not be running, the standard response is to kill
the session. That rolls the transaction back and releases the locks. The
application that owned the session sees an error and can retry.
Testing transactions is trickier than testing most SQL because the interesting
cases involve concurrent sessions. Three techniques cover most scenarios.
Single-session testing for atomicity. Open a transaction, run
your statements, then deliberately cause a failure (raise an error, violate a
constraint, disconnect the session). Verify that the database is unchanged after
the failure.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
-- Deliberately cause a failure
INSERT INTO accounts (account_id, holder_name, balance)
VALUES (1, 'Duplicate', 0); -- primary key violation
ROLLBACK;
-- Verify: account 1's balance is unchanged
Two-session testing for isolation. Open two connections to the
same database. From session A, start a transaction and update a row without
committing. From session B, try to read or update the same row. The behaviour you
see (blocking, seeing the old value, seeing the new value) tells you how the
isolation level is working.
-- Session A
BEGIN TRANSACTION;
UPDATE accounts SET balance = 6000 WHERE account_id = 1;
-- Don't commit yet
-- Session B (concurrent)
SELECT balance FROM accounts WHERE account_id = 1;
-- Under READ COMMITTED: sees the old value
-- Under READ UNCOMMITTED: sees 6000
-- Blocks if a lock is held and isolation requires it
Load testing for deadlocks. Deadlocks only appear under real
concurrency. To test them, run many concurrent sessions doing the same kind of
transaction and see whether deadlocks appear in the database log. Fixing
deadlocks usually means changing the order of access or adding retry logic, and
neither shows up without real load.
Automated test frameworks for transactions are limited. Most teams write custom
scripts that open multiple connections and exercise the concurrency cases
directly. The important thing is to have those tests, because the bugs they catch
(deadlocks, isolation violations, lost updates) are almost invisible in
single-user testing.
Mistakes I Keep Seeing in Interviews
These come up repeatedly. None of them are exotic, and every one of them is easy to
avoid once you know it is coming.
Mistake one: assuming autocommit is off. Most databases default to
autocommit on, which means every statement commits on its own and there is no window
for ROLLBACK. If you want transactional behaviour, you have to explicitly start a
transaction.
Mistake two: leaving transactions open across user interaction. A
transaction waiting on a user click holds locks the entire time. If the user goes to
lunch, the locks stay held. Collect all the input first, then start the transaction.
Mistake three: forgetting that DDL auto-commits. A CREATE TABLE or
DROP TABLE inside a transaction is not rolled back by ROLLBACK on most databases. Only
PostgreSQL supports transactional DDL.
Mistake four: assuming stronger isolation is always better. SERIALIZABLE
is the strictest, but it also costs the most concurrency. Most workloads work fine with
READ COMMITTED — reach for SERIALIZABLE only when correctness demands it.
Mistake five: committing inside a loop. Every COMMIT has real cost —
flushing to disk, updating the log. A million-row operation with a commit per row takes
hours. Batch the changes and commit once.
Mistake six: not handling deadlocks in application code. Deadlocks are
a fact of concurrent access, not a rare bug. Every application that writes to a
multi-user database needs retry logic for the deadlock error.
Follow-Up Questions Interviewers Ask
Getting the first answer right is only half the interview. What usually decides the
outcome is what happens next. Here is what to prepare for on the transaction topics.
After "What is a transaction?" — expect
"What is the difference between COMMIT and ROLLBACK?"
"Can you roll back a committed transaction?"
"What happens to an open transaction if the session disconnects?"
"What is autocommit mode?"
After "What is ACID?" — expect
"Which property is most often relaxed and why?"
"Give me an example of each property in action."
"How does a database guarantee durability?"
"What is the difference between consistency and durability?"
After "What are isolation levels?" — expect
"What is a dirty read?"
"What is a phantom read and which level prevents it?"
"What is the default isolation level in your database?"
"How does MVCC relate to isolation?"
After "What is a deadlock?" — expect
"How does the database detect a deadlock?"
"How do you prevent deadlocks?"
"What happens to the victim transaction?"
"How do you handle deadlocks in application code?"
For every question above, prepare a second-level answer. The candidates who pass are not
the ones who memorised the first answer — they are the ones who kept going for another
thirty seconds and said something useful about why it matters.
Quick Quiz
Three questions to check whether the topics above have landed.
1. What does the "A" in ACID stand for?
2. What does a dirty read mean?
3. What happens to an open transaction if the client disconnects?