Your technical library
PostgreSQL
10 modules · 96 topics
About this book
PostgreSQL
This course explains why PostgreSQL behaves the way it does — why an idle connection can slow everyone else, why a table can double in size while nobody deletes anything, why the same query is fast one day and slow the next, why a standby cancels your report — and what each of those behaviours costs you in production. It assumes no PostgreSQL experience beyond its prerequisite courses, and teaches you to predict it under load, debug it from evidence, and defend an architectural choice in a review. It is not a SQL course.
It builds on four others. The DBMS course builds a database engine from first principles — transactions and isolation, multiversion concurrency, slotted pages, buffer pools, write-ahead logging, lock managers, query optimisers. System Design teaches what changes across machines: replication and failover. Operating Systems covers the processes, shared memory, page cache and I/O underneath. Computer Networking supplies a fourth foundation, used only in the Security module: the TLS handshake, server identity and certificate chains. This course does not repeat them. It shows the particular, often unusual, answers one engine gave, recaps each general idea in a sentence with a link back to where it is taught, and compares PostgreSQL's answer with what MySQL, Oracle and SQL Server chose — including where PostgreSQL's is the worse one.
It is one chain of consequences. Architecture and the founding trade-offs starts with where PostgreSQL came from — a Berkeley research project of 1986 — and the decisions it has never reversed: never overwrite a row, one operating-system process per connection. It then builds the runtime every later module assumes: the shared cache of pages, the locks that guard it and the tables, and the catalogs in which the database describes itself. Physical storage opens a page and a row and finds every version's history written into them. WAL and durability shows how a change to that page survives a power cut, and what each durability setting can lose.
MVCC turns the row's fields into the visibility rule, and follows the bill for keeping old versions in the table: updates that rewrite every index, vacuum, bloat, freezing, row locks, and the horizon one forgotten transaction holds for everyone. Indexes and planner foundations takes the B-tree apart — concurrent splits, the bloat updates create, collations, key choice — and then builds the planner's core: how to read a plan, what a cost is, the statistics behind every estimate and where estimates go wrong. Specialised indexes, planner mechanics and index selection applies that to the access methods for data a B-tree cannot order — GIN, JSONB, full-text, GiST, BRIN, pgvector — then goes inside the planner's machinery and closes with how to choose, and pay for, an index.
The last four modules take the database into production. Recovery and replication replays the log after a crash, restores to a point in time, and keeps copies on standbys. Locking, schema change and partitioning explains why one `ALTER TABLE` can block a thousand reads and how to change a schema without taking the table down. Security covers who may connect — host rules, password exchanges, OAuth tokens — and over which wire, including what `sslmode` does and does not verify; and what a role may then touch, including the ownership and name-resolution rules behind most privilege escalations, the boundary where a superuser becomes the operating-system user, row-level policies that filter inside a table, and the logging and at-rest options that bound all of it. Operations ends the course with the evidence — wait events, statement statistics, memory, poolers, backups and upgrades — and a procedure for going from a symptom to its cause before touching anything.
Every topic says what it inherits and what it leaves open, names the PostgreSQL version that made each behaviour true — the course targets PostgreSQL 18 — and closes on something you can observe or decide.
Two topics are marked optional: the history of the project, which follows the opening picture tour of the server, and the two production accounts of replication at scale, which close the replication module. Nothing later depends on them, so you can pass over them and come back for the story. After the picture tour, the chain of mechanisms starts at The no-overwrite bet.
96 topics across 10 modules, in dependency order.
This course assumes System Design, and does not repeat it. MVCC, isolation levels, the write-ahead log, B-trees, amplification and replication are derived there in the abstract. Here they are only the starting point: what PostgreSQL specifically chose, and what that choice costs in production.
Every topic opens with what to read first. Each entry says what you need from it, so a topic reached from an incident rather than from the start of the course is still readable.
Where PostgreSQL differs from its peers, the lesson says so and gives a verdict — including where a peer is simply better. Transaction-ID wraparound, update amplification across secondary indexes and standby query cancellation are all places PostgreSQL loses, and pretending otherwise would make the rest untrustworthy.
Topics still being written are labelled and excluded from progress, so the count reflects what is actually finished.
Almost everything surprising about PostgreSQL follows from five decisions: old row versions live in the table rather than in an undo log; a connection is an operating-system process; the write-ahead log is physical, over an 8 kB page; planning is cost-based over sampled statistics; and types, operators and access methods are extensible. Each module is one of those decisions arriving as a bill.
Read at your own pace. Each topic is one continuous page; mark it done as you finish.
Start with 01.1 · PostgreSQL in pictures: the server, its processes, its memory and its filesReview what you have finished↷ 0 skipped topics- 01.1PostgreSQL in pictures: the server, its processes, its memory and its files
- 01.2From POSTGRES to PostgreSQL 18: a research storage design, made production-grade in layersOptional
- 01.3The no-overwrite bet, and the four consequences
- 01.4A process per connection
- 01.5Shared buffers and the clock sweep
- 01.6Asynchronous I/O, and 25 years of trusting the kernel
- 01.7What protects shared memory
- 01.8The conflict matrix is the whole story
- 01.9How the database describes itself
- 04.1Transaction ids and the commit log: where xmin comes from
- 04.2xmin, xmax and the visibility rule
- 04.3Why there is no undo log
- 04.4An UPDATE is an INSERT
- 04.5HOT, and the price of admission
- 04.6The cleanup a SELECT performs
- 04.7What vacuum does, and what it does not
- 04.8The dead-TID store, and the memory budget that decides the passes
- 04.9The one number that stops vacuum everywhere
- 04.10Subtransactions, and the cliff at 64
- 04.11The trigger formula, and where it fails
- 04.12Steady-state bloat
- 04.1332-bit XIDs and the freeze deadline
- 04.14The escalating defences, and two outages
- 04.15Row locks live in the row
- 04.16MultiXacts: when several transactions lock one row
- 04.17What REPEATABLE READ and SERIALIZABLE mean here
- 04.18Sequences: the one object that ignores the transaction
- 05.1An index is a relation with a contract
- 05.2Postgres's B-tree
- 05.3Fighting bloat: dedup, bottom-up deletion, and the upgrade traps
- 05.4Collations: when the sort order is someone else's code
- 05.5bigint, UUIDv4, UUIDv7
- 05.6What a plan is, and how to read one line of it
- 05.7What a cost is
- 05.8Scan nodes: how a plan reads a table
- 05.9Bitmap scans: collecting TIDs first, then visiting pages in order
- 05.10Why "covering" needs the visibility map
- 05.11What ANALYZE stores
- 05.12Deriving a row estimate
- 05.13Nested loop, hash, merge — and what a hash does when it does not fit
- 05.14work_mem is per node: the worst-case memory of one query
- 05.15Sorting, grouping and the nodes between
- 05.16Where it breaks, and the fix
- 05.17Reading a plan as evidence
- 05.18EXPLAIN's options, and seeing a plan
- 06.1GIN: one row, many keys
- 06.2JSONB inside a relational engine
- 06.3tsvector, tsquery and ranking
- 06.4GiST and SP-GiST: the generalisation
- 06.5'No overlaps' as a constraint, and temporal keys
- 06.6An index that stores almost nothing
- 06.7Why PostGIS and pgvector can exist
- 06.8pgvector inside PostgreSQL: what the page, MVCC, vacuum and WAL force on a graph index
- 06.9Filtered vector search: post-filtering, iterative scans and partitions
- 06.10Join ordering as a search problem
- 06.11The rewriter: views and row-level security become the query
- 06.12Subqueries, CTEs and what the planner flattens
- 06.13Custom plans, generic plans and the five-execution rule
- 06.14When the planner adds workers
- 06.15JIT compilation, and why it is often turned off
- 06.16Choosing, and paying for, an index
- 07.1Redo, and only redo
- 07.2A standby is a permanent recovery
- 07.3Why a standby cancels your query
- 07.4The retention contract
- 07.5Escaping the physical form
- 07.6Synchronous commit: what a commit waits for
- 07.7Failover and fencing: promoting a standby without two primaries
- 07.8What replication costs at scale: two production accountsOptional
- 09.1Admitting a connection: pg_hba.conf and the method it chooses
- 09.2What is stored and proved: MD5, SCRAM, channel binding and OAuth
- 09.3The wire: listen_addresses, sslmode and certificates
- 09.4Roles, ownership and the search_path trap
- 09.5The superuser boundary: reaching the operating system, logging and encryption at rest
- 09.6Row-level security: the predicate the query never wrote
01.2From POSTGRES to PostgreSQL 18: a research storage design, made production-grade in layers
Optional Background reading: no later topic depends on it, so read it now or come back to it.
01.3The no-overwrite bet, and the four consequences
01.4A process per connection
01.5Shared buffers and the clock sweep
01.6Asynchronous I/O, and 25 years of trusting the kernel
01.7What protects shared memory
01.8The conflict matrix is the whole story
01.9How the database describes itself
02.1Inside the 8 kB page
02.2What a row actually costs
02.3TOAST: the escape hatch for values that do not fit
02.4The heap is unordered, and a relation is several files
02.5The two maps beside the table
03.1Records, LSNs and segments
03.2Torn pages
03.3The recovery-time dial
03.4What each `synchronous_commit` level loses
04.1Transaction ids and the commit log: where xmin comes from
04.2xmin, xmax and the visibility rule
04.3Why there is no undo log
04.4An UPDATE is an INSERT
04.5HOT, and the price of admission
04.6The cleanup a SELECT performs
04.7What vacuum does, and what it does not
04.8The dead-TID store, and the memory budget that decides the passes
04.9The one number that stops vacuum everywhere
04.10Subtransactions, and the cliff at 64
04.11The trigger formula, and where it fails
04.12Steady-state bloat
04.1332-bit XIDs and the freeze deadline
04.14The escalating defences, and two outages
04.15Row locks live in the row
04.16MultiXacts: when several transactions lock one row
04.17What REPEATABLE READ and SERIALIZABLE mean here
04.18Sequences: the one object that ignores the transaction
05.1An index is a relation with a contract
05.2Postgres's B-tree
05.3Fighting bloat: dedup, bottom-up deletion, and the upgrade traps
05.4Collations: when the sort order is someone else's code
05.5bigint, UUIDv4, UUIDv7
05.6What a plan is, and how to read one line of it
05.7What a cost is
05.8Scan nodes: how a plan reads a table
05.9Bitmap scans: collecting TIDs first, then visiting pages in order
05.10Why "covering" needs the visibility map
05.11What ANALYZE stores
05.12Deriving a row estimate
05.13Nested loop, hash, merge — and what a hash does when it does not fit
05.14work_mem is per node: the worst-case memory of one query
05.15Sorting, grouping and the nodes between
05.16Where it breaks, and the fix
05.17Reading a plan as evidence
05.18EXPLAIN's options, and seeing a plan
06.1GIN: one row, many keys
06.2JSONB inside a relational engine
06.3tsvector, tsquery and ranking
06.4GiST and SP-GiST: the generalisation
06.5'No overlaps' as a constraint, and temporal keys
06.6An index that stores almost nothing
06.7Why PostGIS and pgvector can exist
06.8pgvector inside PostgreSQL: what the page, MVCC, vacuum and WAL force on a graph index
06.9Filtered vector search: post-filtering, iterative scans and partitions
06.10Join ordering as a search problem
06.11The rewriter: views and row-level security become the query
06.12Subqueries, CTEs and what the planner flattens
06.13Custom plans, generic plans and the five-execution rule
06.14When the planner adds workers
06.15JIT compilation, and why it is often turned off
06.16Choosing, and paying for, an index
07.1Redo, and only redo
07.2A standby is a permanent recovery
07.3Why a standby cancels your query
07.4The retention contract
07.5Escaping the physical form
07.6Synchronous commit: what a commit waits for
07.7Failover and fencing: promoting a standby without two primaries
07.8What replication costs at scale: two production accounts
Optional Background reading: no later topic depends on it, so read it now or come back to it.
08.1Why one ALTER blocks a thousand SELECTs
08.2Migrations that do not take the table down
08.3Deadlock detection, and advisory locks
08.4Job queues on PostgreSQL
08.5Declarative partitioning, and its sharp edges
09.1Admitting a connection: pg_hba.conf and the method it chooses
09.2What is stored and proved: MD5, SCRAM, channel binding and OAuth
09.3The wire: listen_addresses, sslmode and certificates
09.4Roles, ownership and the search_path trap
09.5The superuser boundary: reaching the operating system, logging and encryption at rest
09.6Row-level security: the predicate the query never wrote
10.1Where the time actually goes
10.2Finding the query that matters
10.3What a pooler changes, and what it breaks
10.4Adding it up before it kills the machine
10.5What you can actually promise
10.6Major-version upgrades
10.7A decision procedure
Skipped topics
Moved past without finishing. A skipped topic still closes its module, and Continue goes on without it — open one to read it or mark it understood.
Glossary
The vocabulary this course uses, with the mechanism behind each term and a link to the topic that derives it.
Backend
The operating-system process that serves one client connection for its whole lifetime.
More detail
PostgreSQL forks a process per connection rather than using a thread pool. Everything shared between sessions therefore lives in one shared-memory segment, a connection is expensive enough that pooling is mandatory rather than optional, and a backend that crashes takes down only its own session.
Postmaster
The supervisor process that listens for connections, forks backends, and starts every background process.
More detail
It owns the shared-memory segment and restarts the cluster if a backend dies abnormally, because a crashed backend may have left shared memory inconsistent. Background processes it starts include the checkpointer, background writer, WAL writer, autovacuum launcher and workers, and the archiver.
Shared buffers
PostgreSQL's own cache of 8 kB pages in shared memory, sitting above the operating system page cache.
More detail
Eviction uses a clock sweep over per-buffer usage counts rather than strict LRU. Large sequential scans are given a small ring buffer so they cannot evict the whole pool. Unlike InnoDB, PostgreSQL does not bypass the filesystem cache, so a page can be held twice.
Buffer pin
A statement of intent that a buffer must not be evicted or rearranged while someone is reading it.
More detail
A pin is not a lock: several backends can pin the same buffer concurrently. Vacuum needs a cleanup lock — exclusive plus a pin count of one — before it can defragment a page, which is why an open cursor can block vacuum on that page and shows as a BufferPin wait.
LWLock
A lightweight shared/exclusive lock protecting an in-memory structure for a short, bounded time.
More detail
It sits between spinlocks (a few instructions) and the heavyweight lock manager (held across statements). LWLock waits in pg_stat_activity mean contention on a shared-memory structure, which is a different problem with a different fix from a Lock wait.
System catalog
The ordinary tables in which PostgreSQL stores its own schema, statistics and configuration.
More detail
Because the catalog is tables, DDL is transactional and rolls back cleanly — the most valuable operational property PostgreSQL has over Oracle and MySQL. The cost is that catalogs bloat like any other table, so churn that creates and drops objects has a price.
Relcache
Each backend's private cache of catalog rows describing relations it has touched.
More detail
Reading the catalog on every query would be intolerable, so every process caches it. A cache in every process needs invalidation, broadcast through shared memory, which is why heavy DDL under load costs more than the DDL itself.
Generic plan
A cached plan for a prepared statement that ignores the specific parameter values.
More detail
PostgreSQL plans the first five executions with the actual values, then compares the average custom plan cost with the generic plan cost and keeps the generic one if it is not worse. plan_cache_mode overrides the heuristic. The cache is per backend, so a pooler in transaction mode can defeat it.
Page
The 8 kB unit of I/O, of WAL full-page writes and of visibility bookkeeping.
More detail
A page carries a 24-byte header, then line pointers growing down from it while tuples grow up from the end. The gap between them is the free space. Everything the course measures — scan cost, bloat, WAL volume — is counted in pages.
Line pointer
A 4-byte (offset, length) slot at the start of a page that indirects to a tuple.
More detail
The indirection is what lets a tuple move within its page without any index needing to know. A line pointer can be normal, a redirect (used by HOT chains), dead, or unused; that state is what vacuum and page pruning manipulate.
Heap tuple
One physical row version: a 23-byte header, an optional null bitmap, then the column values.
More detail
The header is not generic overhead — it is MVCC written to disk. xmin and xmax name the transactions that created and deleted the version, t_ctid points at the next version, and the infomask bits cache commit status and mark HOT chains.
TOAST
The mechanism that compresses, and then moves out of line, values too large to fit in a tuple.
More detail
A tuple may not span pages, so a value over roughly 2 kB is compressed first and, if still too large, chunked at about 2 000 bytes into a side table with its own index. Reading one large value is then an index scan, which SELECT * pays for even when the column is unused.
Fork
One of the separate files that make up a relation's physical storage.
More detail
The main fork holds the data; _fsm is the free-space map, _vm the visibility map, _init the template for unlogged relations. They are separate files because they are written and read on different schedules.
Free-space map
A fork recording approximately how much room each page has, so an insert can find one.
More detail
It is deliberately approximate: exactness would cost more to maintain than it saves. It is updated by vacuum, which is one reason a table that is never vacuumed keeps extending rather than reusing space it already has.
Visibility map
A fork with two bits per page: all-visible, and all-frozen.
More detail
The all-visible bit is what makes an index-only scan possible, since the index itself carries no visibility information. The all-frozen bit is what lets an aggressive vacuum skip pages it has already frozen. Both are maintained by vacuum, so both decay after a write burst.
xmin / xmax
The transaction IDs that created and deleted a row version.
More detail
Visibility is a predicate over these two, the snapshot, and the commit log. Because they live in the row rather than in an undo segment, PostgreSQL can decide visibility locally with no reconstruction — and must clean up the dead versions afterwards.
Snapshot
The record of which transactions a statement may see: an xmin, an xmax, and the list in progress between them.
More detail
Taken by scanning the ProcArray, so its cost grows with the number of backends. The oldest live snapshot in the cluster is what bounds every vacuum, which is why one idle transaction can bloat tables it never touched.
Hint bit
A cached commit-status flag written into a tuple header by the first reader after the transaction ends.
More detail
Without it, every visibility check would consult the commit log. With it, a read-only SELECT can dirty pages and generate write I/O — which is the usual explanation for a first scan being far slower than the second with no caching story to match.
Commit log
Two bits per transaction, in a paged shared-memory structure, recording whether it committed.
More detail
Cheap while the working set of transaction IDs fits in its buffers. Subtransactions have their own equivalent, and each backend caches only 64 subtransaction IDs; past that the snapshot is marked overflowed and every visibility check falls back to that structure, collapsing throughput.
Subtransaction
A nested transaction created by SAVEPOINT, a PL/pgSQL EXCEPTION block, or an ORM emulating nesting.
More detail
Each backend advertises at most 64 of them. Beyond that limit the snapshot overflows and visibility checks become far more expensive cluster-wide. An exception handler inside a hot loop is therefore an architectural decision, not a style choice.
HOT update
A heap-only update whose new version stays on the same page, so no index needs updating.
More detail
It requires two things: no indexed column changed, and room on the page — which is why fillfactor must be spent up front. One index on a frequently-updated column disables it for that table. n_tup_hot_upd against n_tup_upd is the health metric, and it has no analogue in InnoDB because the problem does not arise there.
Vacuum
The process that removes row versions no snapshot can still see, and freezes old ones.
More detail
It returns space to the table, not to the filesystem, which is why disk usage does not drop. It is throttled by a cost budget shared among autovacuum workers — the setting most often left at a default that cannot keep up on a large table.
Bloat
Space occupied by row versions that are dead but not yet reclaimed.
More detail
Not a bug but an equilibrium, set by the update rate and the interval between vacuums. InnoDB bloats too; the difference is that PostgreSQL makes it visible and gives you tools, while engines with undo hide it until a rebuild.
Freezing
Marking a row version as older than every possible snapshot, so its transaction ID no longer matters.
More detail
Transaction IDs are 32-bit and compared modulo 2^32, so "older than" only means anything within a 2^31 window. Rows must be frozen before that window passes or they would appear to be in the future. This is why vacuum is mandatory rather than tidy, and it is pure inherited cost: InnoDB and Oracle use 64-bit identifiers and have no equivalent.
xmin horizon
The oldest transaction ID any live snapshot in the cluster still needs.
More detail
It bounds cleanup for every table. Four unrelated things hold it back: an idle-in-transaction session, a long analytics query, an inactive replication slot, and hot_standby_feedback from a standby. Read it from pg_stat_activity.backend_xmin and pg_replication_slots.xmin.
SIREAD lock
A non-blocking predicate lock recording that a serializable transaction read something.
More detail
SSI uses these to detect the two consecutive read-write dependencies that make a snapshot-isolation history unserializable, and aborts one transaction rather than blocking. They escalate from tuple to page to relation under memory pressure, which is where false-positive aborts come from.
Access method
The contract an index type implements, defined as catalog rows rather than engine code.
More detail
An operator class supplies the operators and support functions a method needs for a given type. This is why a user-defined type can be indexed without changing PostgreSQL, and why PostGIS and pgvector can exist as extensions rather than forks.
Operator class
The set of operators and support functions that let one access method index one data type.
More detail
It is also why LIKE 'abc%' needs text_pattern_ops under a non-C collation: the default class orders text by collation rules, which do not match prefix comparison.
High key
The upper bound stored on a B-tree page, used with the right link to survive a concurrent split.
More detail
Lehman–Yao: a reader that lands on a page which has just split compares against the high key and walks right, rather than restarting from the root. This is what lets PostgreSQL avoid locking the tree on descent.
Bottom-up index deletion
Clearing dead index entries when a page is about to split, rather than waiting for vacuum.
More detail
Added in PostgreSQL 14. It is what stops an index on a frequently-updated column from growing without bound between vacuums. PostgreSQL still does not merge underfull index pages, so an index that shrinks stays large until REINDEX.
Index-only scan
Answering a query from the index alone, without visiting the heap.
More detail
Possible only where the visibility map marks the page all-visible, because index entries carry no visibility information. It therefore degrades silently into heap fetches after a write burst — visible as Heap Fetches in EXPLAIN (ANALYZE). InnoDB needs no equivalent condition.
GIN
An inverted index mapping each key to the sorted list of rows containing it.
More detail
The index type behind JSONB, full-text search and array containment. Because one row update touches many keys, GIN buffers new entries in a pending list; that makes writes cheap and makes the search that triggers the flush expensive.
GiST
A generalized search tree defined by user-supplied functions rather than by a fixed comparison.
More detail
Supplying consistent, union, penalty and picksplit defines a tree for any predicate, including ones with no total order: overlaps, contains, nearest-neighbour. Exclusion constraints fall out of it, and it is the reason PostGIS exists.
BRIN
A summary of each block range — typically min and max — rather than an entry per row.
More detail
Orders of magnitude smaller than a B-tree, and useful only where the column correlates with physical order. It is a filter, not a lookup structure: an update that breaks correlation degrades it silently. pg_stats.correlation is the number that predicts whether it will work.
Cost unit
The abstract unit in which the planner compares unlike plans, anchored at seq_page_cost = 1.
More detail
A random page costs 4 and a tuple 0.01 by default — ratios chosen for a spinning disk, which is why random_page_cost is usually wrong on SSD. effective_cache_size is a hint about the operating system cache, not an allocation.
Selectivity
The estimated fraction of rows a predicate will keep.
More detail
Derived mechanically from the statistics: equality against the most-common-values list or 1/n_distinct, ranges by histogram interpolation, AND by multiplication. The multiplication assumes the predicates are independent, which is where most estimation error starts.
Extended statistics
Explicitly-created statistics describing how several columns vary together.
More detail
CREATE STATISTICS supplies the joint distribution the independence assumption is missing, in three kinds: ndistinct, dependencies and mcv. They are not collected automatically and are not used for joins, so they repair a specific shape of error rather than the general one.
GEQO
The genetic search the planner falls back to when there are too many relations to enumerate.
More detail
Exhaustive dynamic programming over join orders is fine to about ten relations. Past geqo_threshold PostgreSQL searches genetically and no longer guarantees the best plan — and, unlike Oracle or SQL Server, offers no hints or plan baselines to override the result.
work_mem
The memory budget for one sort or hash — per node, per backend, not per query.
More detail
A plan with several such nodes multiplies it, and parallel workers multiply it again, so worst-case memory is a number nobody configured. A hash that does not fit spills to batches. Oracle and SQL Server manage a bounded server-wide budget instead; this is a clear PostgreSQL weakness.
LSN
A log sequence number: a byte offset into the WAL stream.
More detail
Every page stores the LSN of the change that last modified it, which encodes the write-ahead rule: a page may not reach disk before the WAL describing it. LSNs also drive every replication progress measure — sent, write, flush, replay.
Full-page write
A whole 8 kB page image written into the WAL the first time that page changes after a checkpoint.
More detail
Storage guarantees atomicity at 512 bytes or 4 kB, so a power cut can leave a torn page that redo cannot repair. WAL volume therefore spikes right after every checkpoint. InnoDB solves the same problem with the doublewrite buffer, paying a constant 2× instead.
Checkpoint
A point at which dirty buffers are flushed and a starting LSN for recovery is recorded.
More detail
Recovery is bounded by the WAL written since the last one, so the interval is the recovery-time dial. It trades directly against full-page writes: shortening it shortens recovery and increases WAL volume. A requested checkpoint — one triggered by hitting max_wal_size — is a warning sign; a timed one is not.
synchronous_commit
How far a commit must travel before it is acknowledged: off, local, remote_write, on, or remote_apply.
More detail
Setting it off risks losing recent commits, not corrupting data — the distinction most often got wrong. It can be set per transaction, so one session can opt out for bulk work while the rest of the system stays durable.
Timeline
A branch identifier in WAL history, created whenever a server is promoted or recovered to a point in the past.
More detail
Without it, a promoted standby's WAL could be confused with the original primary's from the same LSN onward. It is why point-in-time recovery can be repeated without overwriting the history it came from.
Physical replication
Streaming the byte-exact WAL to a standby that runs recovery forever.
More detail
Nothing needs interpreting, so it is cheap and cannot drift — but both ends must share major version, architecture and block size, the whole cluster is copied, and nothing can be filtered. MySQL replicates a logical binlog instead, which is more flexible and less faithful.
Replication slot
A record on the primary that WAL a given consumer has not yet received must be retained.
More detail
It protects the replica and endangers the primary: a slot for a standby that never returns fills the disk, and a logical slot also holds back the xmin horizon. Watch wal_status in pg_replication_slots, and set max_slot_wal_keep_size.
Hot standby conflict
A standby cancelling a query because applying WAL would remove rows that query can still see.
More detail
The standby cannot both apply and preserve, so it waits max_standby_streaming_delay and then cancels. hot_standby_feedback prevents it by holding the primary’s xmin horizon back — moving the cost to bloat on the primary rather than removing it.
Logical decoding
Reconstructing row-level changes from the physical WAL through an output plugin.
More detail
It is what makes per-table, cross-version replication and CDC possible. The decoder must be able to identify a row, hence REPLICA IDENTITY, and DDL is not replicated. It is also the only supported path to a major-version upgrade with near-zero downtime.
ACCESS EXCLUSIVE
The strongest table lock: it conflicts with every other mode, including plain reads.
More detail
Taken by most rewriting DDL. Because lock requests queue in arrival order, a request for it that is itself instantaneous will stall every later query on the table while it waits behind one long transaction. The fix is lock_timeout plus retry, not a faster statement.
Lock queue
The arrival-ordered wait list for a relation lock.
More detail
A waiting strong request blocks every request behind it, even ones that would not conflict with the current holder. pg_blocking_pids() names who is at the front. MySQL metadata locks pile up the same way, so this is not a PostgreSQL-specific flaw.
Multixact
The structure used when several transactions hold row-level locks on the same row.
More detail
Row locks normally live in the tuple header rather than in the lock table, which is why PostgreSQL never escalates row locks to table locks the way SQL Server does. Multixacts are the price: they have their own shared-memory structure and their own wraparound to manage.
CREATE INDEX CONCURRENTLY
Building an index without holding a lock that blocks writes, at the cost of two table scans.
More detail
It waits for existing transactions between passes and cannot run inside a transaction block — both direct consequences of the visibility rules. If it fails it leaves an invalid index behind that must be dropped and rebuilt.
Partition pruning
Excluding partitions that cannot match, either when the plan is built or while it runs.
More detail
Plan-time pruning needs constants; run-time pruning handles parameters and shows as never executed subplans. Each partition is a real relation with its own indexes, statistics and autovacuum state, which explains most surprises — including that a unique index must contain the partition key.
Wait event
The named thing a backend is waiting on, reported by pg_stat_activity.
More detail
It partitions "the database is slow" into Lock, LWLock, BufferPin, IO, Client and IPC waits, each implicating a different mechanism. PostgreSQL keeps no history of them, so sampling — or pg_wait_sampling — is required; Oracle ships that history as ASH and AWR.
pg_stat_statements
Cumulative execution statistics per normalized statement fingerprint.
More detail
Rank by total time rather than mean: the query that matters is usually fast and frequent, not slow and rare. Its per-call I/O and WAL columns turn it into a write-cost ranking as well.
Connection pooler
A proxy multiplexing many client connections onto few backends.
More detail
Transaction mode is the only mode that saves real resources, and it breaks everything that assumes a session: prepared statements, session-scoped advisory locks, SET, LISTEN/NOTIFY, temporary tables. Needing an external component here is the direct price of the process-per-connection design.
Incremental backup
A base backup containing only blocks changed since a previous one, combined at restore time.
More detail
Added in PostgreSQL 17 via pg_basebackup --incremental, summarize_wal and pg_combinebackup. Oracle RMAN has had block-level incrementals for decades; in practice most PostgreSQL teams use pgBackRest or Barman rather than core tooling.
Background process
The postmaster also starts background processes: server processes that serve no client, each doing one job for the whole server.
Go to PostgreSQL in pictures: the server, its processes, its memory and its files →data directory
Everything the server stores lives in one directory, the data directory, often called PGDATA after the setting that names its location.
Go to PostgreSQL in pictures: the server, its processes, its memory and its files →filenode
16397 is the table's filenode, the number that names its file.
Go to PostgreSQL in pictures: the server, its processes, its memory and its files →Time travel
Time travel was the ability to run a query "as of" an earlier wall-clock time and see the versions that were committed then. (Wall-clock time is the time a clock on the wall shows, as opposed to a transaction counter.) It worked because no version was ever destroyed, only moved.
Go to From POSTGRES to PostgreSQL 18: a research storage design, made production-grade in layers →PostQUEL
PostQUEL was the query language of the Berkeley POSTGRES system, used in place of SQL until Postgres95 replaced it.
Go to From POSTGRES to PostgreSQL 18: a research storage design, made production-grade in layers →Undo log
Keep the old version in an undo log.
Go to The no-overwrite bet, and the four consequences →no-overwrite
A no-overwrite storage manager never updates a record in place.
Go to The no-overwrite bet, and the four consequences →ProcArray
The ProcArray is the shared-memory array with one slot per backend.
Go to A process per connection →double buffering
A hot page can exist twice in RAM, which is called double buffering.
Go to Shared buffers and the clock sweep →usage count
A buffer descriptor per buffer: a small header in shared memory recording which page the frame holds, its usage count and its pin count.
Go to Shared buffers and the clock sweep →clock sweep
The clock sweep is PostgreSQL's buffer replacement policy.
Go to Shared buffers and the clock sweep →ring buffer
It gives such scans a small ring buffer: a handful of frames, allocated normally, that the scan then reuses for its whole duration.
Go to Shared buffers and the clock sweep →Read stream
PostgreSQL 17 introduced read streams, called "streaming I/O" in the 17 release notes.
Go to Asynchronous I/O, and 25 years of trusting the kernel →Asynchronous I/O
Asynchronous I/O means a process submits a read and carries on working, collecting the result later.
Go to Asynchronous I/O, and 25 years of trusting the kernel →I/O worker
How those reads are executed is chosen at server start by io_method: io_methodHow reads are executedWhere it fits worker (default)The backend queues read requests to a pool of I/O workers — background processes that perform reads on backends' behalf (io_workers, default 3) — which perform them and signal completionEvery platform; consistent with the process-per-connection model a process per connection io_uringThe…
Go to Asynchronous I/O, and 25 years of trusting the kernel →spinlock
A spinlock is a lock whose waiter loops, retrying an atomic instruction, rather than going to sleep atomic instructions.
Go to What protects shared memory →cleanup lock
A cleanup lock is an exclusive LWLock on the buffer's content, and a pin count of exactly one — the vacuum's own.
Go to What protects shared memory →heavyweight lock
PostgreSQL's version of it is the heavyweight lock: a lock on a table or other database object.
Go to The conflict matrix is the whole story →lock mode
A lock mode is a named class of access to a relation that a transaction declares when it locks it.
Go to The conflict matrix is the whole story →conflict matrix
Two lock requests on the same relation from different transactions conflict, and the later one waits, only if their modes are marked as conflicting in a fixed table of mode pairs, the conflict matrix.
Go to The conflict matrix is the whole story →Fast-path lock
They go through a per-backend fast path: a small array in the backend's own slot, where it records weak relation locks under a per-backend lock.
Go to The conflict matrix is the whole story →lock escalation
That is lock escalation, and concurrency collapses at a threshold nobody wrote down.
Go to The conflict matrix is the whole story →OID
Every object is named in those rows by its OID (object identifier): the 32-bit number PostgreSQL assigns to each table, index, type, function and other catalog object when it is created.
Go to How the database describes itself →relation
CatalogHolds pg_classOne row per relation — a relation being any stored object with a name and columns: a table, index, view, sequence (a counter object that hands out unique numbers), the side table that holds oversized values TOAST — with its size estimates and relfrozenxid, the oldest transaction id that may still appear in the table, tracked so that the ids can be recycled safely freezing pg_attributeOne row p…
Go to How the database describes itself →heap
Catalogs are ordinary heap tables.
Go to How the database describes itself →Transactional DDL
That property is called transactional DDL:
Go to How the database describes itself →syscache
The syscache — individual catalog rows, keyed the way the code looks them up (a type by OID, a function by name and argument types).
Go to How the database describes itself →invalidation message
An invalidation message (the source calls the mechanism sinval, shared invalidation) is a notice naming a catalog row or relation whose cached copy is now stale.
Go to How the database describes itself →catalog bloat
Dead versions piling up in the system catalogs faster than vacuum removes them is catalog bloat.
Go to How the database describes itself →TID (tuple identifier)
A TID is the physical address of a row version in PostgreSQL: where it is stored, not the value of any column.
Go to Inside the 8 kB page →Dead line pointer
A dead line pointer is one in state LP_DEAD: its tuple has been removed, but the slot stays reserved.
Go to Inside the 8 kB page →MAXALIGN
A tuple is padded to the platform's maximum alignment — bytes on the common 64-bit platforms — so that the next tuple, and any 8-byte value inside it, starts on a multiple of 8 (the rule explained in byte order and alignment); its real cost is for a tuple of logical size .
Go to Inside the 8 kB page →heap tuple header
The heap tuple header (HeapTupleHeaderData) is the fixed record at the start of every row version in a PostgreSQL table.
Go to What a row actually costs →null bitmap
If any column of a row is null, a null bitmap follows the header, with one bit per column; a 1 means not null.
Go to What a row actually costs →alignment padding
So PostgreSQL inserts alignment padding — unused bytes placed before a column so that it starts at a multiple of its type's alignment — wherever the next column's alignment demands it.
Go to What a row actually costs →Missing value (fast default)
The default is evaluated once and stored in the catalog, in pg_attribute.attmissingval, as the column's missing value.
Go to What a row actually costs →TOAST chunk
The value is split into chunks of about 2 000 bytes, sized so four chunk rows fit on a page.
Go to TOAST: the escape hatch for values that do not fit →TOAST pointer
The TOAST pointer left behind in the main row is 18 bytes, regardless of the value's size.
Go to TOAST: the escape hatch for values that do not fit →Storage strategy
Each column has a storage strategy, which says which of the two steps — compress, move out of line — its values may take.
Go to TOAST: the escape hatch for values that do not fit →Index-organised table
Its opposite in this topic is an index-organised table, whose rows are stored inside the primary-key index, in key order.
Go to The heap is unordered, and a relation is several files →All-visible
The all-visible bit says every row version on the page is visible to every current and future snapshot. (A snapshot is a transaction's view of which row versions exist, as the visibility rule derives.) The all-frozen bit says, further, that every row version on it is frozen.
Go to The two maps beside the table →All-frozen
The all-visible bit says every row version on the page is visible to every current and future snapshot. (A snapshot is a transaction's view of which row versions exist, as the visibility rule derives.) The all-frozen bit says, further, that every row version on it is frozen.
Go to The two maps beside the table →Log sequence number
A log sequence number (LSN) is the byte offset of a WAL record in one conceptually infinite stream of log bytes.
Go to Records, LSNs and segments →WAL segment
The stream is stored as a sequence of WAL segments: files of 16 MB each by default, in the pg_wal directory.
Go to Records, LSNs and segments →write-ahead rule
The write-ahead rule: a dirty page may not be written to disk until WAL has been flushed up to that page's pd_lsn.
Go to Records, LSNs and segments →resource manager
After the header come a resource manager id and the data.
Go to Records, LSNs and segments →WAL recycling
What bounds it is recycling.
Go to Records, LSNs and segments →torn page
A torn page is a page whose on-disk bytes come partly from one write and partly from an earlier one, because the device completed only some of the sectors of an 8 kB write before power was lost.
Go to Torn pages →full-page image
A full-page image (FPI) is a copy of a whole page, minus its free-space hole, written into the WAL with the first change to that page after a checkpoint, so that replay can restore the page without trusting the copy on disk.
Go to Torn pages →doublewrite buffer
How other engines answer this PostgreSQL — full-page writesInnoDB (MySQL's default storage engine) — doublewrite buffer MechanismA whole page image in the log, once per page per checkpoint cycleEvery page written twice: first to a dedicated contiguous area, the doublewrite buffer, then to its home location; after a crash, a torn home copy is repaired from the intact doublewrite copy Cost patternSpiky — concentrate…
Go to Torn pages →redo point
The redo point is the LSN from which crash recovery will replay.
Go to The recovery-time dial →checkpoint_completion_target
On a large shared_buffers, that is gigabytes of writes in seconds, and every query competes with them. checkpoint_completion_target spreads the work across a fraction of the expected interval instead.
Go to The recovery-time dial →Requested checkpoint
What triggers one TriggerSettingDefaultMeaning Timecheckpoint_timeout5 minThe healthy case WAL volumemax_wal_size1 GBA requested checkpoint, fired by WAL volume rather than the clock — a warning sign Manual, shutdown, base backup——Explicit: the CHECKPOINT command, a clean shutdown, or the start of a base backup (a full physical copy of the data directory, used to build a standby or a restore) Why it looks this way…
Go to The recovery-time dial →Restartpoint
A standby counts restartpoints.
Go to The recovery-time dial →asynchronous commit
The first row has its own name: asynchronous commit.
Go to What each `synchronous_commit` level loses →Group commit
Group commit makes several transactions' commit records durable with one flush: whichever committer flushes the WAL flushes everything written before it, including the commit records of others waiting behind it, and all of them are released together.
Go to What each `synchronous_commit` level loses →Transaction id (XID)
A transaction id, or XID, is a 32-bit number taken from a cluster-wide counter and assigned to a transaction when it first changes the database.
Go to Transaction ids and the commit log: where xmin comes from →Virtual transaction id
So PostgreSQL gives every transaction a virtual transaction id when it starts.
Go to Transaction ids and the commit log: where xmin comes from →SLRU
PostgreSQL calls this kind of cache an SLRU, a simple least-recently-used buffer pool: a fixed set of page slots for one on-disk file set, found by a linear search and replaced least-recently-used first.
Go to Transaction ids and the commit log: where xmin comes from →Purge
In-place plus undo (InnoDB, Oracle)Append version (PostgreSQL) The row's home location holds, marked deleted by this transaction is atAn undo record, reachable by a roll pointerIts home location, still; it is that goes wherever there was free space — a new tuple with a new TID An old snapshot reads byReading the row, then walking undo backwa…
Go to Why there is no undo log →Heap-only tuple (HOT)
A heap-only tuple is a row version that no index entry points at directly.
Go to HOT, and the price of admission →HOT chain
That sequence of versions, linked through t_ctid and reached from one line pointer, is a HOT chain.
Go to HOT, and the price of admission →Fillfactor
Fillfactor, a per-table storage parameter from 10 to 100, is the percentage of each page that inserts may fill.
Go to HOT, and the price of admission →Pruning
Pruning is the page-local cleanup any backend performs on a page it is reading.
Go to The cleanup a SELECT performs →Dead tuple
A dead tuple is a row version whose deletion (or replacement by an update) has committed, and which no snapshot can still see.
Go to What vacuum does, and what it does not →Idle in transaction
CauseWhy it holds the horizonWhere to look An idle-in-transaction sessionA session is idle in transaction when it has begun a transaction and is sending no statement.
Go to The one number that stops vacuum everywhere →Prepared transaction
The consumer may be disconnected and the slot still holdspg_replication_slots.xmin, catalog_xmin hot_standby_feedbackA standby reports its oldest snapshot to the primary so its queries are not cancelled — deliberately holding the primary's horizon standby conflictspg_stat_replication.backend_xmin A prepared transactionA prepared transaction is one on which PREPARE TRANSACTION has been run, the first phase of two-p…
Go to The one number that stops vacuum everywhere →Suboverflowed snapshot
Any snapshot taken while that backend's transaction runs is marked suboverflowed.
Go to Subtransactions, and the cliff at 64 →Autovacuum
Autovacuum is two kinds of server process, started by the postmaster like every other process model:
Go to The trigger formula, and where it fails →Cost-based vacuum delay
This cost-based vacuum delay works like a meter:
Go to The trigger formula, and where it fails →Transaction ID wraparound
Transaction ID wraparound is the failure in which a tuple's xmin falls more than transactions behind the current id, so the modular comparison reads it as a future transaction and the committed row disappears from every query.
Go to 32-bit XIDs and the freeze deadline →relfrozenxid
Each table records in pg_class the oldest unfrozen xmin any of its rows may still carry, its relfrozenxid; each database records the oldest of its tables' values as its datfrozenxid. age(relfrozenxid) — the function age() counts how many transactions have started since the id it is given — is how far a table is from the deadline.
Go to 32-bit XIDs and the freeze deadline →datfrozenxid
Each table records in pg_class the oldest unfrozen xmin any of its rows may still carry, its relfrozenxid; each database records the oldest of its tables' values as its datfrozenxid. age(relfrozenxid) — the function age() counts how many transactions have started since the id it is given — is how far a table is from the deadline.
Go to 32-bit XIDs and the freeze deadline →aggressive vacuum
But it skips all-visible pages (pages whose tuples every snapshot can see, marked in the visibility map the two maps), so old tuples on them stay unfrozen 150 millionvacuum_freeze_table_ageThe next vacuum of the table becomes an aggressive vacuum.
Go to The escalating defences, and two outages →anti-wraparound autovacuum
It visits every page not marked all-frozen, including the all-visible ones ordinary vacuums skip, so that relfrozenxid can advance 200 millionautovacuum_freeze_max_ageAn anti-wraparound autovacuum — an aggressive autovacuum started because of the table's age, not its dead tuples — is forced, even if autovacuum is disabled.
Go to The escalating defences, and two outages →failsafe
Unlike an ordinary autovacuum, it is not cancelled by conflicting lock requests autovacuum ~1.6 billionvacuum_failsafe_ageThe failsafe: vacuum abandons its cost delays (the deliberate throttle from autovacuum) and skips index cleanup entirely, doing only what is needed to freeze ~2.1 billion—Warnings begin 40 million ids before wraparound (100 million from PostgreSQL 19, in beta).
Go to The escalating defences, and two outages →row lock
A row lock in PostgreSQL is the locking transaction's id stored in the tuple's xmax, with infomask bits marking it lock-only and giving its strength.
Go to Row locks live in the row →member space
The positions in this file are numbered by a counter of their own, and that numbering is the member space.
Go to MultiXacts: when several transactions lock one row →EvalPlanQual
The executor's name for this re-check is EvalPlanQual: "evaluate the plan's qualifications" — the WHERE conditions — again, against the newest version of the row.
Go to What REPEATABLE READ and SERIALIZABLE mean here →first-updater-wins
This is first-updater-wins: of two concurrent transactions that update the same row, the one that got there first proceeds, and the other fails when the first commits.
Go to What REPEATABLE READ and SERIALIZABLE mean here →Serializable Snapshot Isolation
PostgreSQL's SERIALIZABLE adds Serializable Snapshot Isolation — SSI — on top.
Go to What REPEATABLE READ and SERIALIZABLE mean here →rw-antidependency
Fekete, Liarokapis, O'Neil, O'Neil and Shasha proved that every cycle possible under snapshot isolation contains two consecutive rw-antidependencies (a transaction reading a version a concurrent transaction then replaced).
Go to What REPEATABLE READ and SERIALIZABLE mean here →sequence
A sequence is a one-row relation holding a counter. nextval('orders_id_seq') advances it and returns the new value.
Go to Sequences: the one object that ignores the transaction →Non-transactional sequence
nextval() and setval() (which sets the counter to a chosen value) are non-transactional: their effect on the sequence is visible to every other session immediately, and is never undone when the calling transaction aborts.
Go to Sequences: the one object that ignores the transaction →strategy number
A strategy number tells the method which of its standard roles an operator plays.
Go to An index is a relation with a contract →support function
A support function is a function the method calls internally, rather than one a query names.
Go to An index is a relation with a contract →operator family
An operator family groups related operator classes so that operators comparing across types — integer against bigint — can use one index.
Go to An index is a relation with a contract →collation
The default opclass for text orders values by a collation: the set of rules that decides how strings sort and compare in a given language.
Go to An index is a relation with a contract →expression index
An expression index stores the result of a function of the row, rather than a column's value.
Go to An index is a relation with a contract →partial index
A partial index holds entries only for rows that satisfy its WHERE predicate.
Go to An index is a relation with a contract →right link
In a B-link tree every page carries a right link, a pointer to its right sibling on the same level, and a high key, an upper bound on every key the page may hold (the rightmost page of a level has none).
Go to Postgres's B-tree →incomplete split
Replay then leaves the left page flagged with an incomplete split: the right page exists and is linked, but the parent has no entry for it yet.
Go to Postgres's B-tree →suffix truncation
With suffix truncation, a split shortens the separator keys it copies into the parent.
Go to Fighting bloat: dedup, bottom-up deletion, and the upgrade traps →deduplication
With deduplication, a run of equal keys on a leaf merges into one posting-list tuple: a single key followed by a sorted array of heap TIDs.
Go to Fighting bloat: dedup, bottom-up deletion, and the upgrade traps →posting-list tuple
With deduplication, a run of equal keys on a leaf merges into one posting-list tuple: a single key followed by a sorted array of heap TIDs.
Go to Fighting bloat: dedup, bottom-up deletion, and the upgrade traps →simple deletion
This simple deletion was the only kind of B-tree deletion outside vacuum before PostgreSQL 14.
Go to Fighting bloat: dedup, bottom-up deletion, and the upgrade traps →collation provider
A collation provider is the library that implements a collation's comparison rules.
Go to Collations: when the sort order is someone else's code →collation version
A collation version is a string the provider reports for a collation's rules: the glibc release, or ICU's own version for that locale.
Go to Collations: when the sort order is someone else's code →nondeterministic collation
A nondeterministic collation is one that can report two strings equal although their bytes differ.
Go to Collations: when the sort order is someone else's code →rightmost-page fast path
nbtree has a shortcut for exactly this: the rightmost-page fast path.
Go to bigint, UUIDv4, UUIDv7 →UUIDv7
A UUIDv7, which RFC 9562 added, starts with a 48-bit Unix timestamp (milliseconds since 1 January 1970) followed by random bits.
Go to bigint, UUIDv4, UUIDv7 →planner
The planner is the fourth stage; PostgreSQL's documentation also calls it the optimizer.
Go to What a plan is, and how to read one line of it →Query plan
A query plan is a tree of plan nodes, each an operator that produces rows — reading a table, joining two inputs, sorting, grouping, cutting the stream off — together with the planner's estimates for every node.
Go to What a plan is, and how to read one line of it →Plan node
A query plan is a tree of plan nodes, each an operator that produces rows — reading a table, joining two inputs, sorting, grouping, cutting the stream off — together with the planner's estimates for every node.
Go to What a plan is, and how to read one line of it →startup cost
A node's startup cost is the planner's estimate of the work done before it can emit its first row.
Go to What a plan is, and how to read one line of it →total cost
Its total cost is the estimate for emitting all of them.
Go to What a plan is, and how to read one line of it →loops (EXPLAIN ANALYZE)
One field changes how everything else is read: loops, the number of times a node was started during the query.
Go to What a plan is, and how to read one line of it →Cost (planner)
A plan's cost is a dimensionless estimate of the resources it will use — a weighted sum of the pages it reads and the rows, index entries and operators it processes, with the weights set by the cost constants.
Go to What a cost is →skip scan
PostgreSQL 18 adds skip scan.
Go to Scan nodes: how a plan reads a table →ctid
Every heap row has a physical address, its ctid: the pair (page number, line pointer) that locates it in the table page layout.
Go to Scan nodes: how a plan reads a table →TID bitmap
Instead of fetching rows, it records each match's heap location — the page number and line pointer that form its ctid scan nodes — in an in-memory TID bitmap.
Go to Bitmap scans: collecting TIDs first, then visiting pages in order →Lossy bitmap page
It goes lossy.
Go to Bitmap scans: collecting TIDs first, then visiting pages in order →covering index
A covering index is one that contains every column a query reads, in the DBMS course's terms indexes.
Go to Why "covering" needs the visibility map →heap fetch
Each such visit is a heap fetch: a read of the heap page to check the tuple's visibility against the query's snapshot.
Go to Why "covering" needs the visibility map →INCLUDE column
INCLUDE columns (PostgreSQL 11+) are non-key columns stored in leaf entries that take no part in ordering.
Go to Why "covering" needs the visibility map →Most-common-values list
The most-common-values (MCV) list is the list of the column's most frequent values in the sample, stored with the fraction of rows each accounts for.
Go to What ANALYZE stores →Histogram (planner statistics)
The histogram describes every value not in the MCV list. histogram_bounds is a sorted list of values, chosen so that the same number of sampled rows falls between each pair of neighbours.
Go to What ANALYZE stores →default_statistics_target
It is set by default_statistics_target (default 100), the setting that fixes two things: how many entries the MCV list and the histogram may have, and how many rows ANALYZE samples.
Go to What ANALYZE stores →Autoanalyze
Autoanalyze is the ANALYZE that autovacuum runs by itself when enough of a table has changed.
Go to What ANALYZE stores →containment assumption
PostgreSQL's estimate is the one the DBMS course derives under the containment assumption cardinality estimation:
Go to Deriving a row estimate →nested loop join
A nested loop join runs its inner input once for every row of its outer input, looking up the matches each time.
Go to Nested loop, hash, merge — and what a hash does when it does not fit →hash join
A hash join reads one input into an in-memory hash table keyed on the join column, then streams the other input past it. (A hash table finds the entries for a key directly, without scanning them all hash tables.)
Go to Nested loop, hash, merge — and what a hash does when it does not fit →merge join
A merge join takes two inputs already sorted on the join key and walks them together, the way you would merge two sorted lists.
Go to Nested loop, hash, merge — and what a hash does when it does not fit →Bucket (hash join)
It is read once, and each of its rows is placed in a bucket.
Go to Nested loop, hash, merge — and what a hash does when it does not fit →hash_mem_multiplier
Its budget is work_mem, the per-node limit the cost model introduced what a cost is, multiplied by hash_mem_multiplier.
Go to Nested loop, hash, merge — and what a hash does when it does not fit →Batch (hash join)
It batches:
Go to Nested loop, hash, merge — and what a hash does when it does not fit →Memoize
PostgreSQL 14 added Memoize.
Go to Nested loop, hash, merge — and what a hash does when it does not fit →keyset pagination
That is why keyset pagination replaces it on any table that grows.
Go to Sorting, grouping and the nodes between →top-N heapsort
A top-N heapsort is the bounded case.
Go to Sorting, grouping and the nodes between →Incremental Sort
Since PostgreSQL 13, an Incremental Sort sorts each group of equal prefix values separately, and emits it as soon as the group is complete. (In the source, nodeIncrementalSort.c, it first gathers a small batch of rows — at least 32 — and sorts that on all the keys, in case the early groups are tiny.
Go to Sorting, grouping and the nodes between →GroupAggregate
GroupAggregate — the sorted strategy.
Go to Sorting, grouping and the nodes between →HashAggregate
HashAggregate — the hashed strategy.
Go to Sorting, grouping and the nodes between →independence assumption
The independence assumption is the planner's rule that the conditions of a conjunction are statistically independent, so that .
Go to Where it breaks, and the fix →Functional dependency degree
With dependencies collected, ANALYZE measures the degree of the dependency city → state.
Go to Where it breaks, and the fix →auto_explain
It ran once, under production parameters, and your rerun gets a different plan. auto_explain is a module shipped with PostgreSQL that writes to the server log the plan of every statement slower than a threshold, as it actually ran.
Go to Reading a plan as evidence →Inclusive time
A node's actual time is inclusive: it covers the node's own work and everything its children did for it, per loop.
Go to EXPLAIN's options, and seeing a plan →exclusive time
To find where time was actually spent, you need each node's exclusive time.
Go to EXPLAIN's options, and seeing a plan →inverted index
An inverted index maps each key — a word, an array element, a JSON path — to the sorted list of records that contain it.
Go to GIN: one row, many keys →entry tree
Its keys live in a B-tree of their own, the entry tree.
Go to GIN: one row, many keys →Posting list (GIN)
A key that appears in a handful of rows has a short TID list, a posting list.
Go to GIN: one row, many keys →posting tree
Its list becomes a posting tree: a B-tree of its own, over TIDs.
Go to GIN: one row, many keys →pending list
With fastupdate on, which is the default, new entries go into the pending list: an unsorted list of recently inserted entries, not yet merged into the entry tree.
Go to GIN: one row, many keys →containment
a @> b is containment: true when document a includes every key and value of b at the same paths.
Go to JSONB inside a relational engine →jsonb_ops
The default operator class, jsonb_ops, indexes every key and every value as separate GIN keys.
Go to JSONB inside a relational engine →jsonb_path_ops
jsonb_path_ops indexes one hash per complete path-and-value.
Go to JSONB inside a relational engine →Expression statistics
Since PostgreSQL 14 the same statistics can be had without an index, through expression statistics: CREATE STATISTICS … ON (payload ->> 'status') FROM docs.
Go to JSONB inside a relational engine →Lexeme
So both the document and the query are converted into lexemes — normalised word roots — and matched as lexemes:
Go to tsvector, tsquery and ranking →Stop word
The stop words were dropped.
Go to tsvector, tsquery and ranking →stemming
The rest went through stemming, the reduction of each word to a root form.
Go to tsvector, tsquery and ranking →tsvector
A tsvector is a document reduced to a sorted set of distinct lexemes, each with its positions (and optionally a weight).
Go to tsvector, tsquery and ranking →tsquery
A tsquery is a boolean expression over lexemes, built with &, |, ! and the phrase operator <->.
Go to tsvector, tsquery and ranking →text search configuration
Which transformations apply is decided by a text search configuration.
Go to tsvector, tsquery and ranking →dictionary
A dictionary is a rule that turns a token into zero or more lexemes: a stemmer, a stop-word list, a synonym table.
Go to tsvector, tsquery and ranking →Recheck
So GiST returns candidates, and PostgreSQL rechecks each against the real value, discarding false positives.
Go to GiST and SP-GiST: the generalisation →KNN search
KNN-GiST (9.1+) gives nearest-neighbour search in index order, where the index returns rows in increasing distance from a point without computing and sorting every distance.
Go to GiST and SP-GiST: the generalisation →exclusion constraint
An exclusion constraint (PostgreSQL 9.0, Jeff Davis) generalises the relation: no two rows may have values related by any operator you choose.
Go to 'No overlaps' as a constraint, and temporal keys →block range
BRIN stores a summary per block range: a run of consecutive heap pages, 128 by default.
Go to An index that stores almost nothing →Correlation (statistics)
Before creating one, check the column's correlation in pg_stats, the system view that shows what ANALYZE recorded about each column.
Go to An index that stores almost nothing →Summarization (BRIN)
Summarization is the step that computes a block range's summary and stores it in the index.
Go to An index that stores almost nothing →minmax-multi (BRIN)
PostgreSQL 14 added a partial remedy for the few-outliers case: the minmax_multi operator classes (timestamptz_minmax_multi_ops and friends) keep up to values_per_range values per range, 32 by default.
Go to An index that stores almost nothing →extension
An extension is a named, versioned package of database objects — types, functions, operators, operator classes, access methods.
Go to Why PostGIS and pgvector can exist →extension point
Each row of the table below is a different extension point: a place in the server where an extension can add or replace behaviour.
Go to Why PostGIS and pgvector can exist →hook
A hook is a function pointer the server calls at a fixed point, such as before planning or after execution.
Go to Why PostGIS and pgvector can exist →custom scan
A custom scan is a plan node whose behaviour an extension supplies.
Go to Why PostGIS and pgvector can exist →background worker
A background worker is a server process an extension asks the postmaster to start and restart.
Go to Why PostGIS and pgvector can exist →foreign data wrapper
A foreign data wrapper (FDW) is a driver that makes a table stored elsewhere — another PostgreSQL, a file, another database — queryable as though it were local.
Go to Why PostGIS and pgvector can exist →approximate nearest-neighbour index
An approximate nearest-neighbour index returns rows close to a query vector without guaranteeing they are the closest, trading accuracy for speed.
Go to pgvector inside PostgreSQL: what the page, MVCC, vacuum and WAL force on a graph index →recall
Its accuracy is measured as recall: the fraction of the true nearest neighbours that appear among the results it returns.
Go to pgvector inside PostgreSQL: what the page, MVCC, vacuum and WAL force on a graph index →Post-filtering
Post-filtering is applying a WHERE condition to the rows an index has already chosen, instead of letting the index choose among only the rows that satisfy the condition.
Go to Filtered vector search: post-filtering, iterative scans and partitions →Iterative index scan
pgvector 0.8.0 (October 2024) added iterative index scans.
Go to Filtered vector search: post-filtering, iterative scans and partitions →Genetic query optimizer (GEQO)
The genetic query optimizer is PostgreSQL's non-exhaustive join-order search.
Go to Join ordering as a search problem →rewriter
The rewriter is the stage that takes that tree and transforms it by stored rules before the planner sees it.
Go to The rewriter: views and row-level security become the query →optimisation fence
An optimisation fence is a construct in a view or subquery that stops the planner moving an outer predicate inside it, because filtering earlier would change the result.
Go to The rewriter: views and row-level security become the query →InitPlan
InitPlan — an InitPlan is a subquery that does not reference the outer query and returns a single value; such a subquery is called a scalar subquery.
Go to Subqueries, CTEs and what the planner flattens →SubPlan
SubPlan — a SubPlan is a subquery that references the outer row (it is correlated), so it runs once per outer row.
Go to Subqueries, CTEs and what the planner flattens →Hashed SubPlan
hashed SubPlan — a hashed SubPlan handles an uncorrelated IN or NOT IN test that could not become a join.
Go to Subqueries, CTEs and what the planner flattens →CTE inlining
CTE inlining substitutes its definition into the outer query like a subquery in FROM.
Go to Subqueries, CTEs and what the planner flattens →extended query protocol
The extended query protocol.
Go to Custom plans, generic plans and the five-execution rule →prepared statement
It creates a prepared statement: the parsed, analysed and rewritten statement, kept in the server so it can be executed again.
Go to Custom plans, generic plans and the five-execution rule →custom plan
For a prepared statement with parameters, a custom plan is made for one execution with the parameter values substituted, so the planner can use the statistics for those values.
Go to Custom plans, generic plans and the five-execution rule →Gather node
A Gather node is the point in a parallel plan where rows produced by several processes are funnelled back into one.
Go to When the planner adds workers →Leader (parallel query)
Above it, everything runs in the original backend, called the leader.
Go to When the planner adds workers →Parallel query
Parallel query is PostgreSQL's execution of one plan by several processes.
Go to When the planner adds workers →Partial aggregate
Each worker computes a partial aggregate over its own rows: a running state, such as a sum and a count, not yet a final answer.
Go to When the planner adds workers →parallel safety
Whether any of this is allowed depends on parallel safety, a label on every function and operation.
Go to When the planner adds workers →JIT compilation
PostgreSQL's just-in-time (JIT) compilation speeds up two things:
Go to JIT compilation, and why it is often turned off →Tuple deforming
Tuple deforming: unpacking a row from its on-disk layout into column values tuple layout.
Go to JIT compilation, and why it is often turned off →idx_scan
idx_scan: the number of index scans started on it since the statistics were last reset.
Go to Choosing, and paying for, an index →base backup
IngredientProvides A base backup — the whole data directory, copied while the server runsThe data files as of some moment.
Go to Redo, and only redo →WAL archive
The second is the WAL archive.
Go to Redo, and only redo →recovery target
The third is a recovery target, saying where to stop.
Go to Redo, and only redo →Point-in-time recovery
Point-in-time recovery (PITR) restores a base backup and replays archived WAL from the backup's start up to a chosen recovery target, producing the database as it was at that moment.
Go to Redo, and only redo →Streaming replication
Streaming replication keeps a standby current by sending it the primary's WAL over a network connection as it is written, rather than as finished segment files; the standby writes, flushes and replays each record in order, so it is a server permanently in recovery.
Go to A standby is a permanent recovery →walsender
The walsender is a backend on the primary dedicated to one consumer.
Go to A standby is a permanent recovery →walreceiver
The walreceiver is the process on the standby that receives it and writes it to the standby's pg_wal.
Go to A standby is a permanent recovery →hot standby
A hot standby is a standby that accepts read-only queries while it replays, showing each query the state replay has reached.
Go to A standby is a permanent recovery →warm standby
A standby that replays without answering queries is a warm standby.
Go to A standby is a permanent recovery →cascading replication
That arrangement is cascading replication, available since 9.2.
Go to A standby is a permanent recovery →recovery conflict
A recovery conflict arises when replaying a WAL record on a hot standby would remove or lock something that a query running on the standby still needs.
Go to Why a standby cancels your query →max_standby_streaming_delay
Replay waited for the query until it had fallen max_standby_streaming_delay (default 30 s) behind the arrival of the WAL it was trying to apply.
Go to Why a standby cancels your query →hot_standby_feedback
The three responses, and what each costs ResponseEffectCost max_standby_streaming_delay higherThe standby tolerates queries for longer before cancellingReplication lag — replay may now fall up to that far behind, so the standby is a worse failover candidate hot_standby_feedback = onThe standby reports its oldest snapshot to the primary, which then does not vacuum past itBloat on the primary — the standby's queries…
Go to Why a standby cancels your query →catalog_xmin
So a logical slot holds back its catalog_xmin.
Go to The retention contract →failover slot
A slot that is copied this way is a failover slot.
Go to The retention contract →historic catalog snapshot
So the decoder reads the catalog through a historic catalog snapshot — the catalog as it stood when the record was written.
Go to Escaping the physical form →reorder buffer
PieceDoes Logical decodingReads WAL, consults a historic catalog snapshot, and produces row-level changes Reorder bufferThe reorder buffer holds a transaction's changes until it commits, so consumers see only committed work in commit order Output pluginAn output plugin formats those changes — pgoutput, built in, for native replication; third-party plugins such as wal2json and decoderbufs for external consumers Pub…
Go to Escaping the physical form →output plugin
Besides the built-in pgoutput, there are third-party output plugins: wal2json emits JSON text, and decoderbufs emits Protocol Buffers, Google's compact binary message format.
Go to Escaping the physical form →publication
PieceDoes Logical decodingReads WAL, consults a historic catalog snapshot, and produces row-level changes Reorder bufferThe reorder buffer holds a transaction's changes until it commits, so consumers see only committed work in commit order Output pluginAn output plugin formats those changes — pgoutput, built in, for native replication; third-party plugins such as wal2json and decoderbufs for external consumers Pub…
Go to Escaping the physical form →subscription
PieceDoes Logical decodingReads WAL, consults a historic catalog snapshot, and produces row-level changes Reorder bufferThe reorder buffer holds a transaction's changes until it commits, so consumers see only committed work in commit order Output pluginAn output plugin formats those changes — pgoutput, built in, for native replication; third-party plugins such as wal2json and decoderbufs for external consumers Pub…
Go to Escaping the physical form →REPLICA IDENTITY
A table's REPLICA IDENTITY is the setting that says which old values those records carry:
Go to Escaping the physical form →synchronous_standby_names
Each one connects under a name, its application_name. synchronous_standby_names is a setting on the primary, written in postgresql.conf.
Go to Synchronous commit: what a commit waits for →Quorum commit
Quorum commit — synchronous_standby_names = 'ANY k (…)' — makes a commit wait until any of the listed standbys have acknowledged it, irrespective of their order in the list.
Go to Synchronous commit: what a commit waits for →lock_timeout
The setting that limits it is lock_timeout.
Go to Why one ALTER blocks a thousand SELECTs →root blocker
The first row with an empty blocked_by is the root blocker: the session that waits for nobody while others wait for it.
Go to Why one ALTER blocks a thousand SELECTs →table rewrite
A strong lock held for a table rewrite is different.
Go to Migrations that do not take the table down →Volatility (function)
The first is a function's volatility: its declared promise about repeat calls.
Go to Migrations that do not take the table down →NOT VALID constraint
A constraint added this way is a NOT VALID constraint.
Go to Migrations that do not take the table down →invalid index
Then it leaves an invalid index behind: one recorded in the catalog as not valid, so the planner never uses it.
Go to Migrations that do not take the table down →deadlock_timeout
It waits first, for deadlock_timeout: the time a backend waits for a heavyweight lock before it checks for a deadlock, one second by default.
Go to Deadlock detection, and advisory locks →advisory lock
An advisory lock is a heavyweight lock on a key the application chooses — a 64-bit integer, or a pair of 32-bit ones — that corresponds to no table or row.
Go to Deadlock detection, and advisory locks →SKIP LOCKED
SKIP LOCKED is an option of a row-locking clause such as FOR UPDATE.
Go to Job queues on PostgreSQL →lease
That needs a lease: a claim that expires unless renewed.
Go to Job queues on PostgreSQL →declarative partitioning
In declarative partitioning, a partitioned table has no storage of its own.
Go to Declarative partitioning, and its sharp edges →partition key
PostgreSQL routes every inserted or updated row to the partition whose bounds contain its partition key, the column or expression named in PARTITION BY.
Go to Declarative partitioning, and its sharp edges →default partition
A default partition is one that catches rows matching no other partition's bounds.
Go to Declarative partitioning, and its sharp edges →partition-wise join
That is a partition-wise join.
Go to Declarative partitioning, and its sharp edges →pg_hba.conf
pg_hba.conf ("host-based authentication") is the file of rules that chooses an authentication method for each connection.
Go to Admitting a connection: pg_hba.conf and the method it chooses →pg_ident.conf
List the allowed pairs in pg_ident.conf, the "identity" file.
Go to Admitting a connection: pg_hba.conf and the method it chooses →salt
At login the server sends a random 4-byte salt: randomness chosen per exchange, so a stored hash and a given reply cannot simply be replayed.
Go to What is stored and proved: MD5, SCRAM, channel binding and OAuth →nonce
Client and server each contribute a random nonce — a value used once, so that replaying a recorded exchange produces a different transcript; the server sends and .
Go to What is stored and proved: MD5, SCRAM, channel binding and OAuth →libpq
The client in the diagram is libpq, the C client library that nearly every PostgreSQL client is built on — psql (the command-line client), Python's psycopg and many others:
Go to What is stored and proved: MD5, SCRAM, channel binding and OAuth →SCRAM verifier
A SCRAM verifier is what the server stores for a password under SCRAM: the salt, the iteration count, a hash of the client key and the server key.
Go to What is stored and proved: MD5, SCRAM, channel binding and OAuth →Channel binding
Channel binding ties the password exchange to the TLS connection it runs in.
Go to What is stored and proved: MD5, SCRAM, channel binding and OAuth →listen_addresses
Whether it can is decided by listen_addresses: the comma-separated list of interfaces the postmaster binds.
Go to The wire: listen_addresses, sslmode and certificates →TLS
TLS (Transport Layer Security) is the encrypted-channel protocol a PostgreSQL TCP connection can run inside.
Go to The wire: listen_addresses, sslmode and certificates →sslmode
The client decides what it will do with it. sslmode is the libpq connection setting that makes that decision: whether the client uses TLS at all, and how much of the server's certificate it checks.
Go to The wire: listen_addresses, sslmode and certificates →role
A role is a named identity that can own objects, hold privileges and be a member of other roles.
Go to Roles, ownership and the search_path trap →SET ROLE
After switching the session to it with SET ROLE.
Go to Roles, ownership and the search_path trap →Owner (object)
The owner of a database object is the role recorded as its owner in the catalog.
Go to Roles, ownership and the search_path trap →schema
A schema is a namespace inside a database: a set of names in which each name is unique.
Go to Roles, ownership and the search_path trap →search_path
An unqualified name such as orders or lower(x) is looked up along the search_path, a list of schemas searched in order.
Go to Roles, ownership and the search_path trap →SECURITY DEFINER function
A SECURITY DEFINER function runs with the privileges of its owner instead.
Go to Roles, ownership and the search_path trap →Predefined role
A set of predefined roles hands out narrower slices, the way Linux capabilities split root Linux capabilities.
Go to Roles, ownership and the search_path trap →BYPASSRLS
Predefined role or privilegeSinceGrants pg_read_all_data, pg_write_all_data14Read, or write, every table, view and sequence — but not bypass row-level security; that exemption is the separate BYPASSRLS role attribute pg_database_owner14Whatever is granted to the current database's owner; owns public since 15 pg_create_subscription; a narrower CREATEROLE16Creating logical-replication subscriptions; CREATEROLE can n…
Go to Roles, ownership and the search_path trap →Trusted extension
An extension marked trusted in its control file can be installed by any role with CREATE on the database.
Go to The superuser boundary: reaching the operating system, logging and encryption at rest →row-level security
That is what row-level security (RLS) is for.
Go to Row-level security: the predicate the query never wrote →Row-level security policy
A row security policy is created with CREATE POLICY.
Go to Row-level security: the predicate the query never wrote →permissive policies
The default kind, permissive policies, are ORed together.
Go to Row-level security: the predicate the query never wrote →restrictive policies
Any restrictive policies (AS RESTRICTIVE) are ANDed together on top.
Go to Row-level security: the predicate the query never wrote →Leakproof function
Functions and operators marked leakproof may run before the policy check.
Go to Row-level security: the predicate the query never wrote →query identifier
A query identifier is a 64-bit hash of a statement's parse tree (the structure the parser builds from the SQL text) after its constants have been removed.
Go to Finding the query that matters →session pooling
In session pooling a client keeps its server connection until it disconnects.
Go to What a pooler changes, and what it breaks →statement pooling
In statement pooling the connection returns to the pool after every statement, so a transaction spanning several statements is refused.
Go to What a pooler changes, and what it breaks →transaction pooling
In transaction pooling a client holds a server connection only from the start of a transaction to its commit or rollback.
Go to What a pooler changes, and what it breaks →logical dump
The three mechanisms pg_dumpBase backupBase backup + WAL archive ProducesA logical dump: the database's contents as SQL, or a custom archive of it, independent of the on-disk formatA copy of the data directory base backupThe same, plus every WAL segment since, kept in an archive Restores toThe moment the dump startedThe moment the backup finishedAny moment covered by the archive Cross-versionYesNoNo SelectiveYes —…
Go to What you can actually promise →Major version
A PostgreSQL major version is the first number — 16, 17, 18.
Go to Major-version upgrades →Configuration
The configuration parameters this course argues about, with what each default costs and the topic that makes the case.
Not a copy of the documentation’s parameter list — that exists and is better. These are the parameters this course argues about, and each entry carries the thing a reference page does not: what the default costs, what to do instead, and the topic that makes the case.
The line under each name gives the default and whether changing it needs a restart, since those are the two facts you need before touching anything. A parameter settable per table, per role or per transaction says so — several of the most valuable changes in this course are per-table overrides rather than cluster-wide ones.
shared_buffers
PostgreSQL's own page cache, fixed at startup.
More detail
The only bounded allocation in the whole configuration. Roughly 25 % of RAM is a starting point rather than timidity: PostgreSQL reads and writes through the OS page cache as well, so memory given here is taken from a cache the design still depends on. A setting that would be right for InnoDB is wrong here for exactly that reason.
work_mem
Memory for one sort or one hash.
More detail
Per node, per backend, per parallel worker — so the worst case is a product of four numbers nobody configured, and nothing enforces a total. Set it modestly for the common query and raise it per role for the exceptional one; sizing the global value for your worst plan is how a machine that looked fine for a year runs out of memory during a traffic peak.
maintenance_work_mem
Memory for vacuum, index builds and other maintenance.
More detail
Vacuum holds dead tuple identifiers here between its heap and index passes; too little forces repeated full sweeps of every index, multiplying total I/O by the index count. A GIN build leans on it harder still, since its work scales with total keys rather than rows. Per worker, so multiply by autovacuum_max_workers when budgeting.
huge_pages
Map shared memory with huge pages.
More detail
At large shared_buffers the page tables mapping it into every backend become significant, since hundreds of processes each map the same region. Huge pages cut the entry count by a factor of 512, saving memory and TLB pressure. Worth setting to on with pages reserved on any machine with more than a few gigabytes of shared buffers.
autovacuum_vacuum_scale_factor
Fraction of the table added to the vacuum trigger.
More detail
The single most valuable per-table override in PostgreSQL. At the default a table is permanently up to 20 % dead tuples, and the absolute wait grows without limit: 100 million dead rows on a 500-million-row table is most of a day. Set it around 0.01 on large tables. Not tuning — required configuration.
autovacuum_vacuum_max_threshold
Caps the trigger the scale factor computes.
More detail
Added in PostgreSQL 18, and effectively the project conceding that the unbounded scale factor was a known-bad default for large tables. It bounds the damage but does not remove the case for a per-table scale factor, which triggers far sooner.
autovacuum_vacuum_cost_limit
Work vacuum may do before sleeping.
More detail
Shared across all autovacuum workers, not allocated per worker — which is why raising autovacuum_max_workers slows each one down instead of speeding cleanup up. The default was chosen when a database’s disk was a shared spinning one; 1000–2000 is routinely appropriate on modern storage. Raise this before adding workers.
autovacuum_vacuum_insert_threshold
Triggers vacuum on inserts as well as dead tuples.
More detail
An append-only table creates no dead tuples, so before PostgreSQL 13 it was never vacuumed — and therefore never frozen and never marked all-visible, so index-only scans did not work on it and wraparound crept up. This trigger exists for time-series tables specifically.
autovacuum_freeze_max_age
Age at which a wraparound-prevention vacuum is forced.
More detail
Under 10 % of the usable transaction-id window, and the margin exists so the forced vacuum has time to finish on a large table. Alert on age(datfrozenxid) rather than on this: a steadily rising value means freezing is losing, and the remaining time can be days rather than months.
idle_in_transaction_session_timeout
Terminates a session holding a transaction open while doing nothing.
More detail
The highest-value single setting in this course for a cluster that lacks it. An idle-in-transaction session holds the xmin horizon and bloats every table in the cluster, can sit at the head of a lock queue and take a table down, and occupies a pool slot. There is essentially no legitimate reason to sit idle in a transaction for minutes.
random_page_cost
Cost of a random page read, relative to a sequential one.
More detail
The highest-value planner change on modern hardware. The default encodes a mechanical seek on a spinning disk; on SSD or NVMe the real ratio is nearer 1.1, and leaving it at 4 makes the planner systematically reject index scans it should choose. It remains the default only because changing it would move plans on every existing installation.
effective_cache_size
How much memory the planner should assume is caching data.
More detail
Allocates nothing — it is a planner input describing shared_buffers plus the OS page cache together, so the planner can judge how much of an index scan’s repeated access will hit memory. Set too low, index scans look unattractive. Half to three quarters of system memory is a reasonable value.
default_statistics_target
Sets the MCV list length and the ANALYZE sample size.
More detail
The sample is 300 × this value and does not scale with the table, which is why distinct-value estimates degrade on large tables while frequency estimates stay accurate. Raise it on the specific columns whose estimates are wrong rather than globally: a higher global value lengthens every ANALYZE and adds planning time to every query.
join_collapse_limit
How many relations the planner optimises as one problem.
More detail
Simultaneously a planning-time control and the only built-in way to constrain join order. Setting it to 1 makes the planner honour your written join order exactly — the closest thing PostgreSQL has to a join hint, and a commitment to maintaining that order by hand. Set it per query, never globally.
geqo_threshold
Relation count past which the planner searches genetically.
More detail
Exhaustive dynamic programming is Θ(3ⁿ), affordable to about this many relations. Past it the genetic search gives no optimality guarantee and is seeded randomly, so a large-join query can receive different plans on different executions — a candidate cause when warehouse query performance is erratic with no other change.
plan_cache_mode
Overrides the generic-versus-custom plan decision.
More detail
The automatic heuristic builds five custom plans and then switches to the generic plan if it costs less, and never revisits that decision. force_custom_plan pays planning time on every execution and buys parameter-aware plans — the fix when a skewed column causes erratic latency, and when a partitioned table loses plan-time pruning.
checkpoint_timeout
Maximum interval between checkpoints.
More detail
Recovery time is proportional to it and WAL volume is inversely proportional, because every page is re-imaged once per cycle. The costs are asymmetric in frequency — WAL is paid every second, recovery only after a crash — so 15–30 minutes is right for most systems, which is the opposite of what the default suggests.
max_wal_size
WAL volume that triggers a checkpoint.
More detail
A checkpoint triggered by this is *requested* rather than timed, and a high proportion of requested checkpoints is a feedback loop: more checkpoints means more full-page images means more WAL means more checkpoints. The default is small for any busy system, and raising it costs disk and nothing else.
wal_compression
Compress full-page images in the WAL.
More detail
Off by default and close to a free win. It compresses the page images specifically — the dominant term in WAL volume — and typically recovers a large fraction of the cost of a scattered write pattern. Try it before considering full_page_writes = off, which trades the same reduction for a risk of silent corruption.
full_page_writes
Write a whole page image on its first change after a checkpoint.
More detail
The defence against torn pages, which redo cannot repair because replay needs a structurally valid page to start from. Safe to disable only on storage with atomic 8 kB writes or a copy-on-write filesystem; on ordinary devices it risks corruption discovered weeks later.
synchronous_commit
How far a commit must travel before it is acknowledged.
More detail
Five positions, each naming a different failure it does not survive. Setting it off risks losing recent commits, not corruption — a different thing from fsync = off entirely. Per-transaction granularity is the feature: relax it for the ingest that replays and leave the payments alone.
max_slot_wal_keep_size
Bounds the WAL a replication slot may retain.
More detail
Unlimited by default, which converts "a replica went offline" into "the primary filled its disk and stopped accepting writes". Setting it chooses which thing breaks: one consumer, marked lost and rebuilt, rather than the primary. Belongs in the base configuration of any cluster with replication.
hot_standby_feedback
The standby reports its oldest snapshot to the primary.
More detail
Stops standby queries being cancelled, by holding the primary’s xmin horizon back — so the cost moves to bloat on the primary rather than disappearing. Right for a replica running short queries, where the horizon is held briefly. Wrong for a reporting replica, which wants a high max_standby_streaming_delay instead so the cost stays local.
max_standby_streaming_delay
How long replay waits for a conflicting query before cancelling it.
More detail
On a dedicated reporting replica, set this very high or to -1: lag does not matter there and the primary is unaffected, which is the cheapest place to put a cost that cannot be removed. On a failover candidate keep it low, since that replica must stay caught up.
synchronous_standby_names
Which standbys a commit must reach.
More detail
Use the quorum form, ANY k (…). Naming a single standby makes the primary's availability depend on it — take that standby down for maintenance and every commit hangs. One synchronous standby combines the latency cost with an availability penalty and buys nothing the quorum form does not.
max_connections
Backends that may exist at once.
More detail
Sizes shared memory at startup — the lock table alone is allocated max_locks_per_transaction × (max_connections + max_prepared_transactions) slots — so raising it speculatively costs immediately. It is a capacity number derived from λ·E[S] plus headroom for autovacuum, replication and an admin connection, not a number to raise when connections run out.
max_locks_per_transaction
Sizes the shared lock table.
More detail
An average across transactions rather than a per-transaction cap: one statement may take far more as long as the total fits. It is reached by queries touching hundreds of partitions, since each partition is locked separately, and the failure is "out of shared memory" rather than a wait.
deadlock_timeout
How long a lock wait runs before a deadlock check.
More detail
A detection-latency knob, not a tolerance for deadlocks. Detection is lazy because lock waits are common and deadlocks are rare, so checking on every request would add cost to the overwhelmingly common case. The default rarely needs changing.
lock_timeout
How long a statement waits for a lock before failing.
More detail
The fix for the lock-queue pile-up, and the reason it works is that a waiting strong lock blocks everything behind it — so the unavailable window is however long the DDL waits. Set it to a few seconds around every migration and retry; failing is the desired outcome.
statement_timeout
Bounds how long any single statement may run.
More detail
Set per role rather than globally, so analytical roles get a longer bound than the application path. Together with idle_in_transaction_session_timeout it bounds the two ways a session can hold the xmin horizon indefinitely.