SQLab Hub
Lesson 33 · Transactions
Lesson 33 · Advanced

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.

⇄ BEGIN · COMMIT · ROLLBACK ACID · isolation levels · locks 🧪 4 practice exercises 📝 35-question quiz ⏱ ~30 min

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:

SQL · run in a local database
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:

SQLite · runs in the Playground
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

Balances before, after COMMIT and after ROLLBACK
account_idownerbeforeafter_commitafter_rollback
1Neha200015002000
2Vikram150020001500
3Aditi800800800
✓ 3 rows · real output from the Playground database

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.

The SQLabHub Playground only runs SELECT and WITH queries, so BEGIN, UPDATE, COMMIT and ROLLBACK cannot be tried there. The statements on this page work in SQLite, PostgreSQL, MySQL and SQL Server; the small differences are listed in the syntax section.

BEGIN, COMMIT and ROLLBACK

StatementWhat it does
BEGIN / BEGIN TRANSACTION / START TRANSACTIONStarts a transaction
COMMITMakes every change since BEGIN permanent and visible to others
ROLLBACKUndoes every change since BEGIN
SAVEPOINT nameMarks a point inside the transaction
ROLLBACK TO SAVEPOINT nameUndoes changes made after that point; the transaction stays open
RELEASE SAVEPOINT nameRemoves the savepoint; the changes stay
DatabaseStartAuto-commit by default?
SQLiteBEGINYes, each statement is its own transaction
PostgreSQLBEGIN or START TRANSACTIONYes
MySQL (InnoDB)START TRANSACTION or BEGINYes (SET autocommit = 0 turns it off)
SQL ServerBEGIN TRANSACTIONYes
OracleStarts implicitly with the first changeNo; 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

SQL · run in a local database
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
SQL Server · error handling with TRY / CATCH
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:

SQL · run in a local database
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

PropertyGuaranteeIn the transfer example
AtomicityAll statements take effect, or none doNever a debit without the matching credit
ConsistencyThe database moves from one valid state to another; constraints holdA CHECK (balance >= 0) rule rejects the transfer if it would overdraw
IsolationConcurrent transactions do not see each other's unfinished workA report running meanwhile sees the money in one account or the other, never missing
DurabilityOnce committed, changes survive a crash or power lossAfter 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:

SQLite · runs in the Playground
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 whose subtotal matches quantity times price
order_lineslines_consistent
496496
✓ 1 row · real output from the Playground database

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:

AnomalyWhat happens
Dirty readYou read a change another transaction has not committed yet, and it is later rolled back
Non-repeatable readYou read a row twice in one transaction and get different values, because someone committed an update in between
Phantom readYou run the same range query twice and new rows appear, inserted and committed by someone else
Lost updateTwo transactions read the same value, both update it, and the second overwrites the first
Isolation levelDirty readNon-repeatable readPhantom read
READ UNCOMMITTEDPossiblePossiblePossible
READ COMMITTEDPreventedPossiblePossible
REPEATABLE READPreventedPreventedPossible in the standard (prevented in practice by PostgreSQL and MySQL InnoDB)
SERIALIZABLEPreventedPreventedPrevented

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:

SQL · run in a local database
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

LockAllows others toTaken by
Shared (read) lockRead, but not writeReads at stricter isolation levels
Exclusive (write) lockNeither read with a lock nor writeUPDATE, 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:

SQL · run in a local database
-- 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.

SQL · how a deadlock forms
-- 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:

PhaseWhat happens
1. PrepareThe coordinator asks every participant to get ready. Each one writes its changes durably and answers "yes, I can commit" or "no"
2. CommitIf 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

❌ Each statement commits on its own
SQL
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;
✅ One unit of work
SQL
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

❌ Locks held indefinitely
SQL
BEGIN;
UPDATE orders SET status = 'shipped' WHERE id = 42;
-- session left open
✅ Always end the transaction
SQL
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

❌ Waiting for a person inside BEGIN
SQL
BEGIN;
SELECT ... FOR UPDATE;
-- wait for the user to confirm on screen
UPDATE ...;
COMMIT;
✅ Gather input first
SQL
-- 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

❌ Email sent, then rolled back
SQL
BEGIN;
INSERT INTO orders ...;
-- application sends "order confirmed" email
UPDATE stock ...;   -- fails, ROLLBACK
✅ Act after COMMIT
SQL
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

❌ Deadlock-prone
SQL
-- one code path updates account 2 then 1,
-- another updates 1 then 2
✅ Consistent order
SQL
-- always lock the lower account_id first

Opposite orders are the classic cause of deadlocks.

6. Check, then update, without a lock

❌ Lost update / overdraft
SQL
SELECT balance FROM accounts WHERE account_id = 1;  -- 600
-- another session withdraws 400 here
UPDATE accounts SET balance = 600 - 500 WHERE account_id = 1;
✅ Lock or check in the UPDATE
SQL
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

TaskStatement
StartBEGIN; (SQL Server: BEGIN TRANSACTION;, MySQL: START TRANSACTION;)
SaveCOMMIT;
UndoROLLBACK;
Partial undoSAVEPOINT s1; ... ROLLBACK TO SAVEPOINT s1;
IsolationSET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Lock rows you will updateSELECT ... 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.

  1. 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_items

    Show solution
    SQLite · runs in the Playground
    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.

  2. 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
    SQLite · runs in the Playground
    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.

  3. 3Medium Check that every order has at least one order line, which an order-creation transaction should guarantee. Table: orders, order_items

    Show solution
    SQLite · runs in the Playground
    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.

  4. 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
    SQLite · runs in the Playground
    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

Question 1 of 35

What is a transaction in SQL?

Question 2 of 35

Which statement typically starts a transaction?

Question 3 of 35

What does COMMIT do?

Question 4 of 35

What does ROLLBACK do?

Question 5 of 35

What does ACID stand for?

Question 6 of 35

What does atomicity guarantee?

Question 7 of 35

What does consistency guarantee?

Question 8 of 35

What does isolation guarantee?

Question 9 of 35

What does durability guarantee?

Question 10 of 35

What is a savepoint for?

Question 11 of 35

What does ROLLBACK TO SAVEPOINT do?

Question 12 of 35

What is a dirty read?

Question 13 of 35

What is a non-repeatable read?

Question 14 of 35

What is a phantom read?

Question 15 of 35

What is a lost update?

Question 16 of 35

Which isolation level allows dirty reads?

Question 17 of 35

Which level prevents dirty reads but still allows non-repeatable reads?

Question 18 of 35

Which level guarantees a row read once stays the same for the rest of the transaction?

Question 19 of 35

Which level is the strictest?

Question 20 of 35

What is a shared lock?

Question 21 of 35

What is an exclusive lock?

Question 22 of 35

What is a deadlock?

Question 23 of 35

How does a database resolve a deadlock?

Question 24 of 35

What reduces deadlocks?

Question 25 of 35

What is two-phase commit used for?

Question 26 of 35

What happens in the prepare phase?

Question 27 of 35

What is a transaction log (write-ahead log) for?

Question 28 of 35

What does a database do with its log after a crash?

Question 29 of 35

What is a distributed transaction?

Question 30 of 35

How do you safely "check the balance, then debit" with concurrent users?

Question 31 of 35

What is MVCC?

Question 32 of 35

Why keep transactions short?

Question 33 of 35

What is RELEASE SAVEPOINT for?

Question 34 of 35

Why not send a confirmation email before COMMIT?

Question 35 of 35

What is a compensating transaction?

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.