See It Flow
Interactive guide · 8 chapters · ~24 min read

PostgreSQL Fundamentals

What PostgreSQL is, why relational databases exist, and how tables, indexes, transactions, MVCC, locks, and isolation levels fit together — the mental model every deeper Postgres topic builds on.

Mangesh MandlikLast updated:
01 · Foundations

What is PostgreSQL?

PostgreSQL is an open-source, object-relational database management system — a single server process that stores structured data on disk, enforces a strict schema, and guarantees ACID transactions.

Postgres organizes data into tables made of rows and columns, each table living inside a schema, inside a database. Every write goes through a parser, a planner, and an executor before touching disk, and every change is protected by a transaction so the database is never left in a half-updated state.

ClientSQLParserPlannerExecutorPagesheap
The path every SQL statement takes: parse → plan → execute → touch pages on disk.

PostgreSQL Architecture Diagram: How a Query Flows

The diagram above traces the architecture along the query path: a statement passes through the parser, planner, and executor, then reads or writes pages on disk. A transaction protects every change, and the write-ahead log is flushed to disk before a commit returns.

Why use a relational database at all

Strong consistency guarantees

Foreign keys, unique constraints, and check constraints are enforced by the database itself, not by application code that can be bypassed or buggy.

Flexible, ad-hoc queries

SQL lets you ask questions you didn't anticipate when you designed the schema — joins across tables, aggregations, window functions — without writing new code.

ACID transactions

Multiple writes across multiple tables either all happen or none happen, even under concurrent load or a mid-transaction crash.

Mature tooling and ecosystem

Decades of operational experience: replication, backup tooling, monitoring extensions, and a huge base of engineers who already know it.

Interactive

Visual Learning

Follow a single query and a single transaction through Postgres's internals — from the planner's cost estimate to the MVCC row version it reads to the WAL record that makes it durable.

02 · The data model

Tables, Rows, and Primary/Foreign Keys

A relational schema is a set of tables connected by keys — the discipline that keeps data consistent as an application grows.

Table

A named collection of rows sharing the same columns and types. Physically, a table is a sequence of fixed-size pages on disk (see Chapter 3).

Row (tuple)

A single record. Internally called a tuple — the term you'll see throughout Postgres' MVCC machinery.

Primary key

A column (or set of columns) that uniquely identifies each row. Postgres automatically creates a unique B-Tree index to enforce it.

Foreign key

A column that must match a primary key in another table. Enforced at write time — you cannot insert an order for a customer_id that doesn't exist.

03 · Physical layout

How Data is Stored — Pages and Tuples

A table isn't a spreadsheet — it's a sequence of fixed-size 8 KB pages, each packed with row versions.

Page 1Page 2Page 3
Tables are sequences of 8 KB pages. Each page holds one or more tuples plus a small header.

When you INSERT, Postgres places a tuple in a page that has room (Postgres consults the free space map to find one). When no existing page has space, a new page is allocated. Rows are never split across pages — a value too large for a page (like a big JSON blob) is instead TOASTed: compressed and/or moved to a side table, leaving a small pointer in the main tuple.

04 · Speed

Indexes — Why Queries Get Fast

An index is a separate, ordered data structure that lets Postgres jump straight to the rows a query needs, instead of reading every page.

root<500k≥500kleafpage-2leafpage-5leafpage-8leafpage-11
A B-Tree index: a handful of comparisons narrow a million rows down to a single page.

The default index type is a B-Tree — a balanced tree where leaf nodes point at heap pages. Looking up a value takes O(log n) comparisons instead of the O(n) page reads a sequential scan requires.

B-Tree

Best for equality and range queries (=, <, BETWEEN, ORDER BY). The default and by far the most common index type.

Hash index

Equality-only lookups. Rarely used in practice — B-Tree covers the equality case too, with more flexibility.

GIN / GiST

Used for full-text search, array containment, and geospatial queries (PostGIS) — structures that don't fit a simple ordered tree.

05 · Under the hood

Query Execution — Parser, Planner, Executor

Every SQL statement travels through three stages before a single row is touched.

Parser

Tokenizes the SQL text, checks grammar, and confirms every referenced table/column exists. Produces a parse tree.

Planner (optimizer)

Given the parse tree, evaluates candidate execution strategies (sequential scan, index scan, various join orders and algorithms) and picks the one with the lowest estimated cost — a unitless number combining I/O and CPU estimates.

Executor

Runs the chosen plan node-by-node, pulling rows up through the plan tree (joins, filters, sorts) until the final result set is produced.

06 · Reliability

Transactions & ACID

A transaction groups multiple statements into one all-or-nothing unit — the guarantee that makes a banking transfer safe.

Atomicity

Every statement in a transaction commits, or none do. A crash mid-transaction rolls everything back.

Consistency

Constraints (foreign keys, checks, uniqueness) are never violated, even transiently, from the outside observer's point of view.

Isolation

Concurrent transactions don't see each other's uncommitted changes — the exact degree is configurable (see Chapter 8).

Durability

Once a transaction commits, the change survives a crash — guaranteed by the Write-Ahead Log (WAL), which is fsynced to disk before commit returns.

07 · Concurrency

MVCC — How Postgres Handles Concurrent Users

Postgres never blocks a reader to satisfy a writer, or a writer to satisfy a reader — the trick is that both operate on different row versions.

Readersnap @100tuple v1xmin=90 xmax=105tuple v2xmin=105Writertxn 105
An UPDATE never overwrites in place — it marks the old tuple with xmax and inserts a new tuple with a fresh xmin.

Every tuple carries two hidden columns: xmin (the id of the transaction that created it) and xmax (the id of the transaction that deleted/replaced it, if any). A transaction's snapshot determines which tuple versions it's allowed to see, based on which transactions were committed when the snapshot was taken.

08 · Guardrails

Locks & Isolation Levels

MVCC handles read/write concurrency for free. Write/write concurrency still needs locks — and how much you're protected from depends on the isolation level.

Row lock

Taken automatically by UPDATE/DELETE/SELECT FOR UPDATE on the specific rows touched. Two transactions updating different rows never block each other.

Deadlock

Occurs when two transactions each hold a lock the other wants, forming a cycle. Postgres' background detector finds the cycle and aborts one transaction.

Read Committed

The default. Each statement sees a fresh snapshot — you can get non-repeatable reads within one transaction.

Repeatable Read

One snapshot for the whole transaction. Prevents non-repeatable and phantom reads, but if you try to update a row that a concurrent transaction has changed since your snapshot, Postgres raises a serialization error and the transaction must be retried.

Serializable

The strongest level — guarantees the outcome is equivalent to running transactions one at a time. Implemented by detecting and aborting conflicting transactions, so the application must retry.

Key takeaways
  • Postgres is a full query engine, not just storage: parser → planner → executor.
  • Tables are sequences of fixed 8 KB pages; rows are called tuples.
  • B-Tree indexes turn O(n) sequential scans into O(log n) lookups — at the cost of slower writes.
  • MVCC means readers and writers never block each other; UPDATE creates a new tuple version instead of overwriting.
  • Locks handle write/write conflicts; the isolation level controls how much concurrency you're protected from.