PostgreSQL Deep Dive
Go below the surface of PostgreSQL: MVCC tuple mechanics, the write-ahead log, vacuum and bloat, replication, memory tuning and the introspection views that reveal what a production server is really doing.
What is this?
PostgreSQL is an open-source relational database built on MVCC, the write-ahead log and an extensible type system. Every update creates a new row version (tuple) and leaves the old one for VACUUM to reclaim. The WAL guarantees durability and feeds streaming replication to read replicas. Production work centers on tuning memory settings, controlling bloat and vacuum, and reading pg_stat_* views.
Why do you need it?
- MVCC gives snapshot isolation with zero reader-writer blocking — the basis of PostgreSQL's concurrency.
- WAL provides crash safety and streams to replicas for high availability and scale-out reads.
- The extension ecosystem (PostGIS, pgvector, TimescaleDB) makes one server cover spatial, vector and time-series work.
- A rich type and index family (btree, hash, GIN, GiST, BRIN) handles JSON, full-text, spatial and vector data.
- A cost-based optimizer plus EXPLAIN makes performance work measurable rather than guesswork.
Mental model
A whiteboard you never erase, copying rows downward
PostgreSQL MVCC
New versions are written below the old ones so a mid-read is never disturbed; the janitor (VACUUM) eventually wipes the old notes once everyone has left.
How it works
- An INSERT writes a new tuple into the heap with xmin = the current transaction; its WAL record is written first.
- An UPDATE writes a new tuple and marks the old one dead (xmax set), without modifying the original page in place.
- A reader takes a snapshot and checks xmin/xmax to decide tuple visibility — no locks are taken.
- autovacuum sweeps dead tuples, reclaiming space and refreshing the free-space map.
- WAL segments are flushed at commit (subject to synchronous_commit) to guarantee durability.
- WAL archiving and replication slots stream changes to standby servers for failover and read load.
PostgreSQL's MVCC write path and why VACUUM exists.
Key commands
SELECT version();Show the server version, build and platform.
EXPLAIN (ANALYZE, BUFFERS) SELECT ...Run a query and report actual timings, buffer reads and rows.
SHOW shared_buffers; SHOW work_mem;Inspect key memory settings for the session.
shared_buffers: 128MB\nwork_mem: 4MB
SELECT pg_size_pretty(pg_total_relation_size('orders'));Report a table's size including indexes and bloat.
1 GB
SELECT relname, n_live_tup, n_dead_tup FROM pg_stat_user_tables WHERE relname = 'orders';Check live vs dead tuples to gauge bloat and vacuum state.
SELECT pid, state, wait_event_type, query FROM pg_stat_activity;List running queries, durations and wait states.
Examples
Analyzing a hot query and tuning work_mem
-- Measure actual timings, buffer reads and row countsEXPLAIN (ANALYZE, BUFFERS)SELECT u.email, count(*)FROM orders oJOIN users u ON u.id = o.user_idWHERE o.status = 'paid'GROUP BY u.emailORDER BY count(*) DESC; -- A sort-heavy query spills to disk when work_mem is too small.-- Raise it for the session and compare the plan.SET work_mem = '64MB'; SHOW work_mem;SHOW max_connections;EXPLAIN (ANALYZE, BUFFERS) shows actual execution time, buffer reads and whether the sort or hash spilled to disk. If you see 'Sort Method: external merge Disk', the query exceeded work_mem; raising it per-session can keep the sort in memory. SHOW work_mem and max_connections reveal the math behind a memory budget — work_mem is multiplied across concurrent operations.
In production
- OLTP applications run Postgres as the system of record with streaming replicas for reads and failover.
- Geospatial teams use PostGIS for spatial indexes and geocoding on top of standard tables.
- AI applications store embeddings in pgvector columns with HNSW/ivfflat indexes for similarity search.
- Log and event workloads use TimescaleDB hypertables for time-series partitioning and retention.
Best practices
- Right-size shared_buffers (typically 25% of RAM) and let the OS cache the rest; do not starve work_mem.
- Match fillfactor (e.g., 70) on frequently-updated tables to slow page-level bloat.
- Set max_connections honestly and use a connection pooler (PgBouncer) to reduce per-connection overhead.
- Monitor vacuum lag with pg_stat_user_tables and never disable autovacuum on production tables.
- Test config changes on staging and measure with EXPLAIN (ANALYZE, BUFFERS) before rolling out.
Common mistakes
- Maxing out shared_buffers at 75% of RAM, starving the OS page cache and hurting cold reads.
- Running with synchronous_commit=off everywhere to gain latency, then losing committed transactions in a power failure.
- Ignoring autovacuum errors and letting dead tuples balloon until tables bloat to gigabytes.
- Tuning work_mem globally to a large value and multiplying it by max_connections for a huge total.
Common issues
ERROR: out of memorywork_mem is too small for a sort or hash, or memory is exhausted.
Raise work_mem for the query, reduce max_connections, and check for swap thrashing.
database is not accepting commands to avoid wraparound data lossTransaction ID wraparound is imminent because autovacuum fell behind.
Run VACUUM immediately; lower autovacuum_freeze_max_age and monitor via pg_stat_user_tables.
PANIC: could not locate a valid checkpoint recordWAL files are missing or the data directory was corrupted.
Restore from the last base backup plus WAL archive; verify archive_command and replication slots.
Debugging tips
- Watch pg_stat_activity for long-running queries and use pg_stat_statements to rank total query time.
- Track bloat with pg_total_relation_size and n_dead_tup; run VACUUM (VERBOSE) to confirm reclamation.
- Use pg_locks to resolve lock contention and blocking chains.
- Scan the Postgres log for 'checkpoints are occurring too frequently' to tune checkpoint settings.
Interview checkpoint
Hands-on lab
- Generate churn and measure dead tuples
- Observe MVCC in action with snapshots
- Run and verify VACUUM reclamation
- Analyze a hot query with EXPLAIN (ANALYZE, BUFFERS)
Operating a Production Postgres
45 min · advanced
Quick quiz
Which background process reclaims dead tuples in PostgreSQL?
A PostgreSQL reader blocks a writer when both touch the same row.
pg_stat_user_tables shows 50 million dead tuples on a heavily-updated table. What is the immediate priority?
Cheat sheet
shared_buffersIn-server cache, ~25% of RAMwork_memPer sort/hash operationeffective_cache_sizeOS cache estimate for plannersynchronous_commiton = durable, off = fastercheckpoint_timeoutHow often dirty buffers flushpg_stat_activityRunning queries and waitspg_stat_user_tablesLive/dead tuple countspg_stat_statementsTop queries by total timepg_locksLock holders and waiterspg_relation_size()Table/index byte sizes