Skip to main content

🔒 Lesson 18: Transactions

Imagine a customer places an order: you INSERT the order, INSERT the order items, and UPDATE the inventory. But what if the server crashes after the order is inserted but before the inventory is updated? You'd have an order with no inventory deduction — or worse, deducted inventory with no order. A transaction groups these operations into an all-or-nothing unit: either everything succeeds together, or everything is rolled back as if nothing happened.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain the ACID properties of transactions
  • Use START TRANSACTION, COMMIT, and ROLLBACK
  • Create and roll back to SAVEPOINTs
  • Understand autocommit mode and how to control it
  • Use transactions inside stored procedures
  • Recognize common transaction patterns and pitfalls

Estimated Time: 45 minutes

Prerequisites: Stored Procedures & Functions (Lesson 17)

📑 In This Lesson

Why Transactions?

Many real-world database operations involve multiple steps that must succeed or fail as a unit:

  • Bank transfer: Debit Account A and credit Account B — both must happen, or neither
  • Order placement: Create the order, add line items, update inventory — all or nothing
  • User registration: Create the user account, create their profile, send a welcome email record — if any step fails, clean up

Without transactions, a failure midway through leaves your data in an inconsistent state — some steps completed, others not. Transactions prevent this.

graph TD subgraph tx["Transaction"] S1["Step 1: INSERT order"] --> S2["Step 2: INSERT order_items"] S2 --> S3["Step 3: UPDATE inventory"] end S3 -->|"All succeed"| C["COMMIT ✅
Changes saved permanently"] S2 -->|"Step 3 fails"| R["ROLLBACK ❌
All changes undone"] S1 -->|"Step 2 fails"| R

The ACID Properties

Transactions guarantee four properties, known by the acronym ACID:

Property Meaning Example
Atomicity All operations succeed or all are rolled back — no partial completion A bank transfer either moves money or doesn't — it can't debit without crediting
Consistency The database moves from one valid state to another — all constraints are maintained Account balances can't go negative if you have a CHECK constraint
Isolation Concurrent transactions don't interfere with each other Two people buying the last item — only one gets it, not both
Durability Once committed, data survives crashes, power failures, etc. After COMMIT, even a server restart won't lose the data

💡 InnoDB Required

ACID transactions require the InnoDB storage engine, which is MySQL's default since version 5.5. The older MyISAM engine does not support transactions. You can check a table's engine with: SHOW TABLE STATUS LIKE 'table_name';

Basic Transaction Syntax


-- Start a transaction
START TRANSACTION;

-- Do your work (INSERT, UPDATE, DELETE)
INSERT INTO orders (customer_id, order_date) VALUES (1, CURRENT_DATE);
INSERT INTO order_items (order_id, book_id, quantity) VALUES (LAST_INSERT_ID(), 1, 2);

-- If everything worked:
COMMIT;

-- OR if something went wrong:
ROLLBACK;
                

Between START TRANSACTION and COMMIT (or ROLLBACK), your changes are temporary. Other database connections can't see them yet. Only after COMMIT do they become permanent and visible.

💡 BEGIN vs START TRANSACTION

Both work identically in MySQL:


START TRANSACTION;   -- Standard SQL syntax
BEGIN;               -- MySQL alias (same thing)
BEGIN WORK;          -- Another alias (same thing)
                    

START TRANSACTION is the SQL standard and slightly more explicit. Both are fine.

COMMIT — Make It Permanent

COMMIT tells MySQL: "Everything since START TRANSACTION looks good — save it permanently."


START TRANSACTION;

-- Create a new order for Alice (customer_id = 1)
INSERT INTO orders (customer_id, order_date)
VALUES (1, '2026-04-16');

-- Get the new order's ID
SET @new_order_id = LAST_INSERT_ID();

-- Add items to the order
INSERT INTO order_items (order_id, book_id, quantity)
VALUES (@new_order_id, 1, 1);    -- 1 copy of Dune

INSERT INTO order_items (order_id, book_id, quantity)
VALUES (@new_order_id, 2, 3);    -- 3 copies of 1984

-- Everything succeeded — commit!
COMMIT;
                

After COMMIT, the order and its items are permanently stored. Even if the server crashes immediately after, the data survives (that's durability).

What Happens After COMMIT

  • All changes become permanent on disk
  • Other connections can now see the new data
  • The transaction ends — you're back to normal mode
  • You cannot ROLLBACK after COMMIT

ROLLBACK — Undo Everything

ROLLBACK tells MySQL: "Something went wrong — undo all changes since START TRANSACTION."


START TRANSACTION;

-- Accidentally delete all books!
DELETE FROM order_items;
DELETE FROM books;

-- Oh no! Undo everything:
ROLLBACK;

-- Phew — books and order_items are still intact
SELECT COUNT(*) FROM books;  -- Still has all rows
                

ROLLBACK is your safety net. The data is restored to exactly the state it was in before START TRANSACTION.

Practical Example: Transfer with Validation


-- Simulate a "balance transfer" between two accounts
-- accounts table: id, name, balance

START TRANSACTION;

-- Debit the source account
UPDATE accounts SET balance = balance - 500 WHERE id = 1;

-- Check if the balance went negative
SELECT balance INTO @source_balance FROM accounts WHERE id = 1;

-- If negative, abort the whole thing
-- (In the CLI you'd check manually; in a procedure, use IF)
-- For now, let's assume it's fine and credit the destination:
UPDATE accounts SET balance = balance + 500 WHERE id = 2;

-- Verify both accounts look correct, then commit:
COMMIT;
                

⚠️ ROLLBACK Only Undoes DML

ROLLBACK undoes data changes (INSERT, UPDATE, DELETE). It does not undo DDL statements like CREATE TABLE, DROP TABLE, or ALTER TABLE — those are automatically committed and cannot be rolled back.

Autocommit Mode

By default, MySQL runs in autocommit mode — every single statement is automatically wrapped in its own transaction and committed immediately:


-- With autocommit ON (default):
INSERT INTO books (title, price, author_id) VALUES ('Foundation', 11.99, 4);
-- This is immediately committed — you can't ROLLBACK it

-- Check autocommit status:
SELECT @@autocommit;  -- 1 = ON, 0 = OFF
                

When you write START TRANSACTION, autocommit is temporarily suspended for that transaction. After COMMIT or ROLLBACK, autocommit resumes.

Disabling Autocommit


-- Turn off autocommit for the current session
SET autocommit = 0;

-- Now every statement is part of an implicit transaction
INSERT INTO books (title, price, author_id) VALUES ('Foundation', 11.99, 4);
UPDATE books SET price = 12.99 WHERE title = 'Foundation';

-- Nothing is saved until you explicitly COMMIT:
COMMIT;

-- Or undo everything:
ROLLBACK;

-- Turn autocommit back on:
SET autocommit = 1;
                

💡 When to Disable Autocommit

Most applications use the default (autocommit ON) and explicitly wrap multi-step operations in START TRANSACTION ... COMMIT. Disabling autocommit for a whole session is uncommon but useful for bulk data loading or migration scripts where you want manual control over commits.

Implicit Commits

Certain statements cause an implicit commit — they automatically commit any pending transaction, even if you didn't write COMMIT:

  • DDL statements: CREATE TABLE, ALTER TABLE, DROP TABLE
  • TRUNCATE TABLE
  • CREATE INDEX, DROP INDEX
  • CREATE USER, GRANT, REVOKE
  • Starting a new START TRANSACTION (commits the previous one)

⚠️ Don't Mix DDL with Transactions

If you run a CREATE TABLE inside a transaction, it implicitly commits everything before it — including changes you might have wanted to roll back. Keep DDL statements outside of transactions.

Savepoints — Partial Rollback

Sometimes you want to undo part of a transaction without losing everything. A savepoint is a named checkpoint you can roll back to.


START TRANSACTION;

-- Step 1: Create the order
INSERT INTO orders (customer_id, order_date) VALUES (1, CURRENT_DATE);
SET @order_id = LAST_INSERT_ID();

SAVEPOINT after_order;

-- Step 2: Add items
INSERT INTO order_items (order_id, book_id, quantity)
VALUES (@order_id, 1, 1);

SAVEPOINT after_items;

-- Step 3: Try something risky
INSERT INTO order_items (order_id, book_id, quantity)
VALUES (@order_id, 999, 1);  -- book_id 999 doesn't exist!

-- Step 3 failed — roll back to after_items, keeping Steps 1 & 2
ROLLBACK TO SAVEPOINT after_items;

-- Steps 1 and 2 are still intact — commit them:
COMMIT;
                
graph LR S["START"] --> O["INSERT order"] O --> SP1["SAVEPOINT after_order"] SP1 --> I["INSERT items"] I --> SP2["SAVEPOINT after_items"] SP2 --> R["Risky INSERT
(fails)"] R -->|"ROLLBACK TO after_items"| SP2 SP2 --> C["COMMIT ✅
Order + items saved"]

Savepoint Commands


-- Create a savepoint
SAVEPOINT savepoint_name;

-- Roll back to a savepoint (undo changes AFTER the savepoint)
ROLLBACK TO SAVEPOINT savepoint_name;

-- Release (delete) a savepoint without rolling back
RELEASE SAVEPOINT savepoint_name;
                

💡 Savepoints Don't End the Transaction

ROLLBACK TO SAVEPOINT undoes changes made after the savepoint, but the transaction is still active. You still need to COMMIT or ROLLBACK to end it. A full ROLLBACK (without a savepoint name) undoes everything, including all savepoints.

Transactions in Stored Procedures

Transactions inside stored procedures are where the real power emerges — you can combine multi-step logic with error handling.

Basic Pattern


DELIMITER //

CREATE PROCEDURE place_order(
    IN cust_id INT,
    IN book_id_param INT,
    IN qty INT,
    OUT result_message VARCHAR(100)
)
BEGIN
    DECLARE order_id INT;

    -- Use a handler for errors
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET result_message = 'Error: Order failed — rolled back';
    END;

    START TRANSACTION;

    -- Create the order
    INSERT INTO orders (customer_id, order_date)
    VALUES (cust_id, CURRENT_DATE);

    SET order_id = LAST_INSERT_ID();

    -- Add the item
    INSERT INTO order_items (order_id, book_id, quantity)
    VALUES (order_id, book_id_param, qty);

    -- If we got here, everything worked
    COMMIT;
    SET result_message = CONCAT('Success: Order #', order_id, ' created');

END //

DELIMITER ;

-- Test it:
CALL place_order(1, 1, 2, @msg);
SELECT @msg;
-- 'Success: Order #4 created'
                

💡 DECLARE ... HANDLER

The DECLARE EXIT HANDLER FOR SQLEXCEPTION block catches any SQL error during the transaction. When an error occurs, the handler runs: it rolls back the transaction and sets an error message. Without a handler, the procedure would stop at the error, leaving the transaction in a half-finished state.

Transfer Between Accounts


DELIMITER //

CREATE PROCEDURE transfer_funds(
    IN from_account INT,
    IN to_account INT,
    IN amount DECIMAL(10,2),
    OUT result VARCHAR(100)
)
BEGIN
    DECLARE source_balance DECIMAL(10,2);

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET result = 'Error: Transfer failed';
    END;

    START TRANSACTION;

    -- Check source balance
    SELECT balance INTO source_balance
    FROM accounts
    WHERE id = from_account
    FOR UPDATE;  -- Lock the row to prevent concurrent modification

    IF source_balance < amount THEN
        ROLLBACK;
        SET result = 'Error: Insufficient funds';
    ELSE
        -- Debit source
        UPDATE accounts SET balance = balance - amount
        WHERE id = from_account;

        -- Credit destination
        UPDATE accounts SET balance = balance + amount
        WHERE id = to_account;

        COMMIT;
        SET result = CONCAT('Success: Transferred $', amount);
    END IF;

END //

DELIMITER ;
                

💡 SELECT ... FOR UPDATE

FOR UPDATE locks the selected rows until the transaction ends. This prevents another transaction from reading or modifying the same account balance between your check and your update — avoiding a race condition where two concurrent transfers both see "enough balance" and overdraft the account.

Common Transaction Patterns

1. The Try-Commit-Catch-Rollback Pattern

The most common pattern — attempt the work, commit on success, rollback on failure:


DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
    ROLLBACK;
    -- Log the error, set a message, etc.
END;

START TRANSACTION;
    -- Multiple operations...
COMMIT;
                

2. Batch Insert with Transaction

Wrap bulk inserts in a transaction — much faster than individual autocommitted inserts:


START TRANSACTION;

INSERT INTO books (title, price, author_id) VALUES ('Book 1', 9.99, 1);
INSERT INTO books (title, price, author_id) VALUES ('Book 2', 12.99, 2);
INSERT INTO books (title, price, author_id) VALUES ('Book 3', 7.99, 3);
-- ... hundreds more ...

COMMIT;
                

✅ Performance Boost

Wrapping 1,000 INSERTs in a single transaction can be 10–50x faster than 1,000 individual autocommitted INSERTs. Each COMMIT forces a disk sync — doing it once instead of 1,000 times is dramatically faster.

3. Conditional Commit


START TRANSACTION;

DELETE FROM order_items WHERE order_id = 5;
DELETE FROM orders WHERE id = 5;

-- Check if we actually deleted anything meaningful:
-- (In a procedure, use ROW_COUNT() or a variable)
-- If something looks wrong:
--   ROLLBACK;
-- If everything is fine:
COMMIT;
                

4. Read-Only Transaction

You can start a transaction in read-only mode — MySQL can optimize it since it knows no writes will happen:


START TRANSACTION READ ONLY;

SELECT * FROM customers;
SELECT * FROM orders WHERE customer_id = 1;

COMMIT;  -- Nothing changed, but ends the transaction
                

Pitfalls & Best Practices

1. Keep Transactions Short

Long transactions hold locks on rows, blocking other users. Do your prep work (validation, calculations) before starting the transaction, then keep the transaction itself as brief as possible.


-- ❌ BAD: Long transaction with unnecessary work inside
START TRANSACTION;
    -- 5 seconds of complex calculations...
    -- Other users are waiting for locked rows
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

-- ✅ GOOD: Do prep work first, then a quick transaction
-- Calculate everything first...
START TRANSACTION;
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
                

2. Always End Your Transactions

A transaction that's never committed or rolled back holds locks indefinitely. Always ensure every START TRANSACTION has a matching COMMIT or ROLLBACK — even in error cases.

3. Don't Mix DDL and DML in a Transaction


-- ❌ BAD: DDL implicitly commits everything before it
START TRANSACTION;
    INSERT INTO orders (customer_id, order_date) VALUES (1, CURRENT_DATE);
    CREATE TABLE temp_log (msg TEXT);  -- IMPLICIT COMMIT happens here!
    -- The INSERT is now committed — you can't roll it back
    ROLLBACK;  -- Only rolls back changes AFTER the implicit commit
                

4. Handle Deadlocks

When two transactions each hold a lock that the other needs, MySQL detects a deadlock and automatically rolls back one of them. Your application should catch this error and retry:


-- In application code (pseudocode):
-- retry_count = 0
-- while retry_count < 3:
--     try:
--         START TRANSACTION
--         ... do work ...
--         COMMIT
--         break
--     catch deadlock_error:
--         retry_count += 1
--         wait briefly, then retry
                

5. Use Consistent Lock Order

To minimize deadlocks, always access tables (and rows) in the same order across all transactions. If Transaction A locks customers then orders, and Transaction B locks orders then customers, they can deadlock. Both locking customers first prevents this.

✅ Transaction Best Practices Summary

  • Keep transactions as short as possible
  • Always end transactions (COMMIT or ROLLBACK)
  • Use error handlers in stored procedures to ensure ROLLBACK on failure
  • Don't put DDL inside transactions
  • Access tables in a consistent order to prevent deadlocks
  • Use FOR UPDATE when you read-then-write to prevent race conditions
  • Use transactions for batch inserts — dramatically faster than autocommit

Exercises

🏋️ Exercise 1: Basic Transactions

Write SQL to:

  1. Start a transaction, insert a new author ("Ursula K. Le Guin"), insert a book by that author ("The Left Hand of Darkness", $10.99), then commit
  2. Start a transaction, delete all rows from order_items, then ROLLBACK (verify the data is restored)
  3. Start a transaction, insert two new customers, then commit. Verify they exist.
✅ Solution

-- 1
START TRANSACTION;
INSERT INTO authors (name) VALUES ('Ursula K. Le Guin');
SET @author_id = LAST_INSERT_ID();
INSERT INTO books (title, price, author_id)
VALUES ('The Left Hand of Darkness', 10.99, @author_id);
COMMIT;

-- 2
START TRANSACTION;
DELETE FROM order_items;
SELECT COUNT(*) FROM order_items;  -- 0 rows (temporarily)
ROLLBACK;
SELECT COUNT(*) FROM order_items;  -- All rows restored!

-- 3
START TRANSACTION;
INSERT INTO customers (first_name, last_name)
VALUES ('Dave', 'Wilson');
INSERT INTO customers (first_name, last_name)
VALUES ('Eve', 'Brown');
COMMIT;
SELECT * FROM customers WHERE first_name IN ('Dave', 'Eve');
                        

🏋️ Exercise 2: Savepoints

Write a transaction that:

  1. Inserts a new order for customer 2
  2. Creates a savepoint called order_created
  3. Inserts an order item for book_id 1
  4. Creates a savepoint called item_added
  5. Inserts another order item for a non-existent book (book_id 999)
  6. Rolls back to item_added (keeping the order and first item)
  7. Commits the transaction
✅ Solution

START TRANSACTION;

-- 1. Insert order
INSERT INTO orders (customer_id, order_date) VALUES (2, CURRENT_DATE);
SET @oid = LAST_INSERT_ID();

-- 2. Savepoint
SAVEPOINT order_created;

-- 3. Insert valid item
INSERT INTO order_items (order_id, book_id, quantity) VALUES (@oid, 1, 1);

-- 4. Savepoint
SAVEPOINT item_added;

-- 5. Try invalid item (might cause FK error)
-- INSERT INTO order_items (order_id, book_id, quantity) VALUES (@oid, 999, 1);

-- 6. Roll back to item_added
ROLLBACK TO SAVEPOINT item_added;

-- 7. Commit (order + first item are saved)
COMMIT;
                        

🏋️ Exercise 3: Transaction in a Stored Procedure

Create a stored procedure called safe_delete_order that:

  • Takes an order ID as an IN parameter and a result message as an OUT parameter
  • Uses a transaction to delete the order's items from order_items first, then the order from orders
  • Includes an error handler that rolls back on any SQL exception
  • Sets the result message to indicate success or failure
✅ Solution

DELIMITER //

CREATE PROCEDURE safe_delete_order(
    IN order_id_param INT,
    OUT result VARCHAR(100)
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET result = 'Error: Delete failed — rolled back';
    END;

    START TRANSACTION;

    -- Delete items first (child rows)
    DELETE FROM order_items WHERE order_id = order_id_param;

    -- Then delete the order (parent row)
    DELETE FROM orders WHERE id = order_id_param;

    IF ROW_COUNT() = 0 THEN
        ROLLBACK;
        SET result = CONCAT('Error: Order #', order_id_param, ' not found');
    ELSE
        COMMIT;
        SET result = CONCAT('Success: Order #', order_id_param, ' deleted');
    END IF;
END //

DELIMITER ;

-- Test:
CALL safe_delete_order(3, @msg);
SELECT @msg;
                        

🎯 Quick Quiz

Question 1: What does "Atomicity" in ACID mean?

Question 2: What happens to changes inside a transaction before COMMIT is called?

Question 3: What does a SAVEPOINT allow you to do?

Question 4: Why should you keep transactions short?

Summary

🎉 Key Takeaways

  • A transaction groups operations into an all-or-nothing unit
  • ACID: Atomicity, Consistency, Isolation, Durability
  • START TRANSACTION begins, COMMIT saves permanently, ROLLBACK undoes everything
  • Autocommit is ON by default — each statement is its own transaction
  • START TRANSACTION temporarily suspends autocommit
  • Savepoints let you roll back to a checkpoint without undoing the entire transaction
  • DDL statements (CREATE, ALTER, DROP) cause implicit commits — don't mix with DML transactions
  • Use DECLARE EXIT HANDLER FOR SQLEXCEPTION in procedures to auto-rollback on errors
  • SELECT ... FOR UPDATE locks rows to prevent race conditions
  • Keep transactions short to minimize lock contention
  • Batch inserts inside a transaction are dramatically faster than individual autocommits

Quick Reference

Command Purpose
START TRANSACTION Begin a transaction
COMMIT Save all changes permanently
ROLLBACK Undo all changes since START TRANSACTION
SAVEPOINT name Create a named checkpoint
ROLLBACK TO SAVEPOINT name Undo changes after the savepoint
RELEASE SAVEPOINT name Delete a savepoint (no rollback)
SET autocommit = 0/1 Disable/enable autocommit
SELECT ... FOR UPDATE Lock selected rows until transaction ends

📚 Additional Resources

🚀 What's Next?

You can now protect data integrity with transactions. But who gets to run those transactions? In Lesson 19: User Management & Permissions, you'll learn to create database users, control what they can access with GRANT and REVOKE, and apply the principle of least privilege to keep your database secure.