PostgreSQL Essentials
Theme
Palette
Download
Caption (tap to copy)
📖 Deep dive (full written explanation)
This is the engineering-internals post. The promise is that Postgres stops feeling magical once you can name the four mechanisms doing the work: MVCC for concurrency, the planner for choosing how to run a query, indexes for fast lookup, and the WAL for durability. You don't need to implement any of them, but understanding them is what separates someone who uses Postgres from someone who can diagnose it.
We'll trace a query from text to result, then follow a write from your statement to durable disk, and end on the maintenance — VACUUM — that the whole MVCC design quietly depends on.
MVCC, Multi-Version Concurrency Control, is the heart of how Postgres handles many users at once. Instead of locking rows so only one transaction can touch them, it keeps multiple versions of a row. Each transaction sees a snapshot of the database frozen at the moment it began, so it gets a consistent view even while others are writing.
The headline consequence: readers never block writers, and writers never block readers. A long-running report can run against a snapshot while transactions keep updating the same tables. This is why Postgres feels smooth under concurrency — but it's also why dead row versions accumulate, which sets up the need for VACUUM later in the post.
This pipeline shows the four phases every query passes through. Parse turns your SQL text into an internal tree and checks syntax. Plan is where the optimizer evaluates competing strategies and picks the cheapest. Execute runs that chosen plan, reading data via sequential scans, index scans, and join algorithms. Return streams the resulting rows back to the client.
Most performance work lives in the Plan and Execute phases. When a query is slow, you're almost always asking: did the planner pick a bad plan, and why? EXPLAIN, shown shortly, is the window into exactly that decision.
The planner is a cost-based optimizer. Postgres maintains statistics about each table — how many rows, how values are distributed, how many distinct values a column has. From those, it estimates how expensive each possible plan would be and chooses the lowest estimate. For one query it might pick an index scan; for another touching most of the table, a sequential scan is genuinely faster.
The practical lesson is that statistics quality directly drives plan quality. After a big data load or major change, statistics can be stale, the estimates wrong, and a once-fast query suddenly slow. Running ANALYZE refreshes them. This is why 'it got slow and nothing changed in the code' is so often a statistics or VACUUM story.
Indexes are the difference between seeking and scanning. Without one, finding rows matching a condition means reading every row in the table — fine for a hundred rows, catastrophic for ten million. A B-tree index is a balanced, sorted structure that lets Postgres navigate to matching values in logarithmic time, the way you'd use a book's index instead of reading every page.
The tradeoff is real and worth respecting: every index must be updated on every insert, update, and delete to the indexed columns, and it consumes disk. So you index the columns you actually filter, join, and sort on — not every column hopefully. Over-indexing slows writes and bloats storage for reads that never happen.
EXPLAIN is the most important diagnostic tool in Postgres, and EXPLAIN ANALYZE actually runs the query and reports real timings and row counts. The skill is reading the output: 'Index Scan' means it used an index (usually good for selective queries); 'Seq Scan' means it read the whole table (a red flag on a large table with a selective filter).
Beyond the scan type, compare the planner's estimated rows to the 'actual' rows. A large gap signals stale statistics misleading the optimizer. The 'actual time' on each node shows where the real cost concentrates. Learning to read this output turns performance tuning from guesswork into evidence.
Durability in Postgres rests on the Write-Ahead Log. The rule is in the name: before any change is applied to the actual data files, a record of that change is written to the WAL and flushed to disk. So even if the server crashes immediately after, recovery replays the WAL and reconstructs every committed transaction.
This design also pays a second dividend. Because the WAL is a complete, ordered stream of changes, Postgres can ship it to standby servers and have them replay it, giving you streaming replication and read replicas. The same mechanism that protects you from a crash also powers high availability.
This flow diagram shows why the WAL ordering matters. A change first becomes a WAL record that is appended and flushed; the data page is modified in an in-memory buffer; and only later, at a checkpoint, are the dirty pages written to the main data files on disk. The WAL write is the cheap, sequential, durable step that happens on the critical path; the expensive random data-file writes are batched and deferred.
Understanding this explains a lot of Postgres behavior: commit latency is tied to flushing the WAL, checkpoints can cause periodic I/O spikes, and crash recovery time depends on how much WAL must be replayed since the last checkpoint.
VACUUM is the maintenance that MVCC makes necessary. Every UPDATE writes a new row version and leaves the old one as a dead tuple; every DELETE leaves a dead tuple too. VACUUM scans for those dead tuples and makes their space reusable, and it also updates the planner statistics. Autovacuum runs this automatically in the background based on activity thresholds.
When vacuuming falls behind — commonly after bulk updates or deletes, or with very high write rates — tables and indexes bloat, queries slow down, and in extreme cases you risk transaction-id wraparound problems. Treating autovacuum as something to monitor and tune, rather than ignore, is a hallmark of running Postgres well at scale.
This recap names the five mechanisms so you can recall the whole engine on demand. The chain of reasoning connects them: MVCC enables smooth concurrency but produces dead rows; VACUUM cleans those up and refreshes statistics; the planner uses statistics to choose plans; indexes give the planner fast options; and the WAL guarantees that whatever commits is durable and replicable.
Hold these five together and most Postgres behavior — good and bad — becomes explainable rather than mysterious.
That's the engine. You can now reason about why a query is slow (plan, index, or statistics), why writes are durable (WAL), why concurrency is smooth (MVCC), and why maintenance matters (VACUUM).
Next we get concrete and hands-on: a small but real schema with constraints, inserts, a join, an index, and a transaction — all runnable SQL you can paste straight into psql and watch work.