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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.