What are database transactions and ACID properties?

Answer

A database transaction is a sequence of one or more SQL operations that are executed as a single logical unit of work. ACID properties ensure that database transactions are processed reliably and maintain data integrity even in the presence of errors, power failures, or concurrent access.

ACID Properties

A - Atomicity

All operations in a transaction succeed or all fail together.

-- Example: Bank transfer (all or nothing)
START TRANSACTION;

UPDATE accounts 
SET balance = balance - 1000 
WHERE account_id = 'ACC001';

UPDATE accounts 
SET balance = balance + 1000 
WHERE account_id = 'ACC002';

-- If any operation fails, entire transaction is rolled back
COMMIT;  -- Only if both updates succeed

C - Consistency

Database remains in a valid state before and after the transaction.

-- Consistency example: Maintaining referential integrity
START TRANSACTION;

-- Insert order (parent record)
INSERT INTO orders (order_id, customer_id, order_date, total)
VALUES (1001, 123, '2024-01-15', 250.00);

-- Insert order items (child records)
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES 
    (1001, 'P001', 2, 50.00),
    (1001, 'P002', 1, 150.00);

-- Database constraints ensure consistency
-- Foreign key constraints prevent orphaned records
COMMIT;

I - Isolation

Concurrent transactions don’t interfere with each other.

-- Transaction 1
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 'ACC001';  -- Reads 1000
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'ACC001';
-- Transaction sees balance as 900, but not yet committed

-- Transaction 2 (running concurrently)
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 'ACC001';  -- Still reads 1000 (isolation)
-- Won't see Transaction 1's changes until it commits

D - Durability

Committed changes persist even after system failures.

START TRANSACTION;

INSERT INTO critical_data (id, value, timestamp)
VALUES (1, 'Important Data', NOW());

COMMIT;  -- Once committed, data survives system crashes
-- Database ensures data is written to persistent storage

Transaction Control Statements

Basic Transaction Control

-- Start a transaction
START TRANSACTION;  -- or BEGIN;

-- Perform operations
INSERT INTO customers (name, email) VALUES ('John Doe', 'john@email.com');
UPDATE orders SET status = 'PROCESSED' WHERE customer_id = LAST_INSERT_ID();

-- Commit changes (make permanent)
COMMIT;

-- Or rollback changes (undo all operations)
ROLLBACK;

Savepoints

START TRANSACTION;

INSERT INTO orders (customer_id, order_date) VALUES (123, NOW());
SAVEPOINT order_created;

INSERT INTO order_items (order_id, product_id, quantity) 
VALUES (LAST_INSERT_ID(), 'P001', 2);
SAVEPOINT items_added;

-- Something goes wrong with shipping calculation
INSERT INTO shipping (order_id, cost) VALUES (LAST_INSERT_ID(), -10);  -- Invalid cost

-- Rollback to savepoint (keeps order and items, removes invalid shipping)
ROLLBACK TO SAVEPOINT items_added;

-- Add correct shipping
INSERT INTO shipping (order_id, cost) VALUES (LAST_INSERT_ID(), 15.99);

COMMIT;  -- Commits order, items, and correct shipping

Isolation Levels

Read Uncommitted

Lowest isolation level - allows dirty reads.

-- Session 1
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;

-- Session 2
START TRANSACTION;
UPDATE products SET price = 99.99 WHERE product_id = 'P001';
-- Not yet committed

-- Session 1 can read uncommitted changes (dirty read)
SELECT price FROM products WHERE product_id = 'P001';  -- Sees 99.99

-- Session 2
ROLLBACK;  -- Session 1 read data that was rolled back!

Read Committed

Prevents dirty reads but allows non-repeatable reads.

-- Session 1
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT price FROM products WHERE product_id = 'P001';  -- Reads 50.00

-- Session 2
START TRANSACTION;
UPDATE products SET price = 75.00 WHERE product_id = 'P001';
COMMIT;

-- Session 1
SELECT price FROM products WHERE product_id = 'P001';  -- Now reads 75.00 (non-repeatable read)
COMMIT;

Repeatable Read

Prevents dirty and non-repeatable reads but allows phantom reads.

-- Session 1
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT COUNT(*) FROM products WHERE category = 'Electronics';  -- Returns 10

-- Session 2
START TRANSACTION;
INSERT INTO products (name, category, price) 
VALUES ('New Phone', 'Electronics', 599.99);
COMMIT;

-- Session 1
SELECT COUNT(*) FROM products WHERE category = 'Electronics';  -- Still returns 10 in MySQL
-- But might return 11 in other databases (phantom read)
COMMIT;

Serializable

Highest isolation level - prevents all phenomena.

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;

-- This transaction will have complete isolation
-- No other transaction can modify data this transaction has read
SELECT * FROM accounts WHERE balance > 1000;

-- Any concurrent transaction trying to modify these accounts will wait
UPDATE accounts SET balance = balance * 1.05 WHERE balance > 1000;

COMMIT;

Concurrency Control Mechanisms

Locking

-- Explicit locking
START TRANSACTION;

-- Shared lock (read lock)
SELECT * FROM products WHERE product_id = 'P001' LOCK IN SHARE MODE;

-- Exclusive lock (write lock)
SELECT * FROM accounts WHERE account_id = 'ACC001' FOR UPDATE;

UPDATE accounts SET balance = balance - 100 WHERE account_id = 'ACC001';

COMMIT;  -- Releases locks

Deadlock Handling

-- Transaction 1
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'ACC001';
-- Waits for lock on ACC002

-- Transaction 2 (concurrent)
START TRANSACTION;
UPDATE accounts SET balance = balance + 50 WHERE account_id = 'ACC002';
-- Waits for lock on ACC001

-- Deadlock! Database will automatically rollback one transaction

-- Prevention: Always acquire locks in same order
-- Transaction 1 & 2 should both lock ACC001 first, then ACC002

Practical Transaction Examples

E-commerce Order Processing

DELIMITER //
CREATE PROCEDURE ProcessOrder(
    IN p_customer_id INT,
    IN p_product_id VARCHAR(20),
    IN p_quantity INT,
    OUT p_order_id INT,
    OUT p_result_message VARCHAR(255)
)
BEGIN
    DECLARE v_product_price DECIMAL(10,2);
    DECLARE v_available_stock INT;
    DECLARE v_order_total DECIMAL(10,2);
    
    -- Error handler for rollback
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_result_message = 'Order processing failed';
        SET p_order_id = NULL;
    END;
    
    START TRANSACTION;
    
    -- Check product availability (with lock)
    SELECT price, stock_quantity 
    INTO v_product_price, v_available_stock
    FROM products 
    WHERE product_id = p_product_id
    FOR UPDATE;  -- Lock the row
    
    -- Validate stock
    IF v_available_stock < p_quantity THEN
        SET p_result_message = 'Insufficient stock';
        ROLLBACK;
    ELSE
        -- Calculate total
        SET v_order_total = v_product_price * p_quantity;
        
        -- Create order
        INSERT INTO orders (customer_id, order_date, total_amount, status)
        VALUES (p_customer_id, NOW(), v_order_total, 'PENDING');
        
        SET p_order_id = LAST_INSERT_ID();
        
        -- Add order items
        INSERT INTO order_items (order_id, product_id, quantity, unit_price)
        VALUES (p_order_id, p_product_id, p_quantity, v_product_price);
        
        -- Update inventory
        UPDATE products 
        SET stock_quantity = stock_quantity - p_quantity
        WHERE product_id = p_product_id;
        
        -- Log inventory change
        INSERT INTO inventory_log (product_id, change_type, quantity_change, reference_id)
        VALUES (p_product_id, 'SALE', -p_quantity, p_order_id);
        
        SET p_result_message = 'Order processed successfully';
        COMMIT;
    END IF;
    
END //
DELIMITER ;

Financial Transfer with Audit

DELIMITER //
CREATE PROCEDURE TransferFunds(
    IN p_from_account VARCHAR(20),
    IN p_to_account VARCHAR(20),
    IN p_amount DECIMAL(12,2),
    IN p_description VARCHAR(255),
    OUT p_transaction_id INT,
    OUT p_status VARCHAR(50)
)
BEGIN
    DECLARE v_from_balance DECIMAL(12,2);
    DECLARE v_to_balance DECIMAL(12,2);
    DECLARE v_from_exists INT DEFAULT 0;
    DECLARE v_to_exists INT DEFAULT 0;
    
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_status = 'FAILED - Database Error';
        
        -- Log failed transaction
        INSERT INTO transaction_log (
            from_account, to_account, amount, status, 
            description, created_at
        ) VALUES (
            p_from_account, p_to_account, p_amount, 'FAILED',
            p_description, NOW()
        );
    END;
    
    START TRANSACTION;
    
    -- Validate accounts exist and get balances (with locks)
    SELECT COUNT(*), COALESCE(SUM(balance), 0)
    INTO v_from_exists, v_from_balance
    FROM accounts 
    WHERE account_number = p_from_account
    FOR UPDATE;
    
    SELECT COUNT(*), COALESCE(SUM(balance), 0)
    INTO v_to_exists, v_to_balance
    FROM accounts 
    WHERE account_number = p_to_account
    FOR UPDATE;
    
    -- Validation checks
    IF v_from_exists = 0 THEN
        SET p_status = 'FAILED - Source account not found';
        ROLLBACK;
    ELSEIF v_to_exists = 0 THEN
        SET p_status = 'FAILED - Destination account not found';
        ROLLBACK;
    ELSEIF v_from_balance < p_amount THEN
        SET p_status = 'FAILED - Insufficient funds';
        ROLLBACK;
    ELSEIF p_amount <= 0 THEN
        SET p_status = 'FAILED - Invalid amount';
        ROLLBACK;
    ELSE
        -- Perform transfer
        UPDATE accounts 
        SET balance = balance - p_amount,
            last_transaction_date = NOW()
        WHERE account_number = p_from_account;
        
        UPDATE accounts 
        SET balance = balance + p_amount,
            last_transaction_date = NOW()
        WHERE account_number = p_to_account;
        
        -- Create transaction record
        INSERT INTO transactions (
            from_account, to_account, amount, description, 
            status, created_at
        ) VALUES (
            p_from_account, p_to_account, p_amount, p_description,
            'COMPLETED', NOW()
        );
        
        SET p_transaction_id = LAST_INSERT_ID();
        
        -- Create audit entries
        INSERT INTO account_audit (
            account_number, transaction_type, amount, 
            balance_before, balance_after, transaction_id, created_at
        ) VALUES 
        (p_from_account, 'DEBIT', p_amount, v_from_balance, 
         v_from_balance - p_amount, p_transaction_id, NOW()),
        (p_to_account, 'CREDIT', p_amount, v_to_balance, 
         v_to_balance + p_amount, p_transaction_id, NOW());
        
        SET p_status = 'SUCCESS';
        COMMIT;
    END IF;
    
END //
DELIMITER ;

Transaction Performance Optimization

Minimize Transaction Scope

-- Bad: Long-running transaction
START TRANSACTION;
SELECT * FROM large_table;  -- Long operation
-- ... lots of processing ...
UPDATE summary_table SET total = 12345;
COMMIT;

-- Good: Minimize transaction time
-- Do processing outside transaction
SELECT * FROM large_table;
-- ... process data ...

-- Quick transaction for updates only
START TRANSACTION;
UPDATE summary_table SET total = 12345;
COMMIT;

Batch Processing

-- Process large datasets in batches
DELIMITER //
CREATE PROCEDURE ProcessLargeUpdate()
BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE batch_size INT DEFAULT 1000;
    DECLARE processed_count INT DEFAULT 0;
    
    WHILE NOT done DO
        START TRANSACTION;
        
        -- Process batch
        UPDATE products 
        SET price = price * 1.1 
        WHERE last_updated < DATE_SUB(NOW(), INTERVAL 1 YEAR)
        LIMIT batch_size;
        
        SET processed_count = ROW_COUNT();
        
        COMMIT;
        
        -- Check if we're done
        IF processed_count < batch_size THEN
            SET done = TRUE;
        END IF;
        
        -- Small delay to prevent overwhelming the system
        SELECT SLEEP(0.1);
        
    END WHILE;
END //
DELIMITER ;

Monitoring Transactions

Transaction Status Queries

-- MySQL: Show running transactions
SELECT 
    trx_id,
    trx_state,
    trx_started,
    trx_requested_lock_id,
    trx_wait_started,
    trx_mysql_thread_id
FROM information_schema.innodb_trx;

-- Show locks
SELECT 
    lock_id,
    lock_trx_id,
    lock_mode,
    lock_type,
    lock_table,
    lock_index
FROM information_schema.innodb_locks;

-- Show lock waits
SELECT 
    requesting_trx_id,
    requested_lock_id,
    blocking_trx_id,
    blocking_lock_id
FROM information_schema.innodb_lock_waits;

Best Practices

1. Keep Transactions Short

  • Minimize the time between START TRANSACTION and COMMIT
  • Avoid user interaction within transactions
  • Process data outside transactions when possible

2. Handle Errors Properly

-- Always include error handling
START TRANSACTION;

-- Use savepoints for complex operations
SAVEPOINT sp1;

-- Check for errors after each critical operation
IF @@ERROR != 0 THEN
    ROLLBACK TO SAVEPOINT sp1;
END IF;

COMMIT;

3. Choose Appropriate Isolation Levels

  • Use READ COMMITTED for most applications
  • Use REPEATABLE READ for financial applications
  • Use SERIALIZABLE only when absolutely necessary

4. Avoid Deadlocks

  • Always acquire locks in the same order
  • Keep transactions short
  • Use appropriate timeout settings

Interview Tips

  • Understand all four ACID properties and be able to explain each with examples
  • Know the different isolation levels and their trade-offs
  • Be familiar with deadlock prevention and resolution
  • Understand when to use explicit locking vs relying on database defaults
  • Know how to design transactions for high-concurrency applications
  • Be able to explain the performance implications of different isolation levels

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.