Your technical library
DBMS
16 modules · 102 topics
About this book
DBMS
This course builds a relational database engine from nothing. It starts with the problem a database solves — what goes wrong when a program keeps its data in plain files — and follows a query and a transaction down through every layer that answers it: the relational model and SQL, schema design, how tables sit on pages on disk, the indexes that find rows, the operators that run a query and the optimiser that chooses them, the transactions and concurrency control that keep many users correct, and the log that makes a crash survivable. It is written for any engineer who uses databases and wants to know what is inside one, and it assumes no database knowledge at all.
It builds on Data Structures and Algorithms for the in-memory structures and sorting it reuses, on Operating Systems for disks, the page cache, `fsync` and locks, and on Mathematics for the probability behind query estimates. Everything about databases themselves is taught here, from the first definition.
What a database is, the relational model and SQL give the vocabulary and the language: tables, keys and constraints, the algebra every query compiles into, and SQL read as that algebra — including the places, such as NULL, where SQL quietly departs from it. Schema design turns a domain into tables that cannot contradict themselves. Storage and the cost model puts tables on pages and prices every operation in page reads and writes, and indexes and B+ trees and hashing and specialised indexes build the structures that avoid reading everything. Query execution builds the operators that scan, join, sort and aggregate, and query optimisation the planner that chooses among them.
Transactions says what a database promises when many programs change data at once, and locking and timestamps, optimism and versions are the two families of mechanism that keep that promise. Recovery makes committed work survive a crash. Log-structured storage then builds the other great storage design — the LSM-tree — and compares it with the B-tree on the same scale. The last two modules go past the textbook: modern engines asks what changes when data no longer sits on spinning disks, and analytical engines shows how a column store scans a fraction of the data a query covers.
Design decisions are shown against engines that chose differently — PostgreSQL, MySQL's InnoDB, SQLite, RocksDB, LMDB and DuckDB — so each is visibly a choice. The PostgreSQL course then shows what one real engine chose, and System Design what these mechanisms become across many machines.
A few topics are marked optional: the theory behind the practice — relational calculus, the expressive-power proof, the normalisation algorithms and fourth normal form. Nothing later depends on them, so you can pass over them and come back.
102 topics across 16 modules, in dependency order.
This course builds a relational engine from first principles. Not how to use one, and not how one behaves across machines — how one is constructed: the dependency theory a schema rests on, the file and index structures that make a query fast, the algorithms that execute it, the protocols that keep it correct under concurrency, and the recovery method that makes a crash survivable.
A topic ends at the algorithm, not at the name. Each one states its invariant, gives the algorithm in steps, derives its cost in page I/Os, and names the case where it degrades — so you could implement it, or hand-execute it on a five-element example and get the right answer.
Design decisions are shown against engines that chose differently. PostgreSQL, InnoDB, SQLite, RocksDB, LMDB and DuckDB disagree with each other constantly, and those disagreements are the evidence that a decision was real rather than inevitable.
Topics still being written are labelled and excluded from progress, so the count reflects what is actually finished.
The same subject can appear in more than one course here, and what separates the appearances is altitude. B-trees are the clearest case: the node layout, the split and merge algorithms and the latch protocol are in this course; why a page-oriented structure beats a file at all, and when an LSM beats a B-tree, is System Design; what nbtree actually does and what it costs in production is PostgreSQL. Where this course needs a result one of the others already derived, it links back to it and does not repeat it.
Read at your own pace. Each topic is one continuous page; mark it done as you finish.
Start with 01.1 · Why a database: what goes wrong when a program keeps its data in filesReview what you have finished↷ 0 skipped topics- 03.1SQL: SELECT, FROM and WHERE are the algebra
- 03.2SQL works on bags, not sets: the identities duplicates break
- 03.3Joins in SQL: inner, outer, semi and anti
- 03.4The semantics that silently returns the wrong answer
- 03.5Grouping and aggregation: GROUP BY, HAVING and the order clauses run in
- 03.6Subqueries and set operations: a query inside a query
- 03.7Views and common table expressions: naming a query
- 03.8Window functions: aggregates that keep every row
- 03.9Recursive queries: WITH RECURSIVE, and why it terminates
- 03.10What the algebra cannot say: recursion and expressive powerOptional
- 03.11Triggers and stored procedures: code that runs inside the database
- 04.1The entity–relationship model: drawing the domain before the tables
- 04.2The mapping, and what it loses
- 04.3Constraints the keys do not capture
- 04.4The two criteria, and why they conflict
- 04.5Normal forms: 1NF to BCNF, and the anomalies each removes
- 04.6Normalisation algorithms: 3NF synthesis and BCNF decompositionOptional
- 04.7Redundancy that no functional dependency explainsOptional
- 06.1What an index has to answer
- 06.2B-trees: pages, fanout and height
- 06.3The invariant, the fanout, the height
- 06.4Indexes: clustered, secondary & covering
- 06.5How the tree grows
- 06.6How the tree shrinks, and why it usually does not
- 06.7Keys decide the fanout
- 06.8Descending a tree other threads are changing
- 06.9What a latch is made of
- 14.1The append-only log
- 14.2Hash indexes over a log
- 14.3The memtable
- 14.4The SSTable: sorted and immutable
- 14.5The write-ahead log in an LSM-tree: making the memtable durable
- 14.6Compaction & tombstones
- 14.7Bloom filters
- 14.8Leveling vs tiering
- 14.9Big picture: B-tree vs LSM and the three amplifications
- 14.10Trading read cost for write cost, on purpose
- 16.1Encodings chosen for the scan, not the disk
- 16.2Executing on the encoding: the predicate never sees the value
- 16.3Late materialisation: when to stitch columns back into rows
- 16.4Zone maps: an index of two numbers per block, and when it works
- 16.5One physical order for many predicates
- 16.6Open columnar file formats: a storage layer with no engine attached
- 16.7Table formats: transactions over immutable files
- 16.8Deleting a row from an immutable file
- 16.9Four analytical engines, four sets of decisions
01.2Schemas, instances and data independence: describe the data once
01.3Inside a database engine: the life of one query and one update
02.1Relations, keys and identity: what a table is in the model
02.2Integrity constraints: rules the database refuses to break
02.3The algebra is the optimiser's rulebook
02.4Relational calculus: saying what you want, not how to get it
Optional Background reading: no later topic depends on it, so read it now or come back to it.
03.1SQL: SELECT, FROM and WHERE are the algebra
03.2SQL works on bags, not sets: the identities duplicates break
03.3Joins in SQL: inner, outer, semi and anti
03.4The semantics that silently returns the wrong answer
03.5Grouping and aggregation: GROUP BY, HAVING and the order clauses run in
03.6Subqueries and set operations: a query inside a query
03.7Views and common table expressions: naming a query
03.8Window functions: aggregates that keep every row
03.9Recursive queries: WITH RECURSIVE, and why it terminates
03.10What the algebra cannot say: recursion and expressive power
Optional Background reading: no later topic depends on it, so read it now or come back to it.
03.11Triggers and stored procedures: code that runs inside the database
04.1The entity–relationship model: drawing the domain before the tables
04.2The mapping, and what it loses
04.3Constraints the keys do not capture
04.4The two criteria, and why they conflict
04.5Normal forms: 1NF to BCNF, and the anomalies each removes
04.6Normalisation algorithms: 3NF synthesis and BCNF decomposition
Optional Background reading: no later topic depends on it, so read it now or come back to it.
04.7Redundancy that no functional dependency explains
Optional Background reading: no later topic depends on it, so read it now or come back to it.
05.1Why storage engines exist: seeks vs sequential I/O
05.2The cost table everything else is quoted against
05.3Inside one page
05.4The policy, and the mmap argument
05.5OLTP vs OLAP
05.6Column-oriented storage & compression
05.7Row, column, and the hybrid that came back
06.1What an index has to answer
06.2B-trees: pages, fanout and height
06.3The invariant, the fanout, the height
06.4Indexes: clustered, secondary & covering
06.5How the tree grows
06.6How the tree shrinks, and why it usually does not
06.7Keys decide the fanout
06.8Descending a tree other threads are changing
06.9What a latch is made of
07.1Constant-time lookup, and the cost of growth
07.2Indexes that are not comparison trees
07.3When one ordering is not enough
07.4Choosing indexes from a workload
07.5Where the logical schema meets the disk
08.1How operators hand tuples to each other
08.2Selections and access paths: scan, or use the index?
08.3Sorting more than fits
08.4The three ways, and where each wins
08.5Grouping, distinctness, and counting approximately
08.6Removing the interpreter
08.7Using more than one core
09.1From text to plan
09.2The transformations that are always right
09.3The number everything else multiplies
09.4Turning row counts into a number
09.5Choosing among astronomically many plans
09.6When the estimate was wrong
10.1What a transaction guarantees (ACID)
10.2The isolation anomalies
10.3Isolation levels
10.4Serializability: when an interleaving is as good as running one at a time
10.5Recoverability: serializability is not enough
11.1What each phase actually buys
11.2The lock table, and locking a million rows at once
11.3The price of waiting while holding
11.4Locking rows that do not exist yet
12.1Timestamp ordering and optimistic validation: concurrency control without locks
12.2MVCC & snapshot isolation
12.3Four decisions, not one design
12.4Serializable snapshot isolation: detecting the dangerous structure instead of locking
12.5Why the same level name means different things
13.1Steal, force, and the two obligations
13.2What the log must guarantee
13.3Bounding how far back recovery must read
13.4Analysis, redo, undo
13.5Physical, logical, and not logging at all
14.1The append-only log
14.2Hash indexes over a log
14.3The memtable
14.4The SSTable: sorted and immutable
14.5The write-ahead log in an LSM-tree: making the memtable durable
14.6Compaction & tombstones
14.7Bloom filters
14.8Leveling vs tiering
14.9Big picture: B-tree vs LSM and the three amplifications
14.10Trading read cost for write cost, on purpose
15.1When the buffer pool goes away
15.2The memory hierarchy is the new disk
15.3Removing the agreement problem
15.4The device stopped behaving like a disk
15.5The whole course, read across six engines
16.1Encodings chosen for the scan, not the disk
16.2Executing on the encoding: the predicate never sees the value
16.3Late materialisation: when to stitch columns back into rows
16.4Zone maps: an index of two numbers per block, and when it works
16.5One physical order for many predicates
16.6Open columnar file formats: a storage layer with no engine attached
16.7Table formats: transactions over immutable files
16.8Deleting a row from an immutable file
16.9Four analytical engines, four sets of decisions
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.
Bag semantics
SQL tables are multisets, not sets: duplicate rows exist and are counted.
More detail
The single divergence between SQL and the relational algebra that has consequences everywhere else. It breaks idempotence of union, makes projection unsafe to push through a duplicate-sensitive operator, and is why UNION and UNION ALL are different operators rather than an optimisation of each other.
Three-valued logic
Predicates evaluate to TRUE, FALSE or UNKNOWN, and WHERE keeps only TRUE.
More detail
Every surprising NULL behaviour in SQL is this one rule applied in a place with a different default for UNKNOWN. WHERE rejects UNKNOWN; CHECK accepts it, so a constraint is weaker than the identical predicate in a query; GROUP BY treats NULLs as equal although = does not.
Relational algebra
The closed algebra of operators over relations whose equivalence laws are the optimiser’s legal moves.
More detail
Closure — a relation in, a relation out — is what makes an expression rewritable without changing its meaning. The algebra is therefore not a teaching device but literally the rule set module 06 searches over.
Least fixpoint
The semantics of recursive queries: iterate the rule until nothing new is produced.
More detail
Transitive closure is not expressible in relational algebra because every algebra expression has a fixed number of joins. WITH RECURSIVE evaluates a fixpoint instead. For a linear rule the productive rounds number the longest shortest-path length among reachable pairs — the diameter only with qualifications for disconnected graphs and cycles — plus one empty round to confirm termination; semi-naive evaluation feeds each round only what the previous one newly produced.
Functional dependency
X → Y: any two tuples agreeing on X agree on Y, over every legal instance.
More detail
Over every legal instance, not over one sample — inferring dependencies from data you happen to have is the commonest error in normalisation, because a small instance satisfies many dependencies the domain does not guarantee.
Attribute closure
X⁺, the set of attributes X determines under a dependency set.
More detail
Computed by fixpoint iteration in O(|F|·|R|). It is why the full closure F⁺ never has to be computed: membership reduces to Y ⊆ X⁺. Candidate keys are found by searching attribute closures.
Minimal cover
A dependency set with no redundant dependency and no extraneous attribute.
More detail
Produced by two reduction passes — remove extraneous attributes from left-hand sides, then remove dependencies implied by the rest. It is the input to 3NF synthesis, which is why synthesis produces a schema with no redundant relations.
Lossless join
A decomposition whose natural join reconstructs exactly the original relation.
More detail
The binary test is that the common attributes of the two fragments must be a key of at least one of them. A decomposition failing it produces spurious tuples — rows that were never in the original — which is silent data corruption rather than a performance problem.
Dependency preservation
A decomposition where every original dependency is still checkable within one fragment.
More detail
The second correctness criterion, independent of losslessness. A dependency that survives only across a join is in practice not checked at all, because checking it would require joining on every write.
BCNF
Boyce–Codd normal form: every non-trivial determinant is a superkey, with no exemptions.
More detail
Always achievable losslessly and sometimes not while preserving dependencies — which is exactly why real schemas stop at 3NF. BCNF decomposition is also not confluent: splitting on a different violating dependency yields a different, equally valid BCNF schema.
3NF synthesis
Building a schema directly from a minimal cover rather than by splitting.
More detail
Always yields a decomposition that is both lossless and dependency-preserving, which is the theorem that decides between the two normalisation algorithms. It must add a relation containing a candidate key if none of the produced relations already contains one.
Multivalued dependency
X ↠ Y: independence between two multi-valued facts about the same entity.
More detail
Redundancy a relation in BCNF can still contain, because independence is not a functional determination. Every functional dependency is a multivalued one, so 4NF implies BCNF.
Heap file
An unordered file of records: cheap to insert into, linear to search.
More detail
One of three primary organisations, alongside sorted and hashed files. The cost table over these three is the unit every later operator cost in the course is quoted against.
Slotted page
A page with a slot directory growing from one end and record data from the other.
More detail
The indirection through a slot is what lets a record move within its page during compaction while its identifier stays valid — and whether that identifier is stable decides whether secondary indexes can store it directly or must store the primary key and pay a second lookup.
Sequential flooding
One large scan evicting the entire working set for pages that will never be read again.
More detail
LRU’s characteristic failure in a database, and the reason engines do not use plain LRU. LRU-K is the principled fix — it distinguishes a page referenced twice recently from one referenced once — and ring buffers are the cheap one.
LRU-K
Replacement by the time of the K-th most recent reference rather than the most recent.
More detail
With K = 2 a page touched once by a scan ranks below a page touched twice by the workload, so a scan evicts other once-touched pages before any established one — protection that holds while such victims exist, not an absolute guarantee. It is the policy sequential flooding motivates, and 2Q is its practical approximation.
PAX
Partition Attributes Across: column-major layout inside a row-major page.
More detail
Buys cache behaviour rather than I/O, since the page is still the I/O unit. It returned as the standard layout for engines that must serve both transactional and analytical workloads, and is the shape of every modern columnar file format.
Fanout
The number of children an interior node holds, set by page size divided by entry size.
More detail
Entry size is dominated by the key rather than the pointer, which makes key encoding the real lever on tree height — a 64-byte key and an 8-byte key can differ by a whole level at realistic row counts.
Occupancy invariant
Every node except the root is at least half full, and every leaf is at the same depth.
More detail
The single invariant every B+tree algorithm exists to restore. Split restores it on insert, merge or redistribution on delete, and the fact that the tree grows only at the root is what keeps all leaves level.
Page split
Dividing a full node in two and promoting a separator to the parent.
More detail
A leaf split copies the separator up and keeps the key in the leaf; an interior split moves it up and the key leaves the node. Half of all explanations get that distinction wrong.
Latch crabbing
Taking the child’s latch before releasing the parent’s, then releasing ancestors once a node is provably safe.
More detail
"Safe" means the node cannot split or merge under this operation, and the condition differs for search, insert and delete. It is how a concurrent descent avoids holding the root for its whole duration.
B-link tree
A B+tree with a right link and a high key in every node, so a reader that lands mid-split walks right.
More detail
Lehman and Yao’s design removes the need to hold the parent latch at all during descent. It costs one pointer and one key per page and is the strongest of the practical concurrency answers for mixed read/write workloads.
Optimistic lock coupling
Read a version counter, work without a latch, re-read and restart if it changed.
More detail
Gives lock-free-like read performance without a lock-free data structure: a successful read writes no shared state. Writers still exclude each other, and a stalled writer can make readers retry indefinitely. A buffer pool does not rule it out, but the engine must keep a frame from being reused during an optimistic read — LeanStore integrates the two.
MCS latch
A queue latch where each waiter spins on its own local flag rather than a shared one.
More detail
Test-and-set degrades under contention because every failed attempt pulls the same cache line to another core, so coherence traffic per acquisition grows with the number of waiters, and draining a queue of waiters grows quadratically. An MCS queue keeps remote references per acquisition constant — though not the time spent waiting.
Suffix truncation
Storing in an interior node only enough of a key to separate its children.
More detail
An interior entry is a routing decision, not a value, so the rest of the key is dead weight against fanout. Close to free, and should be assumed present in any serious B+tree.
B^ε-tree
A B-tree whose interior nodes split their space between pivots and an update buffer.
More detail
The ε parameter unifies the write-optimised family: ε = 1 recovers the ordinary B-tree and ε → 0 approaches an LSM, with insert cost falling and search cost rising monotonically between them.
Bw-tree
A lock-free B-tree where updates are prepended as delta records behind a mapping table.
More detail
Every node pointer indirects through the mapping table, so a node is modified by one atomic compare-and-swap and never in place. Shipped in SQL Server Hekaton.
Extendible hashing
A hash file with a directory of 2^d pointers, doubled only when a bucket at the global depth splits.
More detail
Local depth is the field doing the work: a bucket with local depth below the global depth splits without touching the directory. Doubling is O(2^d) pointers but touches no data pages, which is why an operation that sounds catastrophic is cheap.
Linear hashing
Splitting buckets in a fixed round-robin order regardless of which one overflowed.
More detail
Removes the directory entirely, at the cost of splitting buckets that did not need it and tolerating overflow chains on the ones that did.
Adaptive radix tree
A trie with four node sizes chosen by child count, plus path compression.
More detail
A trie’s height depends on key length rather than key count, which wins structurally in memory. ART’s adaptive nodes and path compression fix the two problems — space explosion and depth — that make a naive radix tree unusable.
Learned index
A model that predicts a key’s position, used in place of a tree’s interior levels.
More detail
Genuinely smaller than a tree on read-only data, and unshipped in any mainstream write path: updates need a rebuild or a delta buffer and the error bound is empirical rather than worst-case.
Roaring bitmap
A compressed bitmap that switches container format per 65 536-key block.
More detail
Won over run-length schemes such as WAH because it keeps random access and boolean operations fast rather than only compressing well. It is the posting-list format of most modern search and column engines.
Covering index
An index holding every column a query needs, so the answer can come from the index without fetching from the base relation.
More detail
The escape from the unclustered-index crossover: it converts up to m random page fetches into one leaf-range scan, which is why included columns exist. Coverage is per query, and not every engine can act on it unconditionally — PostgreSQL still visits a heap page that is not marked all-visible, to check row visibility.
Iterator model
Volcano-style execution: every operator exposes open, next and close, and pulls one tuple at a time.
More detail
Composes arbitrarily and needs no scheduler, at the cost of a virtual call per operator per tuple — ruinous when the per-tuple work is a single comparison, which is why analytical engines left it.
Pipeline breaker
An operator that must consume its entire input before producing output.
More detail
Sort, hash build and aggregation. They, rather than the operators generally, determine a plan’s memory footprint and its time-to-first-row, so a plan’s cost profile is read off its breakers.
Vectorised execution
Passing a batch of values per call instead of one tuple, amortising the call and enabling SIMD.
More detail
Wins decisively on scans over many rows and buys nothing for a single-row lookup, which is why tuple-at-a-time and vectorised engines coexist rather than one replacing the other.
External merge sort
Sorting more data than memory by producing sorted runs and merging B−1 of them per pass.
More detail
The pass count is 1 + ⌈log_{B−1}⌈N/B⌉⌉, which is why 100 buffer pages sort 100 GB in two passes. It underlies ORDER BY, GROUP BY, merge join and index bulk-loading.
Grace hash join
Partitioning both inputs by the same hash so partition i of one can only join partition i of the other.
More detail
Costs 3(N+M) and needs B > √|R| for a single partitioning pass. Its failure case is skew, where one partition still does not fit and recursive partitioning is required.
Hybrid hash join
Grace hash join that keeps the first partition in memory instead of writing it out.
More detail
Saves that partition’s I/O entirely, so it degrades gracefully from an in-memory hash join to a fully partitioned one as memory shrinks.
Decomposable aggregate
An aggregate computable in parts and combined, such as SUM, COUNT, MIN and MAX.
More detail
The property that lets parallel and partitioned aggregation reduce each piece to a small state early. Holistic aggregates such as MEDIAN and COUNT DISTINCT have no bounded exact partial state — they can still be partitioned exactly, but not reduced early — which is why mergeable sketches such as HyperLogLog exist.
Morsel-driven parallelism
Dispatching small work units dynamically to whichever worker is free.
More detail
Absorbs uneven costs of independent work at runtime instead of fixing each worker’s share in advance. Worker count and work assignment are separate decisions: a fixed team, even PostgreSQL’s worker processes, can claim scan ranges dynamically. It cannot split indivisible work such as one hot join key, which needs the operator itself to decompose the work.
Query compilation
Translating a plan into machine code whose loops keep a tuple in registers across operators.
More detail
Neumann’s produce/consume model makes the pipeline rather than the operator the unit of code generation. Whether it beats vectorisation is genuinely open: it must repay its compile latency over the rows that remain, and engines mix the two — ClickHouse vectorises and JIT-compiles expressions, PostgreSQL JIT-compiles only expressions and tuple deforming.
Decorrelation
Rewriting a correlated subquery into a join so it executes once rather than once per outer row.
More detail
Changes the complexity class rather than the constant. The general algorithm pushes a dependent join down until nothing depends on the outer row; the COUNT bug is the trap, since naive unnesting turns "zero matching rows" into "no row at all".
Equi-depth histogram
A histogram whose buckets hold equal numbers of rows rather than equal value ranges.
More detail
Bounds estimation error on skewed data, which equi-width does not: a bucket covering a heavily populated range is narrow, so the uniformity assumption inside it costs less.
Independence assumption
Estimating a conjunction’s selectivity as the product of its conjuncts’.
More detail
Wrong whenever predicates correlate, and the canonical case is a city and its state, where the product underestimates by orders of magnitude. No engine has solved it; every engine’s answer is to let you declare the correlation.
Interesting order
A sort order that makes a more expensive sub-plan worth keeping because a later operator needs it.
More detail
The subtlety that makes Selinger’s dynamic programming correct: the search must keep one best plan per sort order, not one overall. An optimiser that forgets it systematically underuses merge joins.
Parameter sniffing
Using the parameter values visible during compilation or recompilation to choose a plan that is then reused.
More detail
Reusing that strategy for other values can cause severe slowdowns on skewed data, because the value that triggered compilation need not represent later executions — the same prepared statement is fast for one value and catastrophic for another, with nothing in the query to indicate which.
Recoverable schedule
A schedule where a transaction commits only after every transaction it read from has committed.
More detail
An axis orthogonal to serializability: a perfectly serializable schedule can be impossible to roll back. Strict ⊂ cascadeless ⊂ recoverable, and this chain is why strict two-phase locking exists.
Cascadeless schedule
A schedule where no transaction reads uncommitted data, so one abort never forces another.
More detail
Stronger than recoverable. Without it, aborting one transaction can cascade into aborting every transaction that read from it, which is unbounded work at exactly the wrong moment.
Strict two-phase locking
Two-phase locking that holds write locks until commit.
More detail
The two-phase rule alone gives serializability; holding to commit is bought separately, for recoverability and to make undo a simple before-image restore. Naming the variant matters — "2PL" without one says almost nothing.
Rigorous two-phase locking
Strict 2PL that also holds read locks to commit, making commit order the serialization order.
More detail
The extra property distributed commit protocols depend on, since it lets a coordinator use commit order as a global serialization order without further reasoning.
Intention lock
A mode announced at every ancestor of the item actually being locked.
More detail
IS and IX on the way down let a table-level request detect a conflict with a row-level holder without inspecting a million rows. SIX covers the scan-and-modify case that S plus IX would otherwise require.
Lock escalation
Replacing many fine-grained locks with one coarse lock when the count crosses a threshold.
More detail
Bounds lock-table memory and converts fine-grained contention into table-level blocking at an unpredictable moment. The alternative — refusing to escalate — fails the transaction outright when the table fills, which is at least a clear error.
Predicate lock
A lock on a condition rather than on rows, which is the correct answer to the phantom problem.
More detail
Unimplementable in general, because deciding whether two predicates intersect is not computable. Every engine approximates it with the index instead.
Next-key lock
An index-range lock covering an entry and the gap before it, so no key can be inserted into that gap.
More detail
The practical approximation of a predicate lock. It produces false conflicts because the gap is coarser than the predicate, and it is why the same application deadlocks on REPEATABLE READ and not on READ COMMITTED under InnoDB.
Thomas write rule
Discard an obsolete write rather than aborting the transaction that issued it.
More detail
A write older than the item’s write timestamp would be overwritten immediately anyway, so ignoring it preserves the timestamp order while aborting fewer transactions.
Optimistic concurrency control
Run without locks, then validate the read and write sets at commit and abort on conflict.
More detail
Its throughput curve falls rather than flattens beyond the contention crossover, because work is done before it is discovered to be wasted. "Optimistic is modern, locking is legacy" is marketing: the two curves cross.
Wound-wait
Deadlock prevention by timestamp: an older transaction pre-empts a younger holder rather than waiting.
More detail
Deadlock-free because waits always run one way along the timestamp order, at the cost of aborting transactions that were never actually deadlocked.
Version storage
Where MVCC keeps old row versions: append-only, time-travel, or delta.
More detail
One of four independent axes in the MVCC design space. Append-only retains full versions in the main table; delta storage retains inverse changes separately. Full images avoid historical reconstruction, not the walk to find them; deltas save space for narrow updates but require reconstruction. Both need garbage collection.
Write skew
Two transactions each reading what the other writes, producing a state no serial order allows.
More detail
The anomaly that defines snapshot isolation’s boundary: SI prevents dirty reads and changes to repeated snapshot reads, and rejects overlapping writers of the same row, yet permits this nonserializable history because the two write sets are disjoint.
Serializable snapshot isolation
Snapshot isolation plus detection of the read-write dependency pattern that makes a schedule non-serializable.
More detail
Tracks concurrent read–write antidependencies with SIREAD markers, which block no one, and aborts a transaction when two consecutive rw edges form a dangerous structure. The pattern is necessary for an SI cycle, not proof of one, so it accepts false positives; PostgreSQL narrows them with commit-order and read-only refinements.
Steal policy
Permitting a dirty page of an uncommitted transaction to be written to disk.
More detail
Necessary the moment a transaction can exceed the buffer pool, and it is precisely what creates the obligation to keep undo information.
No-force policy
Not requiring a transaction’s pages to reach disk before its commit is acknowledged.
More detail
Turns commit from a random-write storm into one sequential log write, and creates the obligation to keep redo information. Steal plus no-force is what every durable engine ships, and the log is the debt those two incur.
pageLSN
The log sequence number of the most recent change applied to a page, stamped in the page itself.
More detail
What makes the write-ahead invariant checkable rather than hoped for, and what lets redo decide per page whether a logged change is already present.
Fuzzy checkpoint
A checkpoint that records the dirty page and active transaction tables without quiescing the system.
More detail
Dirty pages are flushed lazily afterwards, so no transaction is blocked. The recLSN of each dirty page, not the checkpoint record itself, is where redo actually starts.
Repeating history
ARIES’s decision to redo every logged change, including those of transactions it is about to roll back.
More detail
Looks wasteful and is the opposite: it reconstructs the exact crash state, so undo starts from a known point. Selective redo would need to know which changes to skip, which is exactly the information the crash destroyed.
Compensation log record
A redo-only record describing an undo action, chained past the record it compensates.
More detail
Its UndoNxtLSN is what makes recovery restartable: a crash during recovery never re-undoes completed work, so total work stays bounded no matter how many times the machine dies.
Nested top action
A structural change, such as a page split, that must survive the abort of the transaction that caused it.
More detail
The index is shared, so undoing a split because one transaction rolled back would corrupt it for everyone. The log records the action as complete in itself.
Physiological logging
Logging physically to a page and logically within it.
More detail
The resolution of the torn-page problem: after a partial write a page is in no known state, so a purely logical redo cannot be applied, while purely physical logging costs a full page image per change.
Doublewrite buffer
Writing every page twice — once to a scratch area, once in place — so a torn write is always recoverable.
More detail
InnoDB’s answer to torn pages, against PostgreSQL’s full-page images in the log. Both cost roughly double the write volume and place it differently.
Shadow paging
Writing modified pages to new locations and atomically swapping a root pointer.
More detail
Needs no log at all, which is why LMDB has none. It pays in fragmentation, write amplification and — because the root swap serialises — a single writer.
Instant recovery
Redoing a page on first access rather than in a full pass, so the system accepts queries in seconds.
More detail
Moves recovery work off the critical path without reducing it, trading steady-state overhead for availability immediately after a crash.
Anti-caching
Evicting cold tuples to disk at tuple rather than page granularity.
More detail
The buffer pool inverted: memory is the primary store and disk is the overflow, so the unit of eviction follows what the workload actually references rather than what the page happens to contain.
Deterministic execution
Ordering transactions first so every replica reaches the same outcome with no agreement protocol.
More detail
Removes distributed commit rather than accelerating it, at the price of needing the full read/write set in advance, forbidding non-deterministic functions, and requiring aborts themselves to be deterministic.
Flash translation layer
The firmware that maps logical blocks onto flash pages and erase blocks.
More detail
Why a random write still costs more than a sequential one on a device with no seek: the FTL relocates and erases, so the device has its own write amplification and its own garbage collector competing with the database’s.
Dictionary encoding
Replacing values by small integers drawn from a per-column dictionary.
More detail
Turns comparisons into integer compares and, when the dictionary is sorted, lets range predicates run on codes directly — absent constants short-circuit to empty before any data is read. The encoding stops paying once cardinality passes the point where the code is no narrower than the value.
Run-length encoding
Storing a run of equal values once as (value, count).
More detail
Aggregates answer from the run vector without touching the values — SUM over RLE is Σ value×count, so sorted data makes aggregates nearly free and compression nearly total at the same time.
Bit-packing
Packing small integers into exactly the number of bits the range needs.
More detail
A column of 0–7 fits in 3 bits each; patched variants choose a frame width and spill the exceptions, which is why the width is chosen per frame rather than per column. Combined with dictionary codes it is the default integer encoding in every columnar format.
FSST
Fast Static Symbol Table: per-column text compression replacing frequent byte strings with one-byte codes.
More detail
Compresses strings where dictionary encoding cannot — high-cardinality text — and still lets some predicates evaluate on compressed data, since an encoded byte string compares correctly for equality.
Zone map
Per-block min/max metadata consulted before reading the block.
More detail
The cheapest index that exists: proportional to block count, maintained on write for free, and conservative forever — a block whose range cannot match is skipped, one that can is read and re-checked. Worth everything on a sorted column and almost nothing on a random one, which is why it is usually the only index a column store keeps.
Space-filling curve
A linear order that keeps multi-dimensional neighbours close, such as Z-order or Hilbert.
More detail
Trades the perfect pruning of a compound key’s leading column for a geometric share of pruning across every column at once — useful when several predicate columns matter equally and none deserves the lead position.
Late materialisation
Carrying row positions instead of tuples and fetching column values only at the end.
More detail
A filter then reads only the columns it needs, at the cost of a gather whose price depends on where the survivors sit — clustered survivors touch few pages, scattered ones touch nearly every page of each column even when their positions are sorted, and unsorted positions can fetch the same page again and again.
Selection vector
A list of positions naming the rows that survive a predicate.
More detail
The currency of late-materialised execution: passed between operators instead of tuples, so each operator adds work to the positions that remain rather than the columns the query needs. Its ordering relative to storage decides the gather cost.
Row group
A horizontal slice of a columnar file, itself stored column-major.
More detail
The unit of skipping and of memory-bounded reads: statistics, indexes and dictionaries live per column chunk inside a row group, so a reader that can skip a group skips every column’s share of it.
Position delete
A delete file naming the exact files and row positions to exclude.
More detail
Merge-on-read’s surgical form: a delete applies at read time without rewriting the data file, at the cost of carrying delete state to every reader. Equality deletes do the same by key where positions are unknown; sequence numbers decide which delete applies to which write.
Deletion vector
A per-file bitmap of deleted rows consulted alongside the data.
More detail
The table-format answer to in-place delete: the file stays immutable, the bitmap marks what is gone, and compaction later reclaims the space — the same trick a column store’s delete mask plays, raised to the level of a shared table format.
Merge-on-read
Writing deletes and updates as side data and letting readers apply them.
More detail
Pays nothing at write time and charges every read — the mirror of copy-on-write, which pays the rewrite immediately so readers stay dumb. Which side of the line an engine picks is fixed by whether its writes or its reads are the scarce resource.
database management system
A database management system (DBMS) is a program that stands between applications and their stored data and takes over five jobs: it keeps one description of the data that every application shares; it coordinates programs that change the data at the same time; it makes finished changes survive a crash; it finds records without reading everything; and it refuses changes that break the rules declared for the data.
Go to Why a database: what goes wrong when a program keeps its data in files →database
A database management system (DBMS) is a program that stands between applications and their stored data and takes over five jobs: it keeps one description of the data that every application shares; it coordinates programs that change the data at the same time; it makes finished changes survive a crash; it finds records without reading everything; and it refuses changes that break the rules declared for the data.
Go to Why a database: what goes wrong when a program keeps its data in files →schema
A schema is the description of a database: the names of its tables, the columns of each with their types, and the rules the data must obey.
Go to Schemas, instances and data independence: describe the data once →instance
An instance is the data that is in the database at one moment: the rows that fill those tables right now.
Go to Schemas, instances and data independence: describe the data once →Data independence
Data independence is the ability to change the description of the data at one level without having to change the description at the level above it, nor the programs written against that level.
Go to Schemas, instances and data independence: describe the data once →catalog
The catalog is the DBMS's own collection of tables that record the schema: every table, column, type, index, view and rule in the database, which the engine reads on every query to know what the names in it mean.
Go to Schemas, instances and data independence: describe the data once →page
We count its choice in page reads, the unit that dominates cost when data lives on disk.
Go to Inside a database engine: the life of one query and one update →query plan
A query plan is a tree of operators.
Go to Inside a database engine: the life of one query and one update →relation
A relation schema is a finite set of attributes, each a name with a domain, written .
Go to Relations, keys and identity: what a table is in the model →superkey
A set of attributes is a superkey of when, in every legal instance, implies for any two tuples .
Go to Relations, keys and identity: what a table is in the model →candidate key
A candidate key is a superkey with nothing to spare: no proper subset of it is still a superkey.
Go to Relations, keys and identity: what a table is in the model →foreign key
Let be a candidate key of a relation , and let be a set of attributes of a relation with the same domains as , taken in pairs. is a foreign key of referencing when, in every legal instance, every -value found in is a -value found in : and may be the same relation.
Go to Relations, keys and identity: what a table is in the model →integrity constraint
An integrity constraint is a rule about the contents of a database, written in its schema, that every legal state must satisfy.
Go to Integrity constraints: rules the database refuses to break →Referential integrity
Referential integrity is the property that every reference in the database has a target: every non-empty value in a foreign key column appears as a key value in the table it references.
Go to Integrity constraints: rules the database refuses to break →referential action
The choice is a referential action: what the database does to the referencing rows when a referenced row is deleted (ON DELETE) or its key changed (ON UPDATE).
Go to Integrity constraints: rules the database refuses to break →deferrable constraint
A deferrable constraint is one declared with DEFERRABLE, so that its check may be postponed from the end of each statement to the end of the transaction.
Go to Integrity constraints: rules the database refuses to break →Closed operator
An operator is closed over relations when, given relations as inputs, it always returns a relation.
Go to The algebra is the optimiser's rulebook →join
The join of and on a predicate (the theta-join) is : all the pairs, then keep those that satisfy .
Go to The algebra is the optimiser's rulebook →semijoin
The semijoin keeps the tuples of that have at least one partner in , with 's attributes only.
Go to The algebra is the optimiser's rulebook →antijoin
The antijoin keeps the tuples of that have none.
Go to The algebra is the optimiser's rulebook →division
With over attributes and over (where and share no names), the division is the set of -values that appear in paired with every tuple of : Read as "for every tuple of ".
Go to The algebra is the optimiser's rulebook →relational calculus
The relational calculus (here, the tuple calculus) is a query language in which a query is a formula of first-order logic over tuple variables that range over stored relations, and its answer is the set of tuples built from the rows that make the formula true.
Go to Relational calculus: saying what you want, not how to get it →safe
A calculus query is safe when, on every database instance, its answer is built only from values that are stored in the database or written in the query.
Go to Relational calculus: saying what you want, not how to get it →relationally complete
A query language is relationally complete when it can express every query the relational algebra can.
Go to Relational calculus: saying what you want, not how to get it →query block
A query block is one SELECT … FROM … WHERE …: a list of output expressions, a list of tables, and a condition.
Go to SQL: SELECT, FROM and WHERE are the algebra →tuple variable
A tuple variable (SQL calls it a correlation name or table alias) is a name in the FROM list that stands for one row of a table at a time.
Go to SQL: SELECT, FROM and WHERE are the algebra →bag
A bag (also called a multiset) over a schema is a function that gives each possible tuple a multiplicity : the number of times occurs.
Go to SQL works on bags, not sets: the identities duplicates break →duplicate elimination
In bag semantics that is an operator, duplicate elimination, written in the algebra and DISTINCT in SQL, and UNION is applied to UNION ALL.
Go to SQL works on bags, not sets: the identities duplicates break →outer join
An outer join returns every pair that an inner join with the same rule returns, plus the rows of at least one input that found no partner, each extended with NULL in the columns of the other input.
Go to Joins in SQL: inner, outer, semi and anti →self-join
A self-join is an ordinary join whose two inputs happen to be the same table.
Go to Joins in SQL: inner, outer, semi and anti →NULL
NULL is SQL's marker for a missing value.
Go to The semantics that silently returns the wrong answer →aggregate function
An aggregate function takes a bag of values, the values one column has in the rows it is given, and returns a single value that summarises them.
Go to Grouping and aggregation: GROUP BY, HAVING and the order clauses run in →grouping
Grouping with aggregation is the operator on a relation .
Go to Grouping and aggregation: GROUP BY, HAVING and the order clauses run in →subquery
A subquery is a SELECT written inside another statement, in parentheses, wherever the language expects a value, a list of values or a table.
Go to Subqueries and set operations: a query inside a query →correlated subquery
A correlated subquery is a subquery that refers to a column of the outer query.
Go to Subqueries and set operations: a query inside a query →view
A view is a query stored in the database's catalog under a name, which other queries can use wherever they could use a table.
Go to Views and common table expressions: naming a query →common table expression
A common table expression (CTE) is a named query written in a WITH clause at the front of a statement.
Go to Views and common table expressions: naming a query →materialized view
A materialized view is a view whose result is computed and stored as a table-like object.
Go to Views and common table expressions: naming a query →window function
A window function computes a value for each row of a query's intermediate result from a set of rows related to that row, called its window, and attaches the value to that row without removing any rows.
Go to Window functions: aggregates that keep every row →window frame
The window frame of a row is the contiguous stretch of its ordered partition that the window function reads for that row, given by a start and an end bound relative to the current row.
Go to Window functions: aggregates that keep every row →peers
Two rows of a partition are peers when the window's ORDER BY cannot tell them apart, that is, when they are equal on every ORDER BY expression.
Go to Window functions: aggregates that keep every row →recursive common table expression
A recursive common table expression is a WITH query whose body is a base query combined by UNION [ALL] with a recursive part that refers to the CTE's own name.
Go to Recursive queries: WITH RECURSIVE, and why it terminates →working table
The rows a round reads are held in the working table.
Go to Recursive queries: WITH RECURSIVE, and why it terminates →fixpoint
For an operator on sets, a fixpoint is a set with , and the least fixpoint is the one contained in every other.
Go to What the algebra cannot say: recursion and expressive power →trigger
A trigger is a named piece of code attached to a table (or a view) that the engine runs automatically when a stated change happens to it: an INSERT, UPDATE or DELETE.
Go to Triggers and stored procedures: code that runs inside the database →entity set
An entity set is a collection of entities of the same kind, all described by the same attributes.
Go to The entity–relationship model: drawing the domain before the tables →relationship set
A relationship set among entity sets is a mathematical relation among them: a set of tuples with each an entity of .
Go to The entity–relationship model: drawing the domain before the tables →cardinality constraint
For an entity set in a relationship set , a cardinality constraint is a pair (min, max): the fewest and the most relationships in that one entity of may take part in.
Go to The entity–relationship model: drawing the domain before the tables →weak entity set
A weak entity set is an entity set without a key of its own.
Go to The entity–relationship model: drawing the domain before the tables →specialisation
A specialisation relates a superclass entity set to one or more subclasses, each a subset of it: every card payment is a payment.
Go to The entity–relationship model: drawing the domain before the tables →decomposition
A decomposition of a relation schema is a set of schemas whose union is : .
Go to The two criteria, and why they conflict →lossless-join decomposition
A decomposition of is a lossless-join decomposition with respect to a set of dependencies when every legal instance of is recovered by rejoining its projections:
Go to The two criteria, and why they conflict →preserves dependencies
A decomposition preserves dependencies when the rules declared in the pieces taken together are as strong as the original rules:
Go to The two criteria, and why they conflict →prime attribute
An attribute is prime when it belongs to at least one candidate key and non-prime otherwise.
Go to Normal forms: 1NF to BCNF, and the anomalies each removes →third normal form
A relation with dependency set is in third normal form (3NF) when, for every non-trivial in , either is a superkey of or is a prime attribute.
Go to Normal forms: 1NF to BCNF, and the anomalies each removes →Boyce–Codd normal form
A relation with dependency set is in Boyce–Codd normal form (BCNF) when, for every non-trivial in , is a superkey of .
Go to Normal forms: 1NF to BCNF, and the anomalies each removes →multi-valued dependency
The multi-valued dependency holds on when, in every legal instance, rows agreeing on can be recombined as above.
Go to Redundancy that no functional dependency explains →fourth normal form
A relation is in fourth normal form (4NF) when every non-trivial multi-valued dependency that holds on it has a superkey as its left side.
Go to Redundancy that no functional dependency explains →storage engine
A storage engine is the part of a database that keeps records on a persistent device and finds them again.
Go to Why storage engines exist: seeks vs sequential I/O →sequential
A page access is sequential when it reads the page that follows the previous access in the file, so the device is already positioned and pays only the streaming time.
Go to Why storage engines exist: seeks vs sequential I/O →random
Otherwise it is random and pays the positioning cost again.
Go to Why storage engines exist: seeks vs sequential I/O →page I/O
A page I/O is one read of a page from storage into memory, or one write of a page from memory to storage.
Go to The cost table everything else is quoted against →file organisation
A file organisation is the rule that decides which page of a file each record goes on, and so what the file promises about where a record can be found.
Go to The cost table everything else is quoted against →search key
Records are found by a search key, the attribute or attributes the file is organised on and queried by.
Go to The cost table everything else is quoted against →record id
A record id (RID) is the address of a record: the pair (page number, slot number).
Go to Inside one page →buffer pool
A buffer pool is a fixed array of page-sized frames in memory, together with a buffer table from page id to frame and some state for every frame.
Go to The policy, and the mmap argument →pin
To pin a frame is to add one to its pin count, promising that the page will stay in this frame until the pin is released; a frame with a positive pin count cannot be chosen as a victim.
Go to The policy, and the mmap argument →dirty
A frame is dirty when its page has been modified in memory and the disk copy is out of date.
Go to The policy, and the mmap argument →OLTP
Online transaction processing (OLTP) is the workload of an application serving its users: a high rate of short reads and writes, each touching a few rows found by key, with millisecond latency targets.
Go to OLTP vs OLAP →OLAP
Online analytical processing (OLAP) is the workload of analysis and reporting: a low rate of long queries, each reading a large fraction of the rows but only a few of the columns, and reducing them to a small result.
Go to OLTP vs OLAP →column-oriented storage
In column-oriented storage (a column store) each column of a table is stored contiguously, apart from the others, and a row is the set of values at one position across all the columns.
Go to Column-oriented storage & compression →Column store
In column-oriented storage (a column store) each column of a table is stored contiguously, apart from the others, and a row is the set of values at one position across all the columns.
Go to Column-oriented storage & compression →index
An index is a redundant structure, stored apart from a table's rows, that maps values of a search key to the locations of the rows holding them, so that a query can find those rows without reading the whole table.
Go to What an index has to answer →RUM conjecture
The RUM conjecture: an access method that can set an upper bound for two of the read, update and memory overheads also sets a lower bound for the third.
Go to What an index has to answer →B-tree
A B-tree of fanout is a search tree whose nodes are disk pages, with these properties. (1) A node holding keys, in sorted order, has children, and the keys separate the key ranges of the children. (2) A node holds at most keys, so at most children. (3) Every node except the root holds at least keys, so at least children; the root…
Go to B-trees: pages, fanout and height →B+tree
A B+tree of order , where is the largest number of children an interior page may have (a whole number), is a rooted tree of pages that satisfies four clauses.
Go to The invariant, the fanout, the height →separator keys
An interior page with children holds separator keys , and child holds exactly the keys with , taking and .
Go to The invariant, the fanout, the height →Heap (table)
Either the rows sit in no order in a heap, wherever a page had room, or the rows sit inside one of the table's own indexes, in that index's key order.
Go to Indexes: clustered, secondary & covering →clustered index
A clustered index is the table itself, organised as an index: its leaves hold whole rows in key order, so a table has at most one.
Go to Indexes: clustered, secondary & covering →secondary index
A secondary index is a separate structure whose leaf entries hold the indexed columns plus a row locator.
Go to Indexes: clustered, secondary & covering →leftmost-prefix rule
This is the leftmost-prefix rule: a composite index can seek on any leading run of its columns, because its entries are grouped by the first column and then, within it, by the second.
Go to Indexes: clustered, secondary & covering →underflows
A page underflows when it holds fewer than the minimum the invariant allows.
Go to How the tree shrinks, and why it usually does not →Free-at-empty
Free-at-empty is the policy of freeing a page only when it holds no entries at all, and leaving any fuller page alone however sparse it is.
Go to How the tree shrinks, and why it usually does not →order-preserving encoding
An order-preserving encoding maps each key value, or tuple of values, to a byte string such that where is memcmp order: byte by byte as unsigned numbers, with a string that is a prefix of another sorting first.
Go to Keys decide the fanout →posting list
Deduplication replaces a run of equal keys with one entry holding the key and a list of row pointers, called a posting list.
Go to Keys decide the fanout →latch
A latch is a short-lived lock on one in-memory structure, here one page in the buffer pool, taken by a thread for the length of a single operation.
Go to Descending a tree other threads are changing →global depth
The number is the global depth.
Go to Constant-time lookup, and the cost of growth →local depth
Each bucket records its local depth , the number of leading bits that all of its records share, so always.
Go to Constant-time lookup, and the cost of growth →split pointer
A split pointer names the next bucket to split.
Go to Constant-time lookup, and the cost of growth →R-tree
An R-tree is a balanced tree of pages.
Go to When one ordering is not enough →bitmap index
A bitmap index on a column keeps, for each distinct value of the column (and for NULL), a bitmap with one bit per row of the table, set where the row holds that value.
Go to When one ordering is not enough →hypothetical index
A hypothetical index is an index described to the cost estimator, with its columns and estimated statistics, but never built.
Go to Choosing indexes from a workload →Index interaction
Two indexes interact when the benefit of one depends on whether the other is present.
Go to Choosing indexes from a workload →Greedy (m, k)
Greedy (m, k) picks up to indexes from the candidates.
Go to Choosing indexes from a workload →Denormalisation
Denormalisation is the deliberate storing of a fact in more than one place, or of a value that could be computed from others, to make reads cheaper, accepting that the copies can now disagree.
Go to Where the logical schema meets the disk →access path
An access path is one way of reaching the rows of a table: reading the whole table in order, or going through one of its indexes.
Go to Selections and access paths: scan, or use the index? →bitmap scan
A bitmap scan is an access path that first collects the row addresses an index (or several) selects, then reads each table page that holds a match exactly once, in file order, and checks the rows on it.
Go to Selections and access paths: scan, or use the index? →sorted run
A sorted run is a stretch of a file, written to disk, whose records are already in key order.
Go to Sorting more than fits →block nested-loop join
A block nested-loop join reads the outer relation in blocks of pages, holds each block in memory, and scans the whole inner relation once per block, so the inner is read times and not once per outer page or record.
Go to The three ways, and where each wins →sort-merge join
A sort-merge join sorts both inputs on the join key, then walks them side by side with one cursor each, advancing whichever cursor holds the smaller key, and pairs up the groups of equal keys it meets.
Go to The three ways, and where each wins →hash join
A hash join builds an in-memory hash table on the join key of one input (the build side), then streams the other input (the probe side) past it, looking each record's key up.
Go to The three ways, and where each wins →Hash aggregation
Hash aggregation computes a grouped aggregate by keeping a hash table with one entry per distinct grouping key, each holding that group's aggregate state, and updating the entry for every input row.
Go to Grouping, distinctness, and counting approximately →Data-centric code generation
Data-centric code generation translates a query plan into code organised around the data rather than the operators: each pipeline of the plan, from a scan (or a pipeline breaker's output) to the next pipeline breaker, becomes one loop in which a row's values stay in processor registers from the first operator to the last.
Go to Removing the interpreter →exchange operator
The exchange operator is an operator that, to the plan around it, looks like any other iterator, but inside connects a group of producer workers running the plan below it to a group of consumer workers running the plan above, passing rows through queues and deciding which consumer each row goes to.
Go to Using more than one core →morsels
In morsel-driven parallelism the input of a pipeline is cut into small, bounded pieces called morsels, and a fixed pool of workers repeatedly takes the next unclaimed morsel, runs it through the whole pipeline up to the next pipeline breaker, and comes back for another.
Go to Using more than one core →logical plan
A logical plan is a tree of relational-algebra operators (, , , , and the rest): relations in, a relation out, and nothing about how the work is done.
Go to From text to plan →physical plan
A physical plan is a tree of the executable operators the engine actually has (sequential scan, index scan, hash join, sort-merge join, hash aggregate, sort), each implementing one logical operator.
Go to From text to plan →null-rejecting
A predicate is null-rejecting, also called strict, when it can never be true if a column it mentions is NULL.
Go to The transformations that are always right →dependent join
For a relation and an expression that may mention attributes of , the dependent join is where is evaluated with its outside names bound to the row , and is the two rows glued together.
Go to The transformations that are always right →count bug
The count bug is the defect in an unnesting rewrite that turns a scalar aggregate (a total function: one answer for any input, including the empty one) into a grouped aggregate (a partial function: one answer per key that occurs) without restoring the answer on the keys that vanished.
Go to The transformations that are always right →cardinality
The cardinality of an operator's output is the number of rows it produces.
Go to The number everything else multiplies →selectivity
The selectivity of a predicate is the fraction of its input's rows that it keeps, a number in , so the estimated number of matching rows is .
Go to The number everything else multiplies →histogram
A histogram over a column is a list of buckets, each a range of values with the count of rows that fall in it.
Go to The number everything else multiplies →cost model
A cost model is a rule that maps a physical plan, together with the estimated row counts at each of its operators, to one number.
Go to Turning row counts into a number →startup cost
A plan's startup cost is the work done before its first row can be delivered; its total cost is the work to deliver every row.
Go to Turning row counts into a number →total cost
A plan's startup cost is the work done before its first row can be delivered; its total cost is the work to deliver every row.
Go to Turning row counts into a number →left-deep tree
In a left-deep tree the right input of every join is a single base table, so the tree is one spine: (((A ⋈ B) ⋈ C) ⋈ D).
Go to Choosing among astronomically many plans →bushy tree
In a bushy tree both inputs of a join may themselves be joins, as in (A ⋈ B) ⋈ (C ⋈ D).
Go to Choosing among astronomically many plans →prepared statement
A prepared statement is a query registered with the engine once, with a placeholder ($1 in PostgreSQL, ? in MySQL and SQLite) for each value to be supplied later.
Go to When the estimate was wrong →custom plan
A custom plan is planned using the actual parameter values of this execution, so the optimiser can read the values against its statistics.
Go to When the estimate was wrong →generic plan
A generic plan is planned without looking at the values, and can be reused for any of them.
Go to When the estimate was wrong →transaction
A transaction is a group of reads and writes that the database treats as one unit of work.
Go to What a transaction guarantees (ACID) →history
A history (some books say schedule) is the operations of several transactions written in the single order in which the database executed them. means transaction read item ; means it wrote ; and mean it committed and aborted.
Go to The isolation anomalies →isolation anomaly
An isolation anomaly is a pattern in a history that lets one transaction be affected by another in a way no run of the transactions one at a time could produce.
Go to The isolation anomalies →isolation level
An isolation level is a named setting, chosen per transaction, that states which anomalies the database will rule out for it.
Go to Isolation levels →schedule
A schedule (also called a history) of a set of transactions is a single sequence containing all of their steps, in which each transaction's own steps keep their own order.
Go to Serializability: when an interleaving is as good as running one at a time →serializable
A schedule is serializable when its effect, meaning every value each transaction reads and the database it leaves behind, is the same as that of some serial schedule of the same transactions.
Go to Serializability: when an interleaving is as good as running one at a time →conflict serializable
A schedule is conflict serializable when it is conflict equivalent to some serial schedule.
Go to Serializability: when an interleaving is as good as running one at a time →precedence graph
The precedence graph of a schedule has one node per transaction.
Go to Serializability: when an interleaving is as good as running one at a time →view serializable
A schedule is view serializable when it is view equivalent to some serial schedule.
Go to Serializability: when an interleaving is as good as running one at a time →recoverable
A schedule is recoverable if no transaction commits before every transaction it read from has committed.
Go to Recoverability: serializability is not enough →cascadeless
It is cascadeless (also written ACA, "avoids cascading aborts") if every read is of already committed data.
Go to Recoverability: serializability is not enough →strict
It is strict if no transaction reads or overwrites an item until its last writer has committed or aborted.
Go to Recoverability: serializability is not enough →shared lock
A shared lock (S) lets its holder read an item, and any number of transactions may hold one on the same item at once.
Go to What each phase actually buys →exclusive lock
An exclusive lock (X) lets its holder read and write it, and no other lock of either kind may be held on that item at the same time.
Go to What each phase actually buys →two-phase locking
Under two-phase locking (2PL) a transaction takes a lock on an item before it touches it, and its life has two phases.
Go to What each phase actually buys →lock table
The first is a lock table: a hash table, chained as in hash tables with chaining, keyed by an identifier for the lockable object.
Go to The lock table, and locking a million rows at once →wait-die
When transaction requests a lock that holds, wait-die lets wait if it is older than and otherwise makes die: abort and restart with its original timestamp.
Go to The price of waiting while holding →gap lock
A gap lock covers the open interval between two adjacent index entries, so that no entry can be inserted anywhere inside it.
Go to Locking rows that do not exist yet →insert-intention lock
The fourth is the lock an insert takes on a gap, and it is an insert-intention lock: a gap lock requested only to announce "I am about to put an entry in here", so that it waits for any lock that protects the gap.
Go to Locking rows that do not exist yet →Timestamp ordering
Timestamp ordering (T/O) is a concurrency-control protocol that gives every transaction a unique timestamp when it begins and lets an operation run only if it agrees with timestamp order: of any two conflicting operations, the one from the transaction with the smaller timestamp must run first.
Go to Timestamp ordering and optimistic validation: concurrency control without locks →validation
Optimistic concurrency control (OCC) runs each transaction in three phases: a read phase, in which it reads the database and writes only into a private workspace; a validation phase, in which the engine checks whether the transaction conflicted with others; and a write phase, in which, if validation passed, the workspace is installed in the database.
Go to Timestamp ordering and optimistic validation: concurrency control without locks →MVCC
Multiversion concurrency control (MVCC) keeps several versions of each row instead of overwriting it.
Go to MVCC & snapshot isolation →snapshot
A snapshot is a reader's fixed picture of the database: the commit stamp of the newest commit it is allowed to see.
Go to MVCC & snapshot isolation →Snapshot isolation
Snapshot isolation is the isolation level in which every read of a transaction is answered from a snapshot of the data committed before the transaction began, and a transaction may commit only if no concurrent transaction has already committed a write to an item it also wrote, the first-committer-wins rule.
Go to MVCC & snapshot isolation →Garbage collection
Garbage collection of versions is the background work of finding versions that no snapshot, present or future, can see, and reusing their space.
Go to MVCC & snapshot isolation →version chain
A version chain links the versions of one row with a pointer in each version.
Go to Four decisions, not one design →collection horizon
This lesson calls the collection horizon, and writes it .
Go to Four decisions, not one design →rw-antidependency
Transaction has an rw-antidependency on transaction , written , when reads a version of an item and creates a later version of the same item.
Go to Serializable snapshot isolation: detecting the dangerous structure instead of locking →dangerous structure
Two consecutive rw-antidependencies of this shape are called a dangerous structure, and the transaction in the middle is its pivot.
Go to Serializable snapshot isolation: detecting the dangerous structure instead of locking →steal
A steal policy says yes: when the pool needs a frame, the replacement policy picks a victim without asking who dirtied it.
Go to Steal, force, and the two obligations →force
A force policy says yes: commit writes them all and waits.
Go to Steal, force, and the two obligations →Undo
Undo is the obligation created by steal: remove from the disk any change of a transaction that did not commit, which needs the information to reverse it.
Go to Steal, force, and the two obligations →Redo
Redo is the obligation created by no-force: put on the disk any change of a transaction that did commit and never got there, which needs the information to reapply it.
Go to Steal, force, and the two obligations →write-ahead log
A write-ahead log is an append-only sequence of records describing every change, kept on stable storage and written ahead of the change it describes: before a changed page overwrites the old version on disk, the log records for that change are already durable, and before a transaction's commit is acknowledged, all its log records, the commit record included, are durable.
Go to What the log must guarantee →log sequence number
A log sequence number (LSN) is a monotonically increasing address into the log.
Go to What the log must guarantee →Group commit
Group commit lets many transactions that commit at about the same time share one log flush: each appends its commit record, one of them issues the flush, and all of them are acknowledged when it completes.
Go to What the log must guarantee →checkpoint
A checkpoint is a record written into the log that lets recovery start later than the beginning of the log.
Go to Bounding how far back recovery must read →dirty page table
The dirty page table (DPT) has one entry per page that is dirty in the buffer pool, mapping its page id to its recLSN (recovery LSN): an LSN at or before the first log record that dirtied the page since it was last written to disk.
Go to Bounding how far back recovery must read →recLSN
The dirty page table (DPT) has one entry per page that is dirty in the buffer pool, mapping its page id to its recLSN (recovery LSN): an LSN at or before the first log record that dirtied the page since it was last written to disk.
Go to Bounding how far back recovery must read →ARIES
ARIES, Algorithm for Recovery and Isolation Exploiting Semantics, is the recovery method published by C.
Go to Analysis, redo, undo →torn page
A torn page is a page left half written by a failure during its write: some of its device-sized pieces carry the new version, some the old, in an order nobody controls.
Go to Physical, logical, and not logging at all →full-page image
A full-page image is a log record that carries a page's entire contents.
Go to Physical, logical, and not logging at all →append-only log
An append-only log is a file to which records are only ever added at the end.
Go to The append-only log →tombstone
A tombstone is a record that says a key was deleted.
Go to The append-only log →Log-structured hash index
A log-structured hash index is a hash table held entirely in memory that maps every live key to the position of its newest record in an append-only log.
Go to Hash indexes over a log →memtable
A memtable is a sorted, in-memory map holding a storage engine's most recent writes, inserts, updates and tombstones alike, until it grows large enough to be written out to disk in key order as one file.
Go to The memtable →SSTable
An SSTable (Sorted String Table) is a file of key-value pairs stored in key order that is never modified after it is written.
Go to The SSTable: sorted and immutable →sparse index
A sparse index is an index with one entry per block of a sorted file rather than one per record.
Go to The SSTable: sorted and immutable →Compaction
Compaction is a background process that reads several SSTables, merges them, and writes one new SSTable holding only the records that survive.
Go to Compaction & tombstones →LSM-tree
A log-structured merge-tree (LSM-tree) is a storage structure that buffers writes in a sorted in-memory component (the memtable), protected by a write-ahead log; writes that component to disk as sorted, immutable files (SSTables); and merges those files in the background (compaction) so that a read consults a bounded number of them.
Go to Compaction & tombstones →Bloom filter
A Bloom filter is a fixed-size summary of a set of keys, a bit array plus hash functions, that answers "definitely not in the set" or "possibly in the set".
Go to Bloom filters →False positive (Bloom filter)
A false positive is a "possibly" for a key that was never added; its probability, taken over keys that are not in the set, is the filter's false-positive rate . (This course writes for it, not , which it spends on the Bε-tree later in this module.)
Go to Bloom filters →leveling
Under leveling, each level holds one sorted run.
Go to Leveling vs tiering →tiering
Under tiering, a level lets sorted runs accumulate untouched, and when it holds of them they are merged, once, into a single new run in the level below.
Go to Leveling vs tiering →Write amplification
Write amplification is the bytes an engine writes to the device divided by the bytes the application changed.
Go to Big picture: B-tree vs LSM and the three amplifications →Read amplification
Read amplification is the bytes (or blocks) the engine reads from the device divided by what the query needed.
Go to Big picture: B-tree vs LSM and the three amplifications →Space amplification
Space amplification is the bytes stored divided by the bytes of live data.
Go to Big picture: B-tree vs LSM and the three amplifications →Bε-tree
A Bε-tree is a B+tree-like search tree in which each interior node of slots gives of them to pivots and child pointers and the rest to a buffer of pending messages, with .
Go to Trading read cost for write cost, on purpose →copy-on-write B-tree
A copy-on-write B-tree never writes over a page that a committed state of the tree points at.
Go to Trading read cost for write cost, on purpose →main-memory database
A main-memory database keeps the primary copy of its data in DRAM and is built on that assumption: a row is reached by its address, not by a page and a slot, and the disk holds only a log and snapshots that exist to survive a restart, never to serve a read.
Go to When the buffer pool goes away →cache-conscious
A data structure or algorithm is cache-conscious when its layout is chosen from the cache's block size and capacity: nodes sized to a cache line, hot parts packed together, accesses ordered so that one fetched line is used fully before it is evicted. (One that stays efficient for every block size without being told any is called cache-oblivious; the last technique below is of that kind.)
Go to The memory hierarchy is the new disk →dependent load
A dependent load is a memory read whose address is produced by an earlier read, so it cannot even start until that earlier read has returned.
Go to The memory hierarchy is the new disk →contention footprint
Calvin's paper names this span a transaction's contention footprint: "the total duration that a transaction holds its locks", including any commit protocol.
Go to Removing the agreement problem →Over-provisioning
Over-provisioning is flash that the drive holds beyond the capacity it advertises, so that the collector always has room to work.
Go to The device stopped behaving like a disk →design point
A design point is a coherent set of storage, concurrency, recovery and execution choices that makes some operations cheap while keeping the database's promised behaviour.
Go to The whole course, read across six engines →row identity
What it holds is the row's row identity: an address in a heap, a primary key, a row number.
Go to The whole course, read across six engines →lightweight encoding
A lightweight encoding exploits one regularity of a column's values (runs, few distinct values, a narrow range, small steps) and decodes in a few instructions per value.
Go to Encodings chosen for the scan, not the disk →order-preserving dictionary
An order-preserving dictionary assigns codes in the sorted order of the values, so that exactly when .
Go to Encodings chosen for the scan, not the disk →frame-of-reference encoding
That is frame-of-reference (FOR) encoding: one base per block and a small offset per value.
Go to Encodings chosen for the scan, not the disk →operates directly on compressed data
An operator operates directly on compressed data when it evaluates its predicate or aggregate on the encoded form (codes, runs, packed words) and decodes only what its result needs.
Go to Executing on the encoding: the predicate never sees the value →Early materialisation
Early materialisation builds tuples as soon as, or before, their values are needed, and runs the rest of the plan on tuples.
Go to Late materialisation: when to stitch columns back into rows →position list
Late materialisation keeps columns separate and passes a position list (the positions that survived so far) from operator to operator, fetching each other column once, at the surviving positions, only when the plan needs its values, often at the very end.
Go to Late materialisation: when to stitch columns back into rows →zones
Divide a column's physical storage into zones — contiguous runs of rows that the engine already reads as a unit: a page, a row group, a file, a micro-partition, whatever the storage layer happens to be made of.
Go to Zone maps: an index of two numbers per block, and when it works →small materialised aggregate
For each zone and each column, the engine records a small materialised aggregate — Moerkotte's term for the idea — typically the minimum, the maximum, the row count and the null count, sometimes the sum.
Go to Zone maps: an index of two numbers per block, and when it works →Z-order
Z-order (Morton order) is the simplest: lay each column's value out in binary and take the bits in round-robin order, most significant first.
Go to One physical order for many predicates →Hilbert curve
The Hilbert curve visits the same cells in a different order:
Go to One physical order for many predicates →clustering depth
A table may declare a clustering key; the engine maintains the layout in the background, and exposes the state of it: SYSTEM$CLUSTERING_INFORMATION reports a clustering depth — how many micro-partitions overlap a typical value range of the key — so a depth near 1 is well-clustered and a depth approaching the partition count is insertion order by another name.
Go to One physical order for many predicates →row groups
Between them sit one or more row groups — the horizontal partitioning of the table's rows — and each row group holds exactly one column chunk per leaf column, contiguous in the file.
Go to Open columnar file formats: a storage layer with no engine attached →column chunk
Between them sit one or more row groups — the horizontal partitioning of the table's rows — and each row group holds exactly one column chunk per leaf column, contiguous in the file.
Go to Open columnar file formats: a storage layer with no engine attached →data page
A column chunk is one or more data pages, optionally preceded by a dictionary page.
Go to Open columnar file formats: a storage layer with no engine attached →dictionary page
A column chunk is one or more data pages, optionally preceded by a dictionary page.
Go to Open columnar file formats: a storage layer with no engine attached →file metadata
After the last row group comes the file metadata: a Thrift-serialized FileMetaData structure holding the schema and, for every column chunk in every row group, its file offset, size, encodings, compression and statistics; then a four-byte little-endian length of that metadata; then the closing PAR1.
Go to Open columnar file formats: a storage layer with no engine attached →definition level
The definition level counts how many of the optional and repeated fields on the column's path are actually defined at this position — it is what lets one NULL say "the parent group existed but this leaf was absent" versus "the parent group itself was absent".
Go to Open columnar file formats: a storage layer with no engine attached →repetition level
The repetition level names the innermost repeated field on the path at which a new list element began — 0 marks a new record, and each deeper level means "a new element in the list at this depth".
Go to Open columnar file formats: a storage layer with no engine attached →page index
The page index is two Thrift structures stored near the footer, with their locations kept in each column chunk's metadata so a reader doing a full scan never pays for them.
Go to Open columnar file formats: a storage layer with no engine attached →column index
The column index lists, per page of a chunk, the page's min, max, and null count — a zone map per page.
Go to Open columnar file formats: a storage layer with no engine attached →offset index
The offset index lists each page's file offset, compressed size, and first_row_index — the row number where it starts — so a reader can seek to a page directly and, once one column's rows are chosen, fast-forward every other retrieved column to the same rows.
Go to Open columnar file formats: a storage layer with no engine attached →split-block Bloom filter
The split-block Bloom filter answers "is value possibly in this chunk" for equality predicates where min–max says nothing and the dictionary was too big to write.
Go to Open columnar file formats: a storage layer with no engine attached →table format
A table format fixes this with one move: since the data files never change, the only mutable state is which set of files is the table — so make that set a versioned metadata object, and commit by atomically repointing it.
Go to Table formats: transactions over immutable files →table metadata
An Iceberg table's table metadata is a JSON document — the schema and its history, the partition spec, and the list of snapshots, each a complete description of the table's files at one moment.
Go to Table formats: transactions over immutable files →manifest list
A snapshot does not list files directly; it names a manifest list, an Avro file listing the snapshot's manifests with per-manifest summaries — partition-field ranges and added/existing/deleted file counts — that let a planner skip manifests without opening them.
Go to Table formats: transactions over immutable files →manifest
A snapshot does not list files directly; it names a manifest list, an Avro file listing the snapshot's manifests with per-manifest summaries — partition-field ranges and added/existing/deleted file counts — that let a planner skip manifests without opening them.
Go to Table formats: transactions over immutable files →partition transform
Iceberg separates the partition logic from the partition layout with partition transforms: the spec's list is identity, bucket[N], truncate[W], year, month, day, hour and void, each mapping a source column to a partition value.
Go to Table formats: transactions over immutable files →Copy-on-write
Copy-on-write (CoW): read the affected file, write a new file with the ten rows changed or removed, and commit the swap — the new file appears, the old one becomes a tombstone.
Go to Deleting a row from an immutable file →equality delete
An equality delete records column values — the spec's equality_ids columns — and deletes every row matching all of them as one conjunctive predicate; floating-point columns are excluded, since an equality that never holds is useless.
Go to Deleting a row from an immutable file →sequence number
Iceberg assigns every commit a sequence number, and the rules decide which deletes a data file must honour: a position delete applies to a data file in the same partition whose sequence number is at most the delete's — equal included, so a delete can cover files written in its own commit — while an equality delete applies to same-partition files with a strictly smaller sequence number.
Go to Deleting a row from an immutable file →Cost model
Every operator this course derives, priced in page I/Os, each linking the topic that derives it.
Every operator this course derives, priced in the same unit: page I/Os, in terms of the relation sizes and the buffer budget B. This is the table the optimiser in module 06 adds up, and it is the single artefact most worth keeping open while reading the rest of the course.
Each entry names the topic that derives it, so a cost you do not believe is one click from its derivation. Costs are stated for the algorithm, not for any engine’s implementation of it; where an engine deviates, that lesson’s comparison card says so.
Heap scan
Read every page of an unordered file.
More detail
The baseline every other access path is compared against, and the one cost in the table with no failure mode: it is the same whatever the predicate, the selectivity or the data distribution. Sequential, so each page costs c_seq rather than c_rand — which is why a scan reading ten times the pages can still beat an index doing random fetches.
Heap equality search
Find a matching record in an unordered file.
More detail
Half the file on average for a unique match, because the scan can stop; the whole file when the match is not unique or does not exist, since nothing proves absence until every page is read. The gap between expected and worst case is the reason this row exists — a heap file has no bad case for scanning and a bad case for searching.
Sorted-file equality search
Binary search a sorted file, then read the matches, which are contiguous.
More detail
The best search cost of the three primary organisations, and irrelevant in practice because of the next row: keeping the file sorted costs O(N) per insert. Sorted files survive as the output of a bulk load, not as a maintained organisation — which is exactly the problem the B+tree solves.
Sorted-file insert
Insert into a sorted file while keeping it sorted.
More detail
The cost that kills the sorted file as a primary organisation. Every insert shifts on average half the file, and each shifted page is read and written. This single cell is why module 04 exists.
Hashed-file equality search
Hash to a bucket and read it.
More detail
The only constant-time search in the table, and it answers exactly one question — no ranges, no prefixes, no ordering. The overflow term grows with load factor and is what extendible and linear hashing exist to bound.
B+tree point lookup
Descend from root to leaf.
More detail
The structural cost is the height h = ⌈log_F N_records⌉, three or four for a large table. The practical cost is about one I/O, because the top two levels are a few megabytes and therefore permanently resident. Quoting h as the I/O count is the commonest overestimate in access-path costing.
B+tree range scan
Descend once, then follow the leaf chain.
More detail
The leaf chaining is the reason every database chose B+trees over B-trees: after the descent the scan is sequential, so a range costs the same per row as a heap scan does. Only true when the index is the relation’s order or the query is answered from the index alone.
Unclustered index lookup
Descend, then fetch each matching record from wherever it lives.
More detail
The m term is page fetches, not records, under the worst case that scattered matches share no pages — and each is random rather than sequential. A scan gets p records per page read and this path gets one, so the crossover sits near 1/p of the relation — under 1% at 100 records per page — rather than at half of it. The single most useful number in the module; locality and caching move it.
Covering index lookup
Answer from the index without fetching from the relation.
More detail
Collapses the previous row’s m random fetches into a leaf-chain scan, a difference of orders of magnitude at moderate selectivity, and the reason included-column indexes exist. The descent already reads the first leaf, so the exact count is one less; engines that check visibility in the relation add those fetches back.
B+tree insert
Descend, write the leaf, and split upward when full.
More detail
Splits are rare in amortisation but not in the tail: a split writes the old page, the new page and the parent, and can propagate to the root. Under ascending keys the splits concentrate on one page, which is both a contention point and the reason ordered keys pack better.
Index maintenance per write
What each additional index costs on every insert, update and delete.
More detail
The term index selection trades against: a cold descent plus one leaf write, per entry a write changes. It is paid whether or not any query uses the index, which is why "index every predicate" loses — the read saving is bounded by the query rate and the write cost is not. An update touching no key or included column of an index may leave it untouched; splits, merges and log bytes are extra.
External merge sort
Produce sorted runs, then merge B−1 of them per pass.
More detail
The log base is B−1 rather than B because one frame buffers output. The practical consequence is that the pass count is almost always two: with 100 buffer pages, 100 GB sorts in two passes, so sorting is nearly linear in practice despite the logarithm.
Sort-based grouping
Sort on the grouping key, then aggregate adjacent runs of equal keys.
More detail
Produces grouping-key order for free, which is worth real money when an ORDER BY on the key follows or a merge join consumes it. If the sort’s final merge feeds grouping directly, the sorted-file write and re-read disappear and the cost is external sort − N — the same as a one-round hash spill, so spilling alone does not favour sorting; the ordered output does.
Hash-based grouping
Build a hash table keyed by the grouping attributes.
More detail
Cheaper than sorting whenever the distinct group count fits in memory, and unordered. Memory follows the number of groups, not rows. The spill case is a partitioning round, which is why its cost matches Grace hash join’s shape; partial or recursive spilling costs N plus two I/Os per spilled page per level.
Nested-loop join (naive)
For every record of the outer, scan the inner.
More detail
Listed so the reader can see what every other join algorithm is avoiding. The multiplier is a record count, not a page count, which is what makes it catastrophic rather than merely bad.
Block nested-loop join
The same algorithm with the buffer actually used: read the outer in blocks.
More detail
The same algorithm as the row above and orders of magnitude cheaper, purely from using memory. It wins outright when the outer relation fits in B−2 frames, where the cost collapses to N + M — one scan of each.
Index nested-loop join
For every outer record, probe an index on the inner join column.
More detail
The best join in the table when the outer is small and the inner index is selective, and the worst when the optimiser underestimates the outer’s cardinality — which is the single commonest catastrophic plan in production. The fetch term is 1 for a covering index and m for an unclustered one.
Sort-merge join
Sort both inputs on the join key, then merge in one pass.
More detail
Free when both inputs are already sorted — which is what makes interesting orders worth tracking in the optimiser. Its degenerate case is many duplicates on both sides, where the merge reintroduces a nested loop inside itself.
Grace hash join
Partition both inputs by the same hash, then join partition-wise.
More detail
Read, write, read again — hence the factor of three. The condition B > √|R| is the most useful inequality in the module: below it, partitions still do not fit and the algorithm recurses, and skew can break the condition for one partition even when the average holds.
Hybrid hash join
Grace hash join that keeps the first partition resident.
More detail
Degrades continuously from an in-memory hash join to a fully partitioned one as memory shrinks, rather than switching discontinuously. It is what a competent engine actually runs when it reports a hash join.
Commit
What no-force reduces a commit to.
More detail
Under force it would be a random write of every page the transaction touched. The whole point of the write-ahead log is to convert that into this, and group commit then amortises even the fsync across every transaction committing in the same window.
Page first touched after a checkpoint
The torn-page insurance premium, paid once per page per checkpoint cycle.
More detail
PostgreSQL pays it as a full-page image in the log; InnoDB pays it as a second write to the doublewrite buffer. Both roughly double write volume for the affected pages, which is why checkpoint interval is a write-amplification parameter and not only a recovery-time one.
Crash recovery
Analysis, redo and undo over the log tail.
More detail
Bounded by the checkpoint interval, and slower per byte than the writes that produced it, because the log was written by many concurrent transactions and is replayed by one process. Measure the replay rate on a copy rather than inferring it from the production write rate.