MVCC & VACUUM
Multi-Version Concurrency Control is Postgres’s secret — readers never block writers, writers never block readers. Each transaction sees a snapshot; deletes mark rows for vacuum later. Understanding MVCC explains why VACUUM exists, why bloat happens, and why long transactions are dangerous.
Snapshots, xmin/xmax, vacuum, bloat
EXAMPLE
-- 1) Every row has hidden columns: xmin (insert tx) + xmax (delete tx)
SELECT id, name, xmin, xmax FROM users LIMIT 5;
-- id | name | xmin | xmax
-- ----+------+-------+------
-- 1 | Mara | 12345 | 0
-- 2 | Sam | 12345 | 0
-- xmin = transaction that INSERTED the row
-- xmax = transaction that DELETED/UPDATED the row (0 = current)
-- 2) Snapshot isolation — read your own writes, ignore others'
-- Transaction A:
BEGIN;
SELECT count(*) FROM orders; -- sees committed state at this moment
-- Meanwhile, transaction B inserts 1000 orders and commits.
-- Transaction A still in same txn:
SELECT count(*) FROM orders; -- SAME count as before — snapshot frozen
COMMIT;
-- Now reading sees the new orders
-- 3) Updates are 'delete + insert'
UPDATE users SET email = 'new@example.com' WHERE id = 1;
-- Old row: xmax = current_tx_id (marked deleted)
-- New row: xmin = current_tx_id (newly inserted)
-- The OLD row stays until vacuum cleans it up.
-- This means: an UPDATE-heavy table accumulates dead tuples (bloat).
-- 4) Visibility rules — what each transaction sees
-- A row is visible to a transaction T if:
-- • xmin committed before T started AND
-- • xmax is null OR uncommitted OR committed after T started
-- Each connection has a 'transaction snapshot' = view of committed state at start.
-- 5) Inspecting bloat
SELECT schemaname, relname, n_live_tup, n_dead_tup,
round(100.0 * n_dead_tup / (n_live_tup + n_dead_tup), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 0
ORDER BY dead_pct DESC LIMIT 10;
-- Heavy bloat (50%+) means VACUUM hasn't kept up. Tune autovacuum or run manually.
-- 6) VACUUM — reclaim dead tuples
VACUUM users; -- non-blocking; reclaim space for reuse
VACUUM (VERBOSE) users; -- show progress
VACUUM ANALYZE users; -- reclaim + update statistics
VACUUM FULL users; -- REWRITES table; holds EXCLUSIVE lock; reclaims to disk
-- VACUUM FULL is DANGEROUS in production (locks for the entire duration).
-- For online compaction, use pg_repack extension.
-- 7) Autovacuum — runs automatically
SHOW autovacuum; -- on by default
SHOW autovacuum_vacuum_scale_factor; -- default 0.2 (20% dead)
SHOW autovacuum_naptime; -- default 60s
-- Per-table tuning for hot tables:
ALTER TABLE orders SET (autovacuum_vacuum_scale_factor = 0.05); -- vacuum at 5%
ALTER TABLE orders SET (autovacuum_vacuum_threshold = 1000); -- absolute min
-- 8) Long-running transactions = bloat risk
-- VACUUM can only clean rows that no LIVE transaction needs.
-- A transaction left open for hours blocks cleanup of recent changes.
-- Find long-running txns:
SELECT pid, usename, application_name, state,
clock_timestamp() - xact_start AS xact_duration,
query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL AND state != 'idle'
ORDER BY xact_duration DESC;
-- Idle transactions are the worst:
SELECT pid, usename, application_name, state, xact_start, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start;
-- Kill if needed (carefully):
SELECT pg_cancel_backend(12345); -- ask politely
SELECT pg_terminate_backend(12345); -- force
-- 9) Transaction ID (XID) wraparound
-- Postgres uses 32-bit XIDs that wrap around after ~2 billion transactions.
-- VACUUM 'freezes' old rows by replacing xmin with a special FrozenXID.
-- If wraparound approaches without freezing, Postgres SHUTS DOWN to prevent data loss.
SELECT datname, age(datfrozenxid) FROM pg_database ORDER BY age(datfrozenxid) DESC;
-- Tune autovacuum_freeze_max_age if approaching 200 million.
-- 10) Isolation levels + MVCC
-- READ COMMITTED (default)
-- Each STATEMENT sees a fresh snapshot. Repeatable issues possible.
--
-- REPEATABLE READ
-- Entire TRANSACTION sees same snapshot. Detects serialisation anomalies as errors.
--
-- SERIALIZABLE
-- Like REPEATABLE READ + ensures serial-equivalent ordering. May abort with 40001.
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- run queries
COMMIT; -- may fail with 'could not serialize access'; retry the transaction
-- 11) Avoiding bloat
-- • Short transactions — commit often
-- • Avoid 'idle in transaction' for hours
-- • Bulk DELETE → consider partitioning + DROP partition instead
-- • HOT updates (heap-only tuple) when index fields not changed → less bloat
-- • Fillfactor < 100 leaves space in pages for HOT updates
ALTER TABLE orders SET (fillfactor = 80);
-- 12) pg_visibility — see which rows are visible to autovacuum
CREATE EXTENSION pg_visibility;
SELECT * FROM pg_visibility_map('users') LIMIT 10;
-- 13) Hot-standby + MVCC
-- Replicas read at lag; queries hold snapshot. Long queries on replica delay vacuum on primary
-- (replication slot keeps WAL alive). Watch hot_standby_feedback.
-- 14) Common bugs
-- • Long-running txn keeps bloat from being reclaimed → vacuum stuck; close transactions
-- • 'idle in transaction' connections from connection pool → set idle_in_transaction_session_timeout
-- • VACUUM FULL during peak → locks table; outage
-- • Bulk DELETE then SELECT immediately → dead tuples still visible to query plan
-- • Autovacuum starved by hot tables — increase autovacuum_max_workers
-- • Forgetting that UPDATE creates a NEW row — every column update on hot table doubles bloat
-- • Heavily-updated wide table — fillfactor + HOT updates required
-- • Transaction ID wraparound warnings ignored → emergency single-user mode
-- • Replicas blocking primary vacuum — adjust hot_standby_feedback / max_standby_streaming_delay
-- • Mistaking 'idle' for 'idle in transaction' — they're different; idle is benign
Why it matters
MVCC = each transaction sees a snapshot; updates create new row versions; VACUUM cleans up the dead ones. Long-running transactions block cleanup and grow bloat — keep transactions short, set idle_in_transaction_session_timeout, monitor pg_stat_activity and pg_stat_user_tables.n_dead_tup. Tune autovacuum on hot tables and never run VACUUM FULL in production traffic.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
-- Each row has xmin/xmax. Writers don't block readers. VACUUM (VERBOSE, ANALYZE) orders; VACUUM FULL orders; -- rebuilds table; locks itTry it Yourself »
Discussion
Loading…