Table of Contents
- Understanding ACID Properties
- Transaction Basics in PostgreSQL
- Core Transaction Commands
- Savepoints: Partial Rollbacks
- 4.1
SAVEPOINT - 4.2
ROLLBACK TO SAVEPOINT - 4.3
RELEASE SAVEPOINT
- 4.1
- Transaction Isolation Levels
- 5.1 Read Uncommitted
- 5.2 Read Committed (Default)
- 5.3 Repeatable Read
- 5.4 Serializable
- Concurrency Control and Locking
- Practical Use Cases
- Best Practices for Transaction Management
- Troubleshooting Common Transaction Issues
- Conclusion
- References
1. Understanding ACID Properties
Transactions in PostgreSQL (and all relational databases) adhere to the ACID properties, which guarantee reliability even in the face of errors, power failures, or concurrent access:
- Atomicity: A transaction is “all or nothing.” If any part fails, the entire transaction is rolled back, and the database remains unchanged.
- Consistency: Transactions transform the database from one valid state to another, ensuring all constraints (e.g., foreign keys, unique indexes) are enforced.
- Isolation: Concurrent transactions do not interfere with each other. PostgreSQL provides isolation levels to control how much one transaction sees the changes of others.
- Durability: Once a transaction is committed, its changes are permanently stored and survive subsequent failures (e.g., crashes). PostgreSQL uses Write-Ahead Logging (WAL) to ensure durability.
2. Transaction Basics in PostgreSQL
2.1 Implicit vs. Explicit Transactions
PostgreSQL supports two types of transactions:
- Implicit Transactions: Single SQL statements executed in “autocommit” mode. Each statement is treated as its own transaction, automatically committed upon success.
- Explicit Transactions: Multi-statement transactions explicitly defined with
BEGIN,COMMIT, orROLLBACK. These are used for sequences of operations that must succeed or fail together.
2.2 Autocommit Mode
By default, PostgreSQL runs in autocommit mode. This means any SQL statement not wrapped in an explicit transaction is immediately committed. To disable autocommit (e.g., for multi-step transactions), use:
SET autocommit = OFF;
To re-enable it:
SET autocommit = ON;
Note: Most PostgreSQL clients (e.g.,
psql, pgAdmin) allow toggling autocommit. Inpsql, use\set autocommit onor\set autocommit off.
3. Core Transaction Commands
3.1 BEGIN
The BEGIN command starts an explicit transaction. All subsequent statements until COMMIT or ROLLBACK are part of the transaction.
Syntax:
BEGIN;
-- or
BEGIN TRANSACTION;
Example:
BEGIN;
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
UPDATE accounts SET balance = balance + 100 WHERE user_id = 1;
-- Transaction is not committed yet!
3.2 COMMIT
COMMIT finalizes the transaction, making all changes permanent. Once committed, changes are visible to other transactions (depending on isolation level) and survive crashes.
Syntax:
COMMIT;
-- or
COMMIT TRANSACTION;
Example:
BEGIN;
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
UPDATE accounts SET balance = balance + 100 WHERE user_id = 1;
COMMIT; -- Changes are now permanent
3.3 ROLLBACK
ROLLBACK undoes all changes made since the start of the transaction, restoring the database to its state before BEGIN. Use this if an error occurs or the transaction needs to be aborted.
Syntax:
ROLLBACK;
-- or
ROLLBACK TRANSACTION;
Example:
BEGIN;
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
UPDATE accounts SET balance = balance + 100 WHERE user_id = 1;
-- Oops! Made a mistake; undo everything
ROLLBACK; -- No changes persisted
4. Savepoints: Partial Rollbacks
Savepoints allow you to roll back part of a transaction without undoing all changes. This is useful for complex workflows where some steps might fail, but others should persist.
4.1 SAVEPOINT
SAVEPOINT creates a named checkpoint in the transaction. You can later roll back to this checkpoint if needed.
Syntax:
SAVEPOINT savepoint_name;
4.2 ROLLBACK TO SAVEPOINT
ROLLBACK TO SAVEPOINT undoes changes made after the specified savepoint, but preserves changes made before it.
Syntax:
ROLLBACK TO SAVEPOINT savepoint_name;
4.3 RELEASE SAVEPOINT
RELEASE SAVEPOINT removes the savepoint, making it unavailable for future rollbacks. Changes after the savepoint are now part of the main transaction.
Syntax:
RELEASE SAVEPOINT savepoint_name;
Example: Using Savepoints
Suppose you’re processing an order with three steps: create order, deduct inventory, charge customer. If charging fails, you should roll back the inventory deduction but keep the order.
BEGIN;
-- Step 1: Create order
INSERT INTO orders (user_id, total) VALUES (1, 99.99) RETURNING order_id INTO :order_id;
SAVEPOINT after_order; -- Checkpoint after order creation
-- Step 2: Deduct inventory
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 5;
SAVEPOINT after_inventory; -- Checkpoint after inventory update
-- Step 3: Charge customer (may fail)
BEGIN
UPDATE payments SET amount = 99.99 WHERE user_id = 1;
EXCEPTION
WHEN OTHERS THEN
-- Rollback to after_order: undo inventory deduction, keep order
ROLLBACK TO SAVEPOINT after_order;
RAISE NOTICE 'Payment failed; order retained but inventory restored';
END;
COMMIT; -- Finalize (order exists; inventory is restored if payment failed)
5. Transaction Isolation Levels
Isolation levels control how concurrent transactions interact with each other. PostgreSQL supports all four standard SQL isolation levels, each preventing different types of race conditions (e.g., dirty reads, non-repeatable reads, phantom reads).
5.1 Read Uncommitted
- Behavior: Transactions see uncommitted changes from others.
- Race Conditions Allowed: Dirty reads, non-repeatable reads, phantom reads.
- Use Case: Rarely used; PostgreSQL treats this as
Read Committed(due to MVCC limitations).
Syntax:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
5.2 Read Committed (Default)
- Behavior: Transactions only see committed changes. A
SELECTre-evaluates each time, so results may change (non-repeatable reads possible). - Race Conditions Prevented: Dirty reads.
- Race Conditions Allowed: Non-repeatable reads, phantom reads.
- Use Case: Most applications (balances safety and performance).
Example: Read Committed Behavior
Transaction 1:
BEGIN;
UPDATE products SET price = 200 WHERE id = 1; -- Uncommitted
Transaction 2 (Read Committed):
SELECT price FROM products WHERE id = 1; -- Returns old price (e.g., 100)
Transaction 1 commits:
COMMIT;
Transaction 2 re-runs SELECT:
SELECT price FROM products WHERE id = 1; -- Returns new price (200)
5.3 Repeatable Read
- Behavior: A transaction sees a consistent snapshot of the database for all
SELECTs. Re-reading the same data returns the same result, even if others commit changes. - Race Conditions Prevented: Dirty reads, non-repeatable reads.
- Race Conditions Allowed: Phantom reads (new rows matching a query may appear).
- Use Case: Reporting or analytics requiring consistent data over time.
Syntax:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
5.4 Serializable
- Behavior: Highest isolation level. Transactions behave as if run sequentially, even if concurrent. Prevents all race conditions.
- Race Conditions Prevented: Dirty reads, non-repeatable reads, phantom reads.
- Use Case: Critical financial systems or scenarios where strict consistency is required.
- Tradeoff: May cause more transaction rollbacks due to serialization failures.
Example: Serializable Preventing Phantom Reads
Transaction 1 (Serializable):
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT COUNT(*) FROM users WHERE age > 30; -- Returns 5
Transaction 2:
BEGIN;
INSERT INTO users (name, age) VALUES ('Bob', 35);
COMMIT; -- New row matches Transaction 1's query
Transaction 1 re-runs SELECT:
SELECT COUNT(*) FROM users WHERE age > 30; -- Still returns 5 (phantom read prevented)
6. Concurrency Control and Locking
6.1 MVCC: Multi-Version Concurrency Control
PostgreSQL uses MVCC to enable high concurrency:
- Each transaction sees a consistent snapshot of the database (based on its start time).
- Writers do not block readers, and readers do not block writers (under
Read Committed/Repeatable Read). - Old versions of rows are cleaned up by the
VACUUMprocess.
6.2 Lock Types in PostgreSQL
Despite MVCC, locks are still used for writes and schema changes:
- Row-Level Locks: Held by
UPDATE,DELETE, orSELECT FOR UPDATEon specific rows. - Table-Level Locks: Held by DDL (e.g.,
ALTER TABLE) orLOCK TABLEcommands. - Advisory Locks: User-defined locks for application-level coordination (e.g.,
pg_advisory_lock).
6.3 Deadlocks and How to Avoid Them
A deadlock occurs when two transactions wait indefinitely for each other’s locks (e.g., T1 locks Row A and waits for Row B; T2 locks Row B and waits for Row A).
How PostgreSQL Handles Deadlocks: It detects deadlocks and aborts one transaction with a deadlock detected error.
Avoiding Deadlocks:
- Access resources (rows/tables) in a consistent order across transactions.
- Keep transactions short to minimize lock持有时间.
- Use lower isolation levels (e.g.,
Read Committed) where possible. - Use
SELECT FOR UPDATE SKIP LOCKEDto skip locked rows instead of waiting.
7. Practical Use Cases
7.1 Fund Transfer Example
Ensure atomicity when transferring funds between accounts:
BEGIN;
-- Deduct from Alice
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
-- Add to Bob
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
-- Verify no negative balances
IF (SELECT balance FROM accounts WHERE user_id = 1) < 0 THEN
ROLLBACK;
RAISE EXCEPTION 'Insufficient funds';
END IF;
COMMIT;
7.2 Batch Processing with Savepoints
Process 1000 records, committing in chunks to avoid long transactions:
BEGIN;
FOR i IN 1..1000 LOOP
INSERT INTO logs (message) VALUES ('Record ' || i);
IF i % 100 = 0 THEN -- Commit every 100 records
COMMIT;
BEGIN; -- Start new transaction
END IF;
END LOOP;
COMMIT;
8. Best Practices for Transaction Management
- Keep Transactions Short: Long transactions block others and increase rollback risk.
- Use the Lowest Isolation Level Needed: Prefer
Read Committedunless strict consistency is required. - Avoid Locking Unnecessary Data: Use
SELECT FOR UPDATEonly on rows you need to modify. - Handle Errors Explicitly: Use
TRY/CATCHblocks (PL/pgSQL) or application-level error handling to roll back on failure. - Monitor Long-Running Transactions: Use
pg_stat_activityto identify blocking transactions:SELECT pid, now() - query_start AS duration, query FROM pg_stat_activity WHERE state = 'active' AND now() - query_start > '5 minutes';
9. Troubleshooting Common Transaction Issues
- Long-Running Transactions Blocking Others: Use
pg_locksto identify locks:SELECT a.pid, a.query, l.locktype, l.mode, l.relation::regclass FROM pg_locks l JOIN pg_stat_activity a ON l.pid = a.pid WHERE l.relation IS NOT NULL; - Deadlocks: Check the PostgreSQL log for
deadlock detectedmessages. Fix by reordering lock acquisition. - Transaction ID Wraparound: Run
VACUUMregularly to prevent XID wraparound (critical for long-running systems).
10. Conclusion
Transaction management is a cornerstone of reliable database applications, and PostgreSQL offers powerful tools to ensure ACID compliance, concurrency, and performance. By mastering BEGIN/COMMIT/ROLLBACK, savepoints, isolation levels, and locking, you can build systems that handle errors gracefully, scale under load, and maintain data integrity.
Remember: the key to effective transaction management is balancing consistency, performance, and simplicity. Always test concurrency scenarios and monitor transaction behavior in production to catch issues early.