Himanshu SharmaSoftware engineer
Let’s talk

MySQL · Lesson 39 of 50

Transactions

Group statements so they all succeed or all fail: ACID, START TRANSACTION, COMMIT, ROLLBACK, SAVEPOINT, isolation levels, locking reads and deadlocks.

18 minFaster and saferUpdated 22 October 2026

Awaiting technical review. This lesson is readable but not yet listed in search engines.

By the end of this lessonWrap related changes in a transaction, roll back mistakes, and explain ACID, isolation levels and row locks.

When things must happen together

A bank transfer takes 500 from Alice and adds 500 to Bob. That is two UPDATE statements. If the server crashes after the first, 500 has vanished. If two transfers run at the same moment, they may read the same balance and overwrite each other. A transaction solves both problems by treating a group of statements as one indivisible unit.

ACID in plain words

Transactions are described by four guarantees:

  • Atomicity: all the statements take effect, or none do.
  • Consistency: a transaction moves the database from one valid state to another. Constraints still apply.
  • Isolation: concurrent transactions do not see each other's unfinished work (to a degree you can choose).
  • Durability: once committed, data survives a crash or power failure.

MySQL's default engine, InnoDB, supports transactions. The older MyISAM engine does not.

A first transaction

Create a small accounts table:

sql
CREATE DATABASE IF NOT EXISTS practice;
USE practice;
DROP TABLE IF EXISTS bank_accounts;
CREATE TABLE bank_accounts (
  id      INT PRIMARY KEY,
  owner   VARCHAR(30) NOT NULL,
  balance DECIMAL(10,2) NOT NULL CHECK (balance >= 0)
);
INSERT INTO bank_accounts VALUES (1, 'Alice', 1000.00), (2, 'Bob', 300.00);

Now the transfer, as one transaction:

sql
START TRANSACTION;
UPDATE bank_accounts SET balance = balance - 500 WHERE id = 1;
UPDATE bank_accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;
SELECT * FROM bank_accounts;
result
+----+-------+---------+
| id | owner | balance |
+----+-------+---------+
|  1 | Alice |  500.00 |
|  2 | Bob   |  800.00 |
+----+-------+---------+

START TRANSACTION (or BEGIN) opens it. COMMIT makes both changes permanent at once. Until the commit, other sessions still see the old balances.

ROLLBACK

If something goes wrong, ROLLBACK undoes everything since the start:

sql
START TRANSACTION;
UPDATE bank_accounts SET balance = balance - 200 WHERE id = 1;
SELECT owner, balance FROM bank_accounts WHERE id = 1;
ROLLBACK;
SELECT owner, balance FROM bank_accounts WHERE id = 1;
result
+-------+---------+
| owner | balance |
+-------+---------+
| Alice |  300.00 |
+-------+---------+
+-------+---------+
| owner | balance |
+-------+---------+
| Alice |  500.00 |
+-------+---------+

Inside the transaction you saw the reduced balance. After the rollback it is back to normal. Applications use this on errors: if any step fails, roll back and report the problem. The CHECK constraint helps too. A transfer that would overdraw an account fails, and the rollback keeps the data consistent:

sql
START TRANSACTION;
UPDATE bank_accounts SET balance = balance + 900 WHERE id = 2;
UPDATE bank_accounts SET balance = balance - 900 WHERE id = 1;
ROLLBACK;
SELECT * FROM bank_accounts;
result
ERROR 3819 (HY000): Check constraint 'bank_accounts_chk_1' is violated.
+----+-------+---------+
| id | owner | balance |
+----+-------+---------+
|  1 | Alice |  500.00 |
|  2 | Bob   |  800.00 |
+----+-------+---------+

The second UPDATE failed with a constraint error (it would have made Alice's balance negative), so we rolled back the first. Bob's extra 900 did not survive.

Autocommit

By default MySQL runs in autocommit mode: every statement is its own transaction, committed immediately. That is why a plain UPDATE cannot be undone. START TRANSACTION suspends autocommit until COMMIT or ROLLBACK. You can also turn it off for the whole session:

sql
SELECT @@autocommit AS autocommit_is_on;
result
+------------------+
| autocommit_is_on |
+------------------+
|                1 |
+------------------+

Remember that closing a connection with an open transaction rolls it back. Also note that some statements, mainly DDL such as CREATE TABLE, ALTER TABLE and TRUNCATE, cause an implicit commit: they end the current transaction and cannot be rolled back.

Savepoints

A SAVEPOINT marks a point inside a transaction that you can return to without discarding everything:

sql
START TRANSACTION;
UPDATE bank_accounts SET balance = balance + 10 WHERE id = 1;
SAVEPOINT after_bonus;
UPDATE bank_accounts SET balance = balance + 9999 WHERE id = 1;
ROLLBACK TO SAVEPOINT after_bonus;
COMMIT;
SELECT owner, balance FROM bank_accounts WHERE id = 1;
result
+-------+---------+
| owner | balance |
+-------+---------+
| Alice |  510.00 |
+-------+---------+

Only the second update was undone; the first was kept and committed.

Isolation levels

When several transactions run at once, what may one see of another? SQL defines four isolation levels:

Level Dirty read Non-repeatable read Phantom read
READ UNCOMMITTED possible possible possible
READ COMMITTED no possible possible
REPEATABLE READ (InnoDB default) no no mostly no
SERIALIZABLE no no no

A dirty read sees data another transaction has not committed. A non-repeatable read gets a different value when it reads the same row twice because someone committed in between. A phantom is a new row that appears in a repeated range query. InnoDB's default, REPEATABLE READ, gives each transaction a consistent snapshot as of its first read.

sql
SELECT @@transaction_isolation AS isolation_level;
result
+-----------------+
| isolation_level |
+-----------------+
| REPEATABLE-READ |
+-----------------+

Higher isolation means more safety and less concurrency. Many applications choose READ COMMITTED for fewer locks; know what your framework sets.

Locking reads

Consider two staff who each read a stock of 10, subtract 1 and write back 9. One sale is lost. Locking the row on read prevents that:

sql
START TRANSACTION;
SELECT balance FROM bank_accounts WHERE id = 2 FOR UPDATE;
UPDATE bank_accounts SET balance = balance - 50 WHERE id = 2;
COMMIT;
result
+---------+
| balance |
+---------+
|  800.00 |
+---------+

SELECT ... FOR UPDATE locks the matching rows until the transaction ends, so a second session trying to lock the same row has to wait. (FOR SHARE allows other readers but blocks writers.) You can see this by opening two terminals: run the first two statements in one, then try the same FOR UPDATE in the other and watch it wait.

The cleanest approach for simple counters is still an atomic update: SET balance = balance - 50 WHERE id = 2 AND balance >= 50.

Deadlocks

If transaction A locks row 1 then wants row 2, while transaction B locks row 2 then wants row 1, they wait for each other forever. InnoDB detects this, rolls one back and reports ERROR 1213: Deadlock found when trying to get lock; try restarting transaction. The remedies: touch rows in the same order everywhere, keep transactions short, and let your application retry on error 1213.

Good habits

  • Keep transactions as short as possible. Do not wait for a user's input inside one.
  • Group only statements that must succeed together.
  • Handle failure: always roll back on errors.
  • Do not mix long reports with updates in one transaction.

Common mistakes

  • Assuming a transaction is open while autocommit has already committed each statement.
  • Forgetting to COMMIT, leaving locks held and other sessions waiting.
  • Putting DDL in a transaction and expecting a rollback.
  • Long transactions that hold locks and bloat the undo log.
  • Not retrying after a deadlock.

Exercise

Write a transaction that moves 100 from Bob to Alice, then verify the totals. Next, start a second transaction that adds 1,000,000 to Bob and roll it back, and confirm Bob's balance is unchanged.

Show one possible answer
sql
START TRANSACTION;
UPDATE bank_accounts SET balance = balance - 100 WHERE id = 2;
UPDATE bank_accounts SET balance = balance + 100 WHERE id = 1;
COMMIT;
SELECT SUM(balance) AS total_money FROM bank_accounts;

START TRANSACTION;
UPDATE bank_accounts SET balance = balance + 1000000 WHERE id = 2;
ROLLBACK;
SELECT owner, balance FROM bank_accounts ORDER BY id;
result
+-------------+
| total_money |
+-------------+
|     1260.00 |
+-------------+
+-------+---------+
| owner | balance |
+-------+---------+
| Alice |  610.00 |
| Bob   |  650.00 |
+-------+---------+

Key points

  • A transaction groups statements into one unit: COMMIT makes them all permanent, ROLLBACK undoes them all.
  • ACID: Atomic (all or nothing), Consistent (rules hold), Isolated (transactions do not see each other's half-done work) and Durable (committed data survives a crash).
  • MySQL runs in autocommit mode by default, so each statement is its own transaction until you START TRANSACTION.
  • SELECT ... FOR UPDATE locks the rows you read, which prevents lost updates. Keep transactions short to avoid blocking and deadlocks.

Check yourself

1. What does ROLLBACK do?
2. By default, what happens when you run a single UPDATE outside START TRANSACTION in MySQL?
3. Which property means a committed transaction survives a power cut?
4. A bank transfer subtracts from account A and adds to account B. Why use a transaction?