Transactions & ACID
Ensure data integrity and consistency
Why Transactions Matter
A transaction is a sequence of database operations that are treated as a single unit. Either all operations succeed, or none do, there's no half-finished state. This is critical for maintaining data integrity. Imagine transferring money between bank accounts: you can't deduct from one account and fail to add to the other. Transactions guarantee that related operations complete together or fail together.
The Problem: Inconsistent Data
Without transactions, operations can fail partway through, leaving data inconsistent.
Money Transfer Without Transaction
-- Step 1: Deduct from Alice UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; -- ✓ Success: Alice now has $900 -- Step 2: Add to Bob UPDATE accounts SET balance = balance + 100 WHERE account_id = 2; -- ✗ ERROR: Server crash!
Result: Alice: $1000 → $900 (money deducted) Bob: $500 → $500 (money never added) Total: $1500 → $1400 ❌ Money disappeared!
Using Transactions
Wrap related operations in BEGIN and COMMIT to make them atomic.
Money Transfer With Transaction
BEGIN; -- Start transaction -- Step 1: Deduct from Alice UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; -- Step 2: Add to Bob UPDATE accounts SET balance = balance + 100 WHERE account_id = 2; COMMIT; -- Make changes permanent
If both succeed: Alice: $1000 → $900 Bob: $500 → $600 Total: $1500 → $1500 ✓ Balanced! If either fails: Alice: $1000 (unchanged) Bob: $500 (unchanged) Total: $1500 ✓ Rolled back, no data lost
Rolling Back on Error
BEGIN; UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; UPDATE accounts SET balance = balance + 100 WHERE account_id = 999; -- Oops! Account doesn't exist ROLLBACK; -- Undo everything
Both operations cancelled, data remains consistent
The ACID Properties
ACID is an acronym for four properties that guarantee reliable transactions.
AAtomicity: All or Nothing
A transaction is indivisible. Either all operations complete successfully, or none do.
BEGIN; INSERT INTO orders (...); -- ✓ INSERT INTO order_items (...); -- ✓ UPDATE inventory (...); -- ✗ FAILS ROLLBACK; Result: All 3 operations cancelled (atomic)
CConsistency: Valid State to Valid State
Database moves from one valid state to another. All constraints, triggers, and rules are enforced.
BEGIN; UPDATE accounts SET balance = -50 -- Violates CHECK constraint WHERE account_id = 1; COMMIT; Result: Transaction rejected, constraint enforced
IIsolation: Transactions Don't Interfere
Concurrent transactions don't see each other's uncommitted changes. Each transaction appears to run alone.
Transaction 1: Transaction 2:
BEGIN; BEGIN;
UPDATE accounts SELECT balance FROM accounts
SET balance = 900 WHERE account_id = 1;
WHERE account_id = 1;
Result: 1000 (not 900)
-- Can't see uncommitted change
COMMIT; COMMIT;DDurability: Changes Persist
Once committed, changes are permanent. They survive crashes, power failures, and restarts.
BEGIN; UPDATE accounts SET balance = 900; COMMIT; -- ✓ Written to disk -- Power failure occurs here! -- After restart: SELECT balance FROM accounts; Result: 900 (change persisted)
Transaction Commands
BEGIN / START TRANSACTION
Start a new transaction.
BEGIN; -- PostgreSQL, MySQL -- or START TRANSACTION; -- Standard SQL, MySQL, PostgreSQL -- or BEGIN TRANSACTION; -- SQL Server
COMMIT
Make all changes permanent.
COMMIT; -- Save all changes
ROLLBACK
Undo all changes in the transaction.
ROLLBACK; -- Cancel all changes
SAVEPOINT
Create a checkpoint within a transaction to rollback to.
BEGIN; UPDATE accounts SET balance = 900 WHERE account_id = 1; SAVEPOINT after_first_update; UPDATE accounts SET balance = 600 WHERE account_id = 2; SAVEPOINT after_second_update; UPDATE accounts SET balance = -100 WHERE account_id = 3; -- Oops! Invalid operation ROLLBACK TO after_second_update; -- Undo only the last change COMMIT; -- Save first two updates
Partial rollback while keeping some changes
Isolation Levels: Trading Safety for Speed
Perfect isolation is slow. Databases offer different isolation levels to balance consistency with performance.
| Level | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | ❌ Possible | ❌ Possible | ❌ Possible |
| READ COMMITTED | ✓ Prevented | ❌ Possible | ❌ Possible |
| REPEATABLE READ | ✓ Prevented | ✓ Prevented | ❌ Possible |
| SERIALIZABLE | ✓ Prevented | ✓ Prevented | ✓ Prevented |
Read Uncommitted (Lowest Isolation)
Can see uncommitted changes from other transactions (dirty reads).
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Transaction 1: Transaction 2:
BEGIN; BEGIN;
UPDATE accounts
SET balance = 500
WHERE id = 1;
SELECT balance FROM accounts
WHERE id = 1;
Result: 500 (dirty read!)
ROLLBACK;
-- Saw data that was rolled back!Read Committed (Default in PostgreSQL)
Only see committed changes. No dirty reads.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
Transaction 1: Transaction 2:
BEGIN; BEGIN;
UPDATE accounts
SET balance = 500
WHERE id = 1;
SELECT balance FROM accounts
WHERE id = 1;
Result: 1000 (sees old value)
COMMIT;
SELECT balance FROM accounts
WHERE id = 1;
Result: 500 (sees new value after commit)Serializable (Highest Isolation)
Transactions appear to run one at a time. Safest but slowest.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- Guarantees complete isolation -- May block or abort conflicting transactions
Concurrency Problems
Dirty Read
Reading uncommitted data that might be rolled back.
Transaction A updates balance to $500 (not committed) Transaction B reads balance: $500 Transaction A rolls back Transaction B used invalid data!
Non-Repeatable Read
Same query returns different results within one transaction.
Transaction A reads balance: $1000 Transaction B updates balance to $500 and commits Transaction A reads balance again: $500 Same query, different result!
Phantom Read
New rows appear in query results within one transaction.
Transaction A: SELECT COUNT(*) FROM orders → 10 rows Transaction B: INSERT INTO orders (...) and commits Transaction A: SELECT COUNT(*) FROM orders → 11 rows Row appeared out of nowhere!
Locking Strategies
Databases use locks to prevent conflicts between concurrent transactions.
Pessimistic Locking
Lock data when you read it, preventing others from modifying.
BEGIN; SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE; -- Lock this row -- Other transactions wait until this commits UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; COMMIT; -- Lock released
Optimistic Locking
Don't lock, but check if data changed before updating.
-- Add version column ALTER TABLE accounts ADD COLUMN version INTEGER DEFAULT 1; -- Read data and version SELECT balance, version FROM accounts WHERE account_id = 1; -- Result: balance=1000, version=1 -- Update only if version hasn't changed UPDATE accounts SET balance = 900, version = version + 1 WHERE account_id = 1 AND version = 1; -- If 0 rows updated, someone else modified it -- Retry or abort
Good for low-conflict scenarios
Deadlocks: When Transactions Block Each Other
A deadlock occurs when two transactions wait for each other's locks.
Transaction 1: Transaction 2: BEGIN; BEGIN; UPDATE accounts UPDATE accounts SET balance = 900 SET balance = 1100 WHERE id = 1; WHERE id = 2; -- Locks row 1 -- Locks row 2 UPDATE accounts UPDATE accounts SET balance = 600 SET balance = 800 WHERE id = 2; WHERE id = 1; -- Waits for row 2 lock -- Waits for row 1 lock ❌ DEADLOCK! Both wait forever
Deadlock Prevention
- Lock in same order: Always lock rows in the same order (e.g., by ID)
- Keep transactions short: Less time holding locks = fewer deadlocks
- Use timeouts: Database detects and breaks deadlocks automatically
Transaction Best Practices
✅ Keep Transactions Short
Long transactions hold locks longer, blocking other transactions. Do only what's necessary, then commit.
✅ Always Use Transactions for Related Changes
Any sequence of operations that must succeed together should be in a transaction. Bank transfers, order + inventory updates, user creation + role assignment.
✅ Handle Errors Properly
Always ROLLBACK on errors. Use try-catch blocks in application code to ensure transactions are cleaned up.
✅ Use Appropriate Isolation Level
READ COMMITTED is fine for most applications. Use SERIALIZABLE only when absolutely necessary.
✅ Avoid User Interaction in Transactions
Never wait for user input while a transaction is open. This holds locks and blocks other users.
Key Takeaways
- Transactions ensure all-or-nothing - guarantees that either all operations succeed or none are applied to the database.
- ACID properties: The golden standard for reliability (Atomicity, Consistency, Isolation, Durability).
- BEGIN starts, COMMIT saves, ROLLBACK cancels - the fundamental syntax for managing manual transactions.
- Isolation levels trade safety for speed - higher levels prevent more anomalies but can slow down concurrent access.
- Locks prevent conflicts - Row-level locks ensure two users don't modify the exact same data at the exact same time.
- Deadlocks occur when - two transactions are stuck waiting for each other to release locks. Modern databases detect and resolve these automatically.
- Data Integrity is the Goal - mastery of transactions is what separates a casual user from a professional database developer.