Transactions

Transactions

1. Transaction Concept

A transaction is a collection of operations that constitute a single logical unit of work.

┌──────────────────────────────────────────────────────────┐
│                   Account Transfer Transaction           │
├──────────────────────────────────────────────────────────┤
│  1. Deduct 100,000 from Account A                        │
│  2. Add 100,000 to Account B                             │
│  → Both must succeed or both must fail                   │
└──────────────────────────────────────────────────────────┘

2. ACID Properties

Property English Description
Atomicity Atomicity All or nothing
Consistency Consistency Data consistency maintained before and after transaction
Isolation Isolation Concurrent transactions don't interfere
Durability Durability Committed transactions are permanently stored

3. Basic Transaction Commands

BEGIN / COMMIT / ROLLBACK

-- Start transaction
BEGIN;
-- Or
START TRANSACTION;

-- Perform operations
UPDATE accounts SET balance = balance - 100000 WHERE id = 1;
UPDATE accounts SET balance = balance + 100000 WHERE id = 2;

-- Commit (confirm changes)
COMMIT;

-- Or rollback (cancel changes)
ROLLBACK;

Practice Example

-- Create table
CREATE TABLE accounts (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    balance NUMERIC(12, 2) DEFAULT 0
);

INSERT INTO accounts (name, balance) VALUES
('Kim', 1000000),
('Lee', 500000);

-- Transfer transaction
BEGIN;

UPDATE accounts SET balance = balance - 100000 WHERE name = 'Kim';
UPDATE accounts SET balance = balance + 100000 WHERE name = 'Lee';

-- Check
SELECT * FROM accounts;

-- Commit or cancel
COMMIT;  -- Or ROLLBACK;

4. Autocommit

psql is in autocommit mode by default.

-- In autocommit mode, each statement is an individual transaction
INSERT INTO accounts (name, balance) VALUES ('Park', 300000);
-- Immediately committed

-- Disable autocommit
\set AUTOCOMMIT off

-- Now explicit COMMIT required
INSERT INTO accounts (name, balance) VALUES ('Choi', 400000);
COMMIT;

-- Re-enable autocommit
\set AUTOCOMMIT on

5. SAVEPOINT

Create partial rollback points within a transaction.

BEGIN;

INSERT INTO accounts (name, balance) VALUES ('New1', 100000);
SAVEPOINT sp1;

INSERT INTO accounts (name, balance) VALUES ('New2', 200000);
SAVEPOINT sp2;

INSERT INTO accounts (name, balance) VALUES ('New3', 300000);

-- Rollback to sp2 (cancel only New3)
ROLLBACK TO SAVEPOINT sp2;

-- Rollback to sp1 (cancel New2, New3)
ROLLBACK TO SAVEPOINT sp1;

-- Commit all (save only New1)
COMMIT;

SAVEPOINT Management

-- Release SAVEPOINT
RELEASE SAVEPOINT sp1;

-- Overwrite SAVEPOINT (recreate with same name)
SAVEPOINT mypoint;
-- ... work ...
SAVEPOINT mypoint;  -- Replace with new point

6. Transaction Isolation Levels

Determines the degree of isolation between concurrently executing transactions.

Isolation Level Types

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

*PostgreSQL's REPEATABLE READ also prevents Phantom Reads

PostgreSQL Default

PostgreSQL's default isolation level is READ COMMITTED.

Setting Isolation Level

-- Per-transaction setting
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Or
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- Session-wide setting
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Check current isolation level
SHOW transaction_isolation;

7. Concurrency Problems

Dirty Read

Reading uncommitted data → Does not occur in PostgreSQL

Non-repeatable Read

Reading the same data twice in the same transaction returns different values

-- Transaction A
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- 1000000

-- Transaction B updates and commits
-- UPDATE accounts SET balance = 900000 WHERE id = 1; COMMIT;

SELECT balance FROM accounts WHERE id = 1;  -- 900000 (different value!)
COMMIT;

Phantom Read

Same query returns different number of rows

-- Transaction A
BEGIN;
SELECT COUNT(*) FROM accounts WHERE balance > 500000;  -- 2 rows

-- Transaction B inserts new row and commits
-- INSERT INTO accounts VALUES (...); COMMIT;

SELECT COUNT(*) FROM accounts WHERE balance > 500000;  -- 3 rows (phantom row!)
COMMIT;

8. Isolation Level Behavior

READ COMMITTED (default)

BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Can see changes committed by other transactions immediately
SELECT * FROM accounts;  -- Latest committed data

COMMIT;

REPEATABLE READ

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- Sees snapshot from transaction start time
SELECT * FROM accounts;

-- Same result even if other transactions commit
SELECT * FROM accounts;  -- Same

COMMIT;

SERIALIZABLE

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Most strict isolation
-- May fail with serialization error
SELECT * FROM accounts WHERE balance > 500000;
UPDATE accounts SET balance = balance + 10000 WHERE id = 1;

COMMIT;
-- ERROR: could not serialize access due to concurrent update
-- (if conflicts with other transactions)

9. Locking

Row-Level Locks

-- SELECT FOR UPDATE: Lock while querying
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- Other transactions cannot modify/delete this row

UPDATE accounts SET balance = balance - 100000 WHERE id = 1;
COMMIT;

-- SELECT FOR SHARE: Shared lock (allow reads, prevent writes)
SELECT * FROM accounts WHERE id = 1 FOR SHARE;

Lock Options

-- Don't wait, fail immediately
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;

-- Wait for specified time
SELECT * FROM accounts WHERE id = 1 FOR UPDATE SKIP LOCKED;

Table-Level Locks

-- Explicit table lock (rarely used)
LOCK TABLE accounts IN EXCLUSIVE MODE;

10. Deadlock

Two transactions waiting for each other's locks

-- Transaction A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Locks id=1

-- Transaction B
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 2;
-- Locks id=2

-- Transaction A
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- Waiting for id=2...

-- Transaction B
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
-- Waiting for id=1... → Deadlock!

-- PostgreSQL automatically aborts one transaction
-- ERROR: deadlock detected

Preventing Deadlocks

-- Always acquire locks in the same order
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- Always smaller id first
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

11. Practice Examples

Practice 1: Basic Transaction

-- Account transfer
CREATE OR REPLACE PROCEDURE transfer(
    from_id INTEGER,
    to_id INTEGER,
    amount NUMERIC
)
AS $$
BEGIN
    -- Withdrawal
    UPDATE accounts SET balance = balance - amount WHERE id = from_id;

    -- Check balance
    IF (SELECT balance FROM accounts WHERE id = from_id) < 0 THEN
        RAISE EXCEPTION 'Insufficient balance';
    END IF;

    -- Deposit
    UPDATE accounts SET balance = balance + amount WHERE id = to_id;

    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
$$ LANGUAGE plpgsql;

-- Usage
CALL transfer(1, 2, 100000);

Practice 2: Using SAVEPOINT

BEGIN;

-- Insert base data
INSERT INTO orders (user_id, amount) VALUES (1, 50000);
SAVEPOINT order_created;

-- Attempt to reduce stock
UPDATE products SET stock = stock - 1 WHERE id = 10;

-- Check stock
IF (SELECT stock FROM products WHERE id = 10) < 0 THEN
    ROLLBACK TO SAVEPOINT order_created;
    -- Keep order but cancel stock reduction
END IF;

COMMIT;

Practice 3: Testing Isolation Levels

Test with two terminals.

-- Terminal 1
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM accounts;

-- Terminal 2
UPDATE accounts SET balance = balance + 50000 WHERE id = 1;
COMMIT;

-- Terminal 1
SELECT * FROM accounts;  -- Old value (snapshot)
COMMIT;

SELECT * FROM accounts;  -- Now shows changed value

Practice 4: FOR UPDATE Lock

-- Check and reduce stock (concurrency-safe)
BEGIN;

-- Query with lock
SELECT stock FROM products WHERE id = 1 FOR UPDATE;

-- Check and reduce stock
UPDATE products
SET stock = stock - 1
WHERE id = 1 AND stock > 0;

COMMIT;

12. Transaction Monitoring

-- Check currently running transactions
SELECT
    pid,
    now() - xact_start AS duration,
    query,
    state
FROM pg_stat_activity
WHERE xact_start IS NOT NULL;

-- Check queries waiting for locks
SELECT
    blocked.pid AS blocked_pid,
    blocking.pid AS blocking_pid,
    blocked.query AS blocked_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid));

Next Steps

Learn about triggers in 12_Triggers.md!

to navigate between lessons