SQL Transactions: COMMIT, ROLLBACK and ACID Explained
A transaction groups several SQL statements so that they all take effect together, or none of them do. Moving money between two accounts is the classic case: the debit and the credit must both happen, or neither. COMMIT makes the changes permanent; ROLLBACK undoes them.
What Is a Transaction?
Transferring 500 from account 1 to account 2 takes two UPDATE statements. If the server fails after the first one, account 1 has lost 500 and account 2 never received it. A transaction removes that risk:
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
Until COMMIT, the changes are provisional. If anything goes wrong, a ROLLBACK (or a crash) returns both accounts to where they were. Other sessions never see the half-finished state with the money missing.
You can see the two possible end states with a SELECT on sample balances. This runs in the Playground; the three accounts are written into the query:
WITH accounts(account_id, owner, balance) AS (
VALUES (1, 'Neha', 2000), (2, 'Vikram', 1500), (3, 'Aditi', 800)
)
SELECT account_id, owner,
balance AS before,
balance + CASE account_id WHEN 1 THEN -500 WHEN 2 THEN 500 ELSE 0 END AS after_commit,
balance AS after_rollback
FROM accounts;
▶ Run this query in the SQL Playground
| account_id | owner | before | after_commit | after_rollback |
|---|---|---|---|---|
| 1 | Neha | 2000 | 1500 | 2000 |
| 2 | Vikram | 1500 | 2000 | 1500 |
| 3 | Aditi | 800 | 800 | 800 |
The total across the accounts is the same before and after the commit. Keeping rules like that true, even when something fails halfway, is what transactions are for.
BEGIN, COMMIT and ROLLBACK
| Statement | What it does |
|---|---|
BEGIN / BEGIN TRANSACTION / START TRANSACTION | Starts a transaction |
COMMIT | Makes every change since BEGIN permanent and visible to others |
ROLLBACK | Undoes every change since BEGIN |
SAVEPOINT name | Marks a point inside the transaction |
ROLLBACK TO SAVEPOINT name | Undoes changes made after that point; the transaction stays open |
RELEASE SAVEPOINT name | Removes the savepoint; the changes stay |
| Database | Start | Auto-commit by default? |
|---|---|---|
| SQLite | BEGIN | Yes, each statement is its own transaction |
| PostgreSQL | BEGIN or START TRANSACTION | Yes |
| MySQL (InnoDB) | START TRANSACTION or BEGIN | Yes (SET autocommit = 0 turns it off) |
| SQL Server | BEGIN TRANSACTION | Yes |
| Oracle | Starts implicitly with the first change | No; you must COMMIT |
In auto-commit mode every single statement is committed as soon as it finishes. That is why a two-statement transfer needs an explicit BEGIN.
ROLLBACK on error
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
-- the application checks the result
-- (e.g. the balance went negative, or the second account does not exist)
ROLLBACK; -- account 1 is back to its original balance
BEGIN TRY
BEGIN TRANSACTION;
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
IF @@TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
Savepoints
A savepoint lets you undo part of a transaction. Here an order is created, the optional gift wrap step fails, and only that step is undone:
BEGIN;
INSERT INTO orders (id, user_id, total, status) VALUES (501, 4, 120.00, 'pending');
INSERT INTO order_items (order_id, product_id, quantity, unit_price, subtotal)
VALUES (501, 17, 2, 60.00, 120.00);
SAVEPOINT before_gift_wrap;
UPDATE orders SET total = total + 5 WHERE id = 501; -- gift wrap fee
-- the gift wrap service is unavailable:
ROLLBACK TO SAVEPOINT before_gift_wrap;
COMMIT; -- the order and its item are saved, without the fee
Savepoints work in SQLite, PostgreSQL, MySQL and Oracle. SQL Server writes SAVE TRANSACTION name and ROLLBACK TRANSACTION name.
The ACID Properties
| Property | Guarantee | In the transfer example |
|---|---|---|
| Atomicity | All statements take effect, or none do | Never a debit without the matching credit |
| Consistency | The database moves from one valid state to another; constraints hold | A CHECK (balance >= 0) rule rejects the transfer if it would overdraw |
| Isolation | Concurrent transactions do not see each other's unfinished work | A report running meanwhile sees the money in one account or the other, never missing |
| Durability | Once committed, changes survive a crash or power loss | After COMMIT returns, the transfer is on disk |
Durability is usually built on a write-ahead log: changes are written to a log file before the data files. After a crash the database replays committed work from the log and discards what was not committed.
Consistency rules a transaction protects
Many rules span several rows, which is exactly why multi-statement changes need a transaction. One such rule in the Playground: each order line's subtotal equals its quantity times its unit price. Any code that changes a quantity must change the subtotal in the same transaction. Checking it:
SELECT COUNT(*) AS order_lines,
SUM(ABS(subtotal - quantity * unit_price) < 0.01) AS lines_consistent
FROM order_items;
▶ Run this query in the SQL Playground
| order_lines | lines_consistent |
|---|---|
| 496 | 496 |
All 496 lines pass. A query like this is a useful regular check on real data: if it ever finds a mismatch, some code updated one column without the other.
Isolation Levels and Read Problems
Full isolation is expensive, so databases let you choose how much two concurrent transactions may affect each other. Weaker levels allow these anomalies:
| Anomaly | What happens |
|---|---|
| Dirty read | You read a change another transaction has not committed yet, and it is later rolled back |
| Non-repeatable read | You read a row twice in one transaction and get different values, because someone committed an update in between |
| Phantom read | You run the same range query twice and new rows appear, inserted and committed by someone else |
| Lost update | Two transactions read the same value, both update it, and the second overwrites the first |
| Isolation level | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
| READ UNCOMMITTED | Possible | Possible | Possible |
| READ COMMITTED | Prevented | Possible | Possible |
| REPEATABLE READ | Prevented | Prevented | Possible in the standard (prevented in practice by PostgreSQL and MySQL InnoDB) |
| SERIALIZABLE | Prevented | Prevented | Prevented |
Defaults differ: READ COMMITTED in PostgreSQL, SQL Server and Oracle; REPEATABLE READ in MySQL InnoDB; SERIALIZABLE in SQLite, which allows only one writer at a time. Set the level per transaction:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- PostgreSQL (inside the transaction), SQL Server, MySQL (before it)
BEGIN;
...
COMMIT;
PostgreSQL, Oracle and MySQL implement isolation with MVCC (multi-version concurrency control): writers create new row versions, and readers see a consistent snapshot without waiting for locks.
Locks and Deadlocks
| Lock | Allows others to | Taken by |
|---|---|---|
| Shared (read) lock | Read, but not write | Reads at stricter isolation levels |
| Exclusive (write) lock | Neither read with a lock nor write | UPDATE, DELETE, INSERT, SELECT ... FOR UPDATE |
Preventing a lost update
Check-then-update logic, such as "is there enough balance? then debit", is unsafe if another transaction can change the balance between the two steps. Lock the row when you read it, or let the UPDATE itself check the condition:
-- PostgreSQL, MySQL, Oracle
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1 FOR UPDATE; -- row is locked
-- application checks the balance
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
COMMIT;
-- or in one statement: only debits if the money is there
UPDATE accounts
SET balance = balance - 500
WHERE account_id = 1 AND balance >= 500; -- check the affected row count
Deadlocks
A deadlock happens when transaction A holds a lock B needs, while B holds a lock A needs. Neither can continue. The database detects the cycle and rolls one of them back (the "victim"), which then gets an error and should be retried.
-- Transaction A -- Transaction B
UPDATE accounts ... WHERE account_id = 1; UPDATE accounts ... WHERE account_id = 2;
UPDATE accounts ... WHERE account_id = 2; UPDATE accounts ... WHERE account_id = 1;
-- A waits for B's lock on 2, -- B waits for A's lock on 1: deadlock
To avoid them: touch rows and tables in the same order in every transaction (for example lowest account_id first), keep transactions short, and index the columns in WHERE so fewer rows are locked.
Distributed Transactions and Two-Phase Commit
When one unit of work spans two databases, for example an order database and a separate billing database, a single COMMIT is not enough. Two-phase commit (2PC) coordinates it:
| Phase | What happens |
|---|---|
| 1. Prepare | The coordinator asks every participant to get ready. Each one writes its changes durably and answers "yes, I can commit" or "no" |
| 2. Commit | If every answer was yes, the coordinator tells all to commit; if any said no, all roll back |
2PC is slow and blocks if the coordinator fails at the wrong moment, so many modern systems avoid it. Instead they use a sequence of local transactions with compensating transactions to undo earlier steps if a later one fails, for example issuing a refund rather than un-charging a card.
Common Transaction Mistakes
Six errors and how to avoid them.
1. Forgetting BEGIN in auto-commit mode
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
-- crash here: the debit is already committed
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
Without BEGIN, the first UPDATE is permanent as soon as it finishes.
2. Forgetting to COMMIT or ROLLBACK
BEGIN;
UPDATE orders SET status = 'shipped' WHERE id = 42;
-- session left open
BEGIN;
UPDATE orders SET status = 'shipped' WHERE id = 42;
COMMIT;
An open transaction keeps its locks, blocks others and, in some databases, prevents old row versions from being cleaned up.
3. Long transactions
BEGIN;
SELECT ... FOR UPDATE;
-- wait for the user to confirm on screen
UPDATE ...;
COMMIT;
-- collect the confirmation first
BEGIN;
UPDATE ... WHERE ... AND version = 7; -- detect concurrent changes
COMMIT;
Locks are held for the whole transaction. Keep only the database work inside it.
4. Side effects before COMMIT
BEGIN;
INSERT INTO orders ...;
-- application sends "order confirmed" email
UPDATE stock ...; -- fails, ROLLBACK
BEGIN;
INSERT INTO orders ...;
UPDATE stock ...;
COMMIT;
-- now send the email
A rollback cannot undo an email, a payment call or a file write. Do those after the commit succeeds.
5. Locking rows in different orders
-- one code path updates account 2 then 1,
-- another updates 1 then 2
-- always lock the lower account_id first
Opposite orders are the classic cause of deadlocks.
6. Check, then update, without a lock
SELECT balance FROM accounts WHERE account_id = 1; -- 600
-- another session withdraws 400 here
UPDATE accounts SET balance = 600 - 500 WHERE account_id = 1;
UPDATE accounts
SET balance = balance - 500
WHERE account_id = 1 AND balance >= 500;
Computing the new value from a stale read overwrites the other change. Let the UPDATE work from the current value and enforce the condition.
Transactions Cheat Sheet
| Task | Statement |
|---|---|
| Start | BEGIN; (SQL Server: BEGIN TRANSACTION;, MySQL: START TRANSACTION;) |
| Save | COMMIT; |
| Undo | ROLLBACK; |
| Partial undo | SAVEPOINT s1; ... ROLLBACK TO SAVEPOINT s1; |
| Isolation | SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; |
| Lock rows you will update | SELECT ... FOR UPDATE; (PostgreSQL, MySQL, Oracle) |
Practice Transactions Queries
4 exercises on the real Playground tables.
Write each query yourself first. Every solution has been run against the Playground database.
-
1Easy Find any order lines whose subtotal does not equal quantity times unit price (the rule a transaction should protect). How many are there? Table:
order_itemsShow solution
SELECT COUNT(*) AS inconsistent_lines FROM order_items WHERE ABS(subtotal - quantity * unit_price) >= 0.01;▶ Run this query in the SQL Playground
The answer is 0, which is what you want from such a check. ABS with a small tolerance avoids false alarms from rounding.
-
2Medium Using the sample accounts, show which accounts could afford a 900 transfer out, using the same condition a safe UPDATE would use. Table:
accounts (sample)Show solution
WITH accounts(account_id, owner, balance) AS ( VALUES (1, 'Neha', 2000), (2, 'Vikram', 1500), (3, 'Aditi', 800) ) SELECT account_id, owner, balance, CASE WHEN balance >= 900 THEN 'debit allowed' ELSE 'rejected' END AS outcome FROM accounts;▶ Run this query in the SQL Playground
UPDATE ... WHERE account_id = ? AND balance >= 900 would change one row for Neha and Vikram and zero rows for Aditi.
-
3Medium Check that every order has at least one order line, which an order-creation transaction should guarantee. Table:
orders, order_itemsShow solution
SELECT COUNT(*) AS orders_without_lines FROM orders o WHERE NOT EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = o.id);▶ Run this query in the SQL Playground
Creating the order and its lines in one transaction means an order can never exist without lines.
-
4Medium Using the sample accounts, confirm that a 500 transfer from account 1 to 2 keeps the total balance unchanged. Table:
accounts (sample)Show solution
WITH accounts(account_id, balance) AS ( VALUES (1, 2000), (2, 1500), (3, 800) ) SELECT SUM(balance) AS total_before, SUM(balance + CASE account_id WHEN 1 THEN -500 WHEN 2 THEN 500 ELSE 0 END) AS total_after FROM accounts;▶ Run this query in the SQL Playground
Atomicity is what keeps this invariant true: either both changes happen or neither.
📝 Quiz — Transactions
Test your understanding · 0/35 answered
What is a transaction in SQL?
All or nothing.
Which statement typically starts a transaction?
The exact keyword depends on the database.
What does COMMIT do?
After COMMIT, other sessions can see the changes.
What does ROLLBACK do?
The data returns to how it was before BEGIN.
What does ACID stand for?
The four guarantees of a transaction.
What does atomicity guarantee?
No half-finished transfers.
What does consistency guarantee?
Constraints and rules hold at commit.
What does isolation guarantee?
Isolation levels tune how strict this is.
What does durability guarantee?
Usually through a write-ahead log.
What is a savepoint for?
SAVEPOINT name; ... ROLLBACK TO SAVEPOINT name;
What does ROLLBACK TO SAVEPOINT do?
You still need COMMIT or ROLLBACK afterwards.
What is a dirty read?
Only possible at READ UNCOMMITTED.
What is a non-repeatable read?
Prevented from REPEATABLE READ upwards.
What is a phantom read?
Prevented at SERIALIZABLE.
What is a lost update?
Prevent it with locking or by updating from the current value.
Which isolation level allows dirty reads?
The weakest level.
Which level prevents dirty reads but still allows non-repeatable reads?
The default in PostgreSQL, SQL Server and Oracle.
Which level guarantees a row read once stays the same for the rest of the transaction?
The default in MySQL InnoDB.
Which level is the strictest?
Transactions behave as if run one after another.
What is a shared lock?
Also called a read lock.
What is an exclusive lock?
Taken by UPDATE, DELETE and SELECT ... FOR UPDATE.
What is a deadlock?
Neither can continue on its own.
How does a database resolve a deadlock?
The victim gets an error and should retry.
What reduces deadlocks?
Consistent order removes the cycle.
What is two-phase commit used for?
Prepare, then commit.
What happens in the prepare phase?
Commit follows only if all say yes.
What is a transaction log (write-ahead log) for?
It underpins durability.
What does a database do with its log after a crash?
That restores a consistent state.
What is a distributed transaction?
Coordinated with 2PC or replaced by compensating steps.
How do you safely "check the balance, then debit" with concurrent users?
Otherwise another session can change the balance in between.
What is MVCC?
Used by PostgreSQL, Oracle and MySQL InnoDB.
Why keep transactions short?
Do only database work inside them.
What is RELEASE SAVEPOINT for?
The transaction continues.
Why not send a confirmation email before COMMIT?
Do side effects after the commit.
What is a compensating transaction?
Used where a single atomic transaction is not possible.
Transactions Interview Questions
Q1. Explain ACID with an example.
In a bank transfer, atomicity means the debit and credit both happen or neither does; consistency means rules such as "no negative balance" still hold afterwards; isolation means a report running at the same time never sees the money missing; durability means once COMMIT returns, a crash will not lose the transfer.
Q2. What is the difference between COMMIT and ROLLBACK?
COMMIT makes the transaction's changes permanent and visible to others. ROLLBACK discards them and returns the data to its state at BEGIN.
Q3. What isolation level would you use, and why?
Usually the database default (READ COMMITTED in PostgreSQL and SQL Server). Raise it to REPEATABLE READ or SERIALIZABLE for logic that reads and then writes based on what it read, such as balance checks, and be ready to retry on serialization errors.
Q4. How do you prevent a lost update?
Lock the row when reading (SELECT ... FOR UPDATE), or update relative to the current value with the condition in WHERE (SET balance = balance - 500 WHERE id = 1 AND balance >= 500), or use optimistic locking with a version column.
Q5. What causes deadlocks and how do you reduce them?
Two transactions locking the same resources in opposite orders. Lock in a consistent order, keep transactions short, index the columns used to find rows, and retry the transaction that was chosen as the victim.
Q6. What happens to an uncommitted transaction if the server crashes?
On restart the database uses its log to undo it. Only committed transactions survive.
Frequently Asked Questions
What is a transaction in SQL?
A transaction is a unit of work made of one or more SQL statements that either all succeed together (COMMIT) or all fail together (ROLLBACK).
Why do we use transactions?
To make sure related changes, such as debiting one account and crediting another, either both happen or neither does, so the data never ends up half-updated.
What are the ACID properties in SQL?
Atomicity (all or nothing), Consistency (the database moves from one valid state to another), Isolation (concurrent transactions do not see each other's unfinished work) and Durability (committed changes survive a crash).
What does COMMIT do in SQL?
COMMIT makes every change made in the current transaction permanent and visible to other sessions, and ends the transaction.
What does ROLLBACK do in SQL?
ROLLBACK undoes every change made in the current transaction and returns the data to the state it was in before the transaction began.
What is a SAVEPOINT in SQL?
A savepoint marks a point inside a transaction. ROLLBACK TO SAVEPOINT undoes only the changes made after it, without ending the transaction.
How do transactions ensure data consistency?
Atomicity means multi-row changes are applied completely or not at all, and constraints are checked so the data is valid when the transaction commits. Isolation stops other sessions from acting on half-finished work.
Are transactions important in SQL interviews?
Yes. ACID, COMMIT versus ROLLBACK, savepoints, isolation levels, read anomalies such as dirty and phantom reads, and deadlocks are common interview questions.