MySQLTransactions

MySQL Transactions

A transaction is a group of SQL statements that execute as a single atomic unit. Either every statement in the group succeeds and the changes are permanently saved, or an error occurs and every change is undone — leaving the database exactly as it was before the transaction started. Transactions are the foundation of data integrity in production systems.

Starting a Transaction

Use START TRANSACTION (or the equivalent BEGIN) to begin a transaction block:

SQL
-- Both are equivalent
START TRANSACTION;
-- ... SQL statements ...
COMMIT;

BEGIN;
-- ... SQL statements ...
COMMIT;
COMMIT and ROLLBACK

SQL
-- Classic bank transfer example
START TRANSACTION;

-- Deduct from sender
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;

-- Add to receiver
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;

-- Both updates succeeded — save permanently
COMMIT;

-- -----------------------------------------------

-- If an error occurred instead:
START TRANSACTION;

UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
-- Suppose account 2 was closed and the update fails
-- Roll back both changes — account 1 is restored
ROLLBACK;
Note
Only InnoDB (the default storage engine) supports transactions. MyISAM tables do not support transactions — a ROLLBACK on MyISAM data is silently ignored.
autocommit Mode

By default, MySQL runs in autocommit mode: every individual SQL statement is automatically wrapped in its own transaction and committed immediately. This means without START TRANSACTION, every INSERT/UPDATE/DELETE is permanent instantly.

SQL
-- Check autocommit status
SHOW VARIABLES LIKE 'autocommit';

-- Disable autocommit for the session
SET autocommit = 0;

-- Now every statement must be explicitly committed
UPDATE products SET price = 9.99 WHERE product_id = 1;
COMMIT;   -- Must commit manually, or the change is lost when session ends

-- Re-enable autocommit
SET autocommit = 1;
Tip
Explicitly using START TRANSACTION is clearer than disabling autocommit. It makes transaction boundaries obvious to anyone reading the code.
SAVEPOINT — Partial Rollback

A savepoint marks a point within a transaction you can roll back to without abandoning the entire transaction:

SQL
START TRANSACTION;

INSERT INTO orders (customer_id, order_date, status)
VALUES (42, NOW(), 'pending');

SAVEPOINT after_order;   -- Mark this point

INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (LAST_INSERT_ID(), 7, 2, 29.99);

-- Suppose the item insert causes a problem
ROLLBACK TO SAVEPOINT after_order;
-- The order row is still in place, the item row was rolled back

-- Try a different item
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (LAST_INSERT_ID(), 12, 1, 59.99);

COMMIT;   -- Commits the order + the second item attempt
RELEASE SAVEPOINT

SQL
START TRANSACTION;

SAVEPOINT sp1;
-- ... work ...

SAVEPOINT sp2;
-- ... more work ...

-- Remove sp2 — cannot roll back to it anymore, but does NOT commit
RELEASE SAVEPOINT sp2;

-- Can still roll back to sp1
ROLLBACK TO SAVEPOINT sp1;

COMMIT;
Implicit Commits — DDL Statements

Certain statements cause an implicit commit — they automatically commit any open transaction before and after themselves. You cannot roll them back:

Statement Type

Examples

DDL

CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE, RENAME TABLE

Privilege management

GRANT, REVOKE, CREATE USER, DROP USER

Session control

SET autocommit = 1, LOCK TABLES, UNLOCK TABLES

Transaction control

START TRANSACTION, BEGIN (commits previous transaction)

SQL
START TRANSACTION;

INSERT INTO customers (first_name, last_name, email)
VALUES ('Test', 'User', 'test@example.com');

-- WARNING: This implicitly commits the INSERT above!
-- The INSERT cannot be rolled back after this point
ALTER TABLE customers ADD COLUMN phone VARCHAR(20);

ROLLBACK;  -- Does NOT undo the INSERT or the ALTER TABLE
Warning
Never mix DDL statements with DML statements inside the same transaction expecting to roll back the DML. The DDL will trigger an implicit commit of everything that came before it.
Transaction with Error Handling in Application Code

Most application-layer code wraps database transactions in try/catch blocks. Here is the universal pattern:

SQL
-- Pattern used in application code (Python, Node, PHP, Java):
-- try {
--   START TRANSACTION;
--   ... DML statements ...
--   COMMIT;
-- } catch (error) {
--   ROLLBACK;
--   throw error;
-- }

-- Replicated in a stored procedure:
DELIMITER //

CREATE PROCEDURE transfer_funds(
  IN p_from_account INT,
  IN p_to_account   INT,
  IN p_amount       DECIMAL(10,2),
  OUT p_result      VARCHAR(100)
)
BEGIN
  DECLARE v_balance DECIMAL(10,2);

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK;
    SET p_result = 'Transfer failed — transaction rolled back';
  END;

  START TRANSACTION;

  -- Check sufficient funds
  SELECT balance INTO v_balance FROM accounts WHERE account_id = p_from_account FOR UPDATE;

  IF v_balance < p_amount THEN
    ROLLBACK;
    SET p_result = 'Insufficient funds';
    LEAVE transfer_funds;
  END IF;

  UPDATE accounts SET balance = balance - p_amount WHERE account_id = p_from_account;
  UPDATE accounts SET balance = balance + p_amount WHERE account_id = p_to_account;

  INSERT INTO transfers (from_account, to_account, amount, transferred_at)
  VALUES (p_from_account, p_to_account, p_amount, NOW());

  COMMIT;
  SET p_result = 'Transfer successful';
END //

DELIMITER ;
Transaction Best Practices
  • Keep transactions short — long-running transactions hold locks, block other sessions, and increase deadlock risk

  • Do all reads you need BEFORE starting the transaction when possible

  • Never wait for user input inside a transaction — open transaction + user thinking = disaster

  • Use SELECT ... FOR UPDATE to lock rows you will modify, preventing concurrent conflicts

  • Always handle ROLLBACK in your error/catch path to avoid partial updates

  • Avoid mixing DDL and DML in the same transaction block

Checking Transaction Status

SQL
-- See open transactions in InnoDB
SELECT * FROM information_schema.INNODB_TRXG

-- See locks held
SELECT * FROM information_schema.INNODB_LOCKSG
-- (MySQL 8.0+: use performance_schema.data_locks instead)
SELECT * FROM performance_schema.data_locksG

-- See lock waits
SELECT * FROM performance_schema.data_lock_waitsG
Practical Example: Order Checkout Transaction

SQL
START TRANSACTION;

-- 1. Reserve stock (lock the product rows)
SELECT product_id, stock_qty FROM products
WHERE product_id IN (7, 12, 15) FOR UPDATE;

-- 2. Create the order header
INSERT INTO orders (customer_id, order_date, status, total_amount)
VALUES (42, NOW(), 'pending', 0);

SET @order_id = LAST_INSERT_ID();

-- 3. Insert order items
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
  (@order_id, 7, 2, 29.99),
  (@order_id, 12, 1, 59.99),
  (@order_id, 15, 3, 9.99);

-- 4. Deduct stock
UPDATE products SET stock_qty = stock_qty - 2 WHERE product_id = 7;
UPDATE products SET stock_qty = stock_qty - 1 WHERE product_id = 12;
UPDATE products SET stock_qty = stock_qty - 3 WHERE product_id = 15;

-- 5. Update order total from actual line items
UPDATE orders
SET total_amount = (
  SELECT SUM(quantity * unit_price) FROM order_items WHERE order_id = @order_id
)
WHERE order_id = @order_id;

-- 6. All good — commit permanently
COMMIT;
READ ONLY Transactions

SQL
-- Declare a read-only transaction (optimization hint for InnoDB)
START TRANSACTION READ ONLY;

SELECT SUM(total_amount) FROM orders WHERE customer_id = 42;
SELECT * FROM customers WHERE customer_id = 42;

COMMIT;  -- or ROLLBACK — both end the read-only transaction
Tip
Marking a transaction READ ONLY tells InnoDB it does not need to assign a transaction ID or prepare undo log entries, reducing overhead for long-running analytical reads.
Transaction Isolation in Multi-Session Scenarios

SQL
-- Use a consistent snapshot for a long analytical read
-- START TRANSACTION READ ONLY prevents InnoDB from assigning a write TRX ID
START TRANSACTION READ ONLY;

SELECT
  DATE_FORMAT(order_date, '%Y-%m') AS month,
  COUNT(*)                         AS orders,
  SUM(total_amount)                AS revenue
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;

-- Even if other sessions commit new orders while we read,
-- we see a consistent snapshot from when we opened the transaction
COMMIT;
Transactions in Application Code Patterns

Every application language has a standard pattern for wrapping database calls in transactions:

Bash
# Node.js (mysql2) pattern:
# const conn = await pool.getConnection();
# try {
#   await conn.beginTransaction();
#   await conn.query('UPDATE accounts SET balance = balance - 500 WHERE id = 1');
#   await conn.query('UPDATE accounts SET balance = balance + 500 WHERE id = 2');
#   await conn.commit();
# } catch (err) {
#   await conn.rollback();
#   throw err;
# } finally {
#   conn.release();
# }

# Python (mysql-connector) pattern:
# cnx = mysql.connector.connect(...)
# cursor = cnx.cursor()
# try:
#   cursor.execute('UPDATE accounts SET balance = balance - 500 WHERE id = 1')
#   cursor.execute('UPDATE accounts SET balance = balance + 500 WHERE id = 2')
#   cnx.commit()
# except Exception as e:
#   cnx.rollback()
#   raise e
Common Transaction Mistakes
  • Not rolling back on error — leaving the connection in an open transaction state

  • Performing network calls or file I/O inside a transaction — transactions should be pure DB operations

  • Using TRUNCATE inside a transaction (it causes an implicit commit — cannot be rolled back)

  • Running a transaction across multiple HTTP requests — always commit within a single request lifecycle

  • Ignoring lock wait timeout errors (1205) — they mean a transaction has timed out waiting for a lock and was partially rolled back

Best Practices Summary
  • Use START TRANSACTION explicitly rather than relying on autocommit=0

  • Keep transactions as short as possible — commit early, commit often

  • Always handle errors with ROLLBACK in catch blocks

  • Use SAVEPOINT for complex multi-step operations where partial rollback may be needed

  • Monitor long-running transactions via information_schema.INNODB_TRX

  • Test transaction logic with intentional failures to verify rollback paths work correctly

  • Never use DDL (ALTER TABLE, CREATE INDEX) inside a DML transaction you want to roll back