π PostgreSQL Internals (important parts only)
1. Process model
- The postmaster forks one backend process per connection (MBs each + a context switch) β connections are expensive β pool (HikariCP/pgxpool + PgBouncer)
- Shared memory: shared_buffers (page cache); background processes: checkpointer, background writer, WAL writer, autovacuum launcher/workers, WAL senders (replication)
2. Storage
- Tables/indexes = files of 8 KB pages; a page has line pointers β tuples
- Tuple header:
xmin(inserting txn),xmax(deleting/locking txn),ctid(physical location) - Large values β TOAST (compressed/out-of-line)
3. MVCC
- UPDATE = insert a new tuple version + mark the old oneβs
xmax(no in-place update) - Snapshot: which txn IDs were committed when it was taken; visibility = f(xmin, xmax, snapshot, commit log)
- Read Committed: new snapshot per statement Β· Repeatable Read: one snapshot per transaction (+ serialization errors on conflicting updates) Β· Serializable: SSI tracks read-write dependencies and aborts dangerous structures
- Readers never block writers, and writers never block readers
- HOT updates: if no indexed column changed and the page has room, the new version stays on the same page β no index update (tune
fillfactor)
4. VACUUM
- Dead tuples stay until VACUUM marks the space reusable (it doesnβt shrink files;
VACUUM FULL/pg_repack rewrite them) - Updates the visibility map (enables index-only scans) and the free space map
- Freezing prevents 32-bit transaction ID wraparound (an emergency shutdown if ignored)
- Long-running transactions or stale replication slots block cleanup β bloat
5. WAL (write-ahead log)
- Changes are written to the WAL before data pages; commit = WAL flushed (
synchronous_commit); data pages are written later by the checkpointer - Crash recovery = replay WAL from the last checkpoint; full-page writes after each checkpoint protect against torn pages
- Streaming replication = shipping WAL; logical decoding (used by Debezium CDC) reads WAL via replication slots β β οΈ an abandoned slot retains WAL forever β the disk fills up
6. Indexes & planner
- B-tree (Lehman-Yao, concurrent-friendly); GIN (jsonb, arrays, full-text), GiST, BRIN (huge append-only tables), HNSW/IVFFlat via pgvector
- The planner estimates row counts from statistics (
ANALYZE: histograms, most-common values, n_distinct) β costs plans β picks seq/index/bitmap scans and nested loop/hash/merge joins - Bad estimates (correlated columns, stale stats) β bad plans β
CREATE STATISTICS,ANALYZE
7. Locks
- Row locks live in the tuple (
xmax+ multixact), not in memory β unlimited row locks - Table-level lock modes; the lock queue: an
ALTER TABLEwaiting forACCESS EXCLUSIVEblocks every later query β uselock_timeoutin migrations β - Deadlock detector runs after
deadlock_timeout; advisory locks for app-level coordination
π¬ Prove it
-
SELECT ctid, xmin, xmax, * FROM t;β update a row β see the new ctid/xmin;pageinspectβ see the dead tuple - Reproduce non-repeatable read (RC), a lost update, and write skew (RR) β fix with SERIALIZABLE or
FOR UPDATE - Hold a transaction open for 10 min while updating a hot table β watch bloat (
pg_stat_user_tables.n_dead_tup) β commit β VACUUM - Lock-queue outage: a long SELECT +
ALTER TABLE ADD COLUMN+ new SELECTs β all block β fix withlock_timeout - Create a logical replication slot, stop consuming, and watch
pg_replication_slotsretain WAL - Stale-statistics bad plan β
ANALYZEβ a plan change inEXPLAIN - Index-only scan appears only after VACUUM updates the visibility map
Interview questions interview-q
How MVCC works in Postgres Β· why VACUUM exists Β· isolation levels + anomalies (write skew!) Β· what WAL is and how replication/CDC use it Β· why connections are expensive Β· why a migration took down production (the lock queue)