▣datahall
← back to gallery
client-serverCest. 1996PostgreSQL License

PostgreSQL

The most-extensible open-source RDBMS: MVCC, a cost-based planner, and 30+ years of catalog evolution.

PostgreSQL is a client-server object-relational database with a process-based architecture, multi-version concurrency control, a cost-based query optimizer, and a deeply extensible type system. It supports user-defined types, operators, index methods, procedural languages, and foreign data wrappers, all cataloged in system tables.

Architecture
process per connection
Isolation
MVCC + SSI
Indexes
B-tree, hash, GIN, BRIN, GiST
Replication
streaming + logical

MVCC without undo logs

Every row version is written in-place with xmin/xmax system columns. Old versions become dead tuples and are reclaimed by VACUUM.

Cost-based planner

The planner enumerates access paths, joins, and aggregations, scores each by an estimated cost, and picks the lowest. Statistics from ANALYZE feed the estimator.

Extensible catalogs

Tables, types, functions, and operators are first-class rows in pg_catalog. Extensions install new rows at runtime, not via recompilation.

Logical and physical replication

Streaming replication ships WAL to replicas; logical replication decodes WAL into row-level change streams. Both can run in parallel.

MVCC and tuple visibility

PostgreSQL never overwrites a row in place. An UPDATE writes a new tuple version and marks the old one as deleted. Visibility is decided per-query by comparing each tuple’s xmin and xmax against the transaction snapshot taken at statement (or transaction) start. This is what makes readers never block writers and writers never block readers.

Dead tuples accumulate and consume space until VACUUM reclaims them. Autovacuum is a background launcher that schedules vacuum workers when the dead-tuple fraction in a table crosses a threshold.

Query lifecycle

A client sends SQL over the wire protocol. The backend parses it into a parse tree, rewrites it against the rule system, analyzes it (binding names to catalog entries), and hands it to the planner. The planner produces a tree of plan nodes: Seq Scan, Index Scan, Hash Join, Aggregate, Sort. The executor pulls tuples top-down through the plan.

Each backend is a separate OS process. A connection-pooling proxy (pgBouncer) is the usual way to keep thousands of clients from spawning thousands of backends.

              CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

EXPLAIN (ANALYZE, BUFFERS)
SELECT relname, n_live_tup
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC
LIMIT 10;
            

Indexes beyond B-tree

PostgreSQL ships with five built-in index access methods. B-tree handles the 80% case. GIN indexes an inverted list per key (great for full-text, JSONB, arrays). BRIN summarizes a range of pages into a tiny min/max (great for time-series with monotonic keys). GiST and SP-GiST are extensible balanced-tree frameworks for spatial, range, and custom types.

Because indexes are plug-in, the community has built hundreds of them: PostGIS for geometry, pg_trgm for fuzzy text, vector for embeddings.

Diagrams

wal-timelineWAL + checkpoint timelineclick nodes for description
timexlog insertappend record at LSNcommitfsync WAL (synchronous_commit)background writerflush dirty bufferscheckpointeradvance redo ptrcrash / power losson restart: replay WAL → consistent state

A walwriter appends to the WAL; a checkpointer flushes dirty buffers and recycles WAL segments. Crash recovery replays from the last checkpoint.

page-microscopeHeap page anatomyclick nodes for description
postgres page · 8 KiBpage header (24 B)pd_lsn · pd_checksum · pd_lower / pd_upper · pd_specialitem-id arraygrows downfree spaceheap tuplesgrows uptuple detail (full mode)t_xmint_xmaxt_cid / t_infomaskuser dataTPD slot exists for HOT updates

Each 8 KiB heap page carries a page header (lsn, tli, free space), an item-pointer array, and tuple headers with xmin/xmax for MVCC.

pipelineRead path: SQL -> parser -> planner -> executor -> heap pageclick nodes for description
clientnetworkwire protocol,parameter bindingparser + analyzerexecutionparse tree, namesresolvedplannerexecutioncost-based access pathsexecutorexecutionpulls tuples top-downheap pagesstoragevisibility checkagainst xmin/xmaxWAL / sharedbuffersdurabilityreplay-safe snapshotreadmode: read

A prepared statement goes through parse, analyze, rewrite, plan, execute. The executor walks heap pages and index pages and applies MVCC visibility per snapshot.

pipelineWrite path: SQL -> WAL append -> heap update -> background VACUUMclick nodes for description
clientnetworkwire protocolparser +analyzerexecutionbind and analyzeplannerexecutioncost-based accesspathsexecutorexecutionwrites new tuple,marks oldWAL appenddurabilityfsync on commitheap pagestoragein-place updatewith xmin/xmaxbackgroundwritermemoryflushes dirty pagesmode: write

Writes append to the WAL, then update heap tuples in place; dead tuples are reclaimed by autovacuum.

walkthroughMVCC visibility, the commit boundary, and recoveryclick nodes for description
v1: balance = 100 (xmin = 10)visible to snapshots before Av2: balance = 250 (xmin = 100)written by tx A — visibility depends on commitreader B (old snapshot) sees 100✖ crash before the commit recordrecovery: replay WAL to last commitreader C sees 100 — A abortedWAL: A-beginA-update v2

visible after recoveryv1 (balance 100)
v2 (250)aborted — never committed
WAL replay stop pointlast committed record
durability rulecommit waits for WAL flush
All steps
  1. Tx A: UPDATE balance 100 → 250 — A new version (v2, xmin = 100) is written and chained; v1 is marked dead-upon-A’s-commit. Nothing in place is overwritten — writers never block readers.
  2. Reader B (old snapshot) still sees 100 — B’s snapshot was taken before A ran. xmin = 100 is in progress ⇒ v2 is invisible to B; it follows the chain to v1 (100). Repeatable reads in action.
  3. Crash before the commit record — A’s WAL records exist, but the commit record was never written or flushed. Durability is defined by that record — without it, A never happened.
  4. Recovery replays WAL to the last commit — On restart, WAL is replayed up to the last committed record. A’s half-written v2 has no commit ⇒ treated as aborted; v1 (100) is the visible state.
  5. Reader C sees 100 again — Aborted xids are invisible to every snapshot. Atomicity came from the commit boundary, not from undoing pages.

Transaction visibility, the WAL commit boundary, and crash recovery — PostgreSQL-flavored. Simplified: xmin-only version chains, no xmax/clog details; the durable rule shown here is exact: a transaction exists only once its commit record is flushed to WAL.

Simplified xmin-only model of PostgreSQL MVCC — the durable rule shown (commit waits for WAL flush) is exact; xmax/clog details are in the MVCC chapter.

[caption] Simplified xmin-only model of PostgreSQL MVCC — the durable rule shown (commit waits for WAL flush) is exact; xmax/clog details are in the MVCC chapter.

Stepwise: a new version is invisible until its transaction commits; old-snapshot readers keep seeing v1; crash recovery replays WAL to the last committed record.

diagramStreaming replication topologyclick nodes for description
Replication · Async primary → replicas
Primarywrites + WALReplica Areads + cascadingReplica Breads onlyCascadingreads only

Primary streams WAL; one replica streams further to a cascading replica.

query-planEXPLAIN plan: filtered joinclick nodes for description

A representative plan tree from EXPLAIN (ANALYZE): a nested loop joins an index scan on users to a filtered scan on events; costs are planner units.

  1. 1
    Limit10 rows · cost 42.5
  2. 2
    Sort96 rows · cost 41.9
  3. 3
    Nested Loop96 rows · cost 38.2
  4. 4
    Index Scan using users_pkey12 rows · cost 8.3
  5. 5
    Seq Scan on events (filter: kind = 2)8 rows · cost 28.1
total est. cost 159

[caption] Costs are planner units: 1.0 is one sequential page fetch.

A representative plan tree from EXPLAIN (ANALYZE): a nested loop joins an index scan on users to a filtered scan on events; costs are planner units.

index-statsB-tree shape over 1.2M rowsclick nodes for description

Three levels at the default 90% fill factor: one root, ~32 internal routing pages, and a leaf level that dominates the page count.

L0
1 nodes · ×32
L1
32 nodes · ×160
L2
5.1k nodes
Cardinality
1.2M
distinct indexed values
Selectivity
2.1%
fraction of rows matching a predicate

[caption] Upper levels are pure routing pages; leaves carry heap TIDs.

Three levels at the default 90% fill factor: one root, ~32 internal routing pages, and a leaf level that dominates the page count.