One paper. One change of model. Fifty years of consequence.
In June 1970, in the Communications of the ACM, an IBM researcher named Edgar F. Codd published a paper titled "A Relational Model of Data for Large Shared Data Banks." It was twelve pages long, packed with set-theoretic notation, and proposed something that sounded almost like a complaint: that the databases of the day were forcing programmers to think about how data was stored when they should only have to think about what data they wanted. Codd called the existing approach navigational — programmers wrote code that walked pointer to pointer through tree-shaped or network-shaped data structures, and any change to the storage layout broke every program above it. He proposed replacing all of that with a single abstraction: the relation, a mathematical set of tuples, queryable by a small algebra of operations that returned more relations.
The world Codd was reacting to was real and dominant. IMS, IBM's hierarchical database that ran on the System/360 and was used by banks and airlines, modelled data as a tree — to find a customer's orders you walked from a root to a customer node to an orders subtree. CODASYL, a network database standardised in 1969, modelled data as a graph — to find a customer's orders you traversed a "set" relationship from customer to orders, following pointers. Both worked well enough; neither survived two decades. Codd's complaint was not that they were slow but that they were brittle: the schema, the queries, and the storage layout were entangled, and changing any one broke the others.
"Future users of large data banks must be protected from having to know how the data is organized in the machine."
— E. F. Codd, "A Relational Model of Data for Large Shared Data Banks," Communications of the ACM, June 1970The paper proposed three things at once. First, a data model: every piece of data is a row (tuple) in a table (relation), and there is no other structure — no pointers, no hierarchies, no embedded references. Second, a query language based on set operations on relations — selection, projection, join, union, difference. Third, an independence principle: queries should describe the result, not the procedure to obtain it; the database engine should be free to choose any execution strategy that produces the same answer. Each idea by itself was conventional in mathematics; the combination, applied to commercial computing, was revolutionary.
IBM was unenthusiastic. The internal IMS team viewed Codd's work as academic, abstract, and unmarketable; an IBM prototype called System R was begun in 1973 but did not ship as a product until 1981 (as SQL/DS). In the meantime, in 1977, a 32-year-old programmer at a small consulting firm read Codd's paper, recognised its commercial potential, and founded a company specifically to build a relational database before IBM did. His name was Larry Ellison, the company was Software Development Laboratories (renamed Oracle in 1982 after the product), and Oracle V2 shipped in 1979 — two years before IBM's. The relational database industry began as a startup beating its inventor to market.
A second prototype, separate from IBM, was started at the University of California, Berkeley by Michael Stonebraker and Eugene Wong in 1973. Called Ingres, it became the basis for the open-source Postgres (1986) — which became PostgreSQL (1996) — and indirectly for Sybase, which Microsoft licensed and turned into SQL Server. So the modern relational database landscape descends from two prototypes built within five hundred miles of each other in the 1970s: System R at IBM Almaden, and Ingres at Berkeley. Almost every database in production in 2026 — Oracle, SQL Server, DB2, PostgreSQL, MySQL, SQLite, Snowflake, ClickHouse — has one of those two as its grand-grand-parent. Codd's paper produced an entire industry.
Two prototypes spawned the entire industry. System R at IBM Almaden begat DB2 (still at IBM) and indirectly Oracle (Ellison read Codd's paper, founded a company, beat IBM to market). Ingres at Berkeley begat Postgres (Stonebraker again, ten years later), which became PostgreSQL — now the most-used open-source database — and Sybase, which Microsoft licensed and renamed SQL Server. SQLite, MySQL, Snowflake, ClickHouse, BigQuery: all descend, by code or by influence, from one of these two trees. Codd's twelve-page paper produced a fifty-billion-dollar-a-year industry, of which he received approximately none.
Codd, IBM, and the Turing Award. Codd received the ACM Turing Award in 1981 for the relational model, the same year IBM finally shipped a relational product. He spent his last decade increasingly frustrated that commercial databases — including IBM's — implemented incomplete versions of his model and used "relational" as a marketing term for systems that were not quite. In 1985 he published "Twelve Rules for Relational Databases" as a definitional response, listing properties any system claiming the title must satisfy. Most commercial systems satisfied perhaps eight. The gap between Codd's mathematics and the working systems descended from his idea — index hints, query planner overrides, vendor-specific dialects — has never closed.
The math under SQL · three operators do most of the work.
A relation is a set of tuples that all share the same structure — a table, in everyday language, where each row is a tuple and each column is an attribute. The math of relations, called relational algebra, is a small collection of operators that take one or two relations as input and produce a new relation as output. The crucial property — the property that makes SQL composable — is closure: the result of any relational operation is itself a relation, and so can be the input to the next operation. Relational algebra has six fundamental operators (and several derived ones), but three matter especially. Most working SQL is built almost entirely from them.
Three operators do most of the work in any real query. Selection (σ, sigma) keeps the rows where a predicate holds; the rest are dropped. Projection (π, pi) keeps only the named columns; the rest disappear, and duplicates are eliminated because the result is a set. Join (⨝) takes two relations and produces every pair of rows whose values match on the named attributes — so combining a table of users with a table of orders gives you one row per (user, order) match. Closure says: each output is a relation, so any output can be the input to the next operator. The whole of SQL is, in spirit, repeated application of these three.
The other operators fall out of the same pattern. Union (∪) joins two relations of the same shape into one, keeping no duplicates. Difference (−) returns rows in the first relation that are not in the second. Cross product (×) returns every pairing of rows from two relations — the join without a matching predicate, mostly useless directly but the mathematical foundation of the join. Rename (ρ) lets a column or relation be referred to by a different name, useful for self-joins. With these six operators — σ, π, ⨝, ∪, −, × — plus ρ as a renaming utility, every relational query can be expressed. The relational algebra is, in the formal sense, a complete query language.
One subtlety is worth flagging. Relational algebra works on
sets: no duplicate rows. SQL, by contrast, works on
multisets (bags): duplicates are allowed unless you write
SELECT DISTINCT. The decision was pragmatic — many
applications want to count "how many orders have status='paid',"
which requires keeping duplicates — but it created a long-running
tension between SQL's behaviour and the formal theory underneath.
Codd considered this a flaw; modern SQL standards have largely lived
with it. Most working SQL programmers never know the distinction
exists; query optimisers know it intimately, because converting
multiset operations into set operations is one of the optimisations
that can dramatically speed up a query.
Relational algebra has the same role in databases that Boolean algebra has in circuits (Chapter 2): it is the smallest mathematical structure that captures the essential operations and is closed under composition. Closure means the algebra is compositional: the result of any expression is the same kind of object as its inputs, so expressions can be nested without bound. This is the property that lets a query optimiser take a complex SQL statement, parse it into a tree of relational operators, and then rewrite that tree algebraically into an equivalent but faster tree.
Two algebraic identities the optimiser leans on heavily: selection pushdown — σp(R ⨝ S) = σp(R) ⨝ S whenever the predicate p only mentions attributes from R — lets the engine filter R before joining, dramatically reducing the work. Projection pushdown — πcols(R ⨝ S) = πcols(πcols ∩ R(R) ⨝ πcols ∩ S(S)) — lets the engine throw away unneeded columns early. The transformations are correct because the algebra is mathematically rigorous; the speedups are real because they reduce the size of intermediate relations.
Declarative · standardised · still arguing about itself.
Codd's relational algebra is mathematics. SQL — Structured Query Language — is the working programmer's interface to it, designed at IBM in the mid-1970s alongside System R, originally called SEQUEL (Structured English Query Language) and renamed for trademark reasons. SQL is not a one-to-one transliteration of relational algebra; it is a deliberately English-shaped declarative language meant to be writable by analysts, not just by programmers. The compiler reads the English, parses it into a relational-algebra tree, runs an optimiser over the tree, and produces an execution plan. The English is the contract; the plan is private to the engine and is the engine's problem to optimise.
The language has two main halves. DDL — Data
Definition Language — is for changing the schema:
CREATE TABLE, ALTER TABLE,
DROP TABLE, CREATE INDEX. DML
— Data Manipulation Language — is for changing the data:
SELECT, INSERT, UPDATE,
DELETE. A typical application uses DDL during
deployment (when schemas change, which should be rare) and DML
during operation (every request the application handles). A typical
SQL statement someone writes looks deceptively close to English:
A SQL statement is parsed into a relational-algebra expression, which is then handed to the optimiser. The optimiser does not execute the algebra literally — it rewrites the tree using algebraic identities (selection pushdown, projection pushdown, join reordering, choice of join algorithm), uses statistics about each table's size and value distribution, and produces an execution plan. The plan is what actually runs. Two SQL statements that mean the same thing usually compile to the same plan; the optimiser does not care about the surface syntax. EXPLAIN in PostgreSQL or MySQL prints the plan — every working SQL programmer eventually learns to read it, because the difference between a fast query and a slow one is almost always the plan, not the SQL.
Three join algorithms
A JOIN in SQL is a single keyword. Underneath, it can be
executed three different ways, with very different cost profiles.
The optimiser picks one based on the size of the tables and the
presence of indexes. Working SQL programmers should at least know
that all three exist, because reading an EXPLAIN plan is
mostly the act of recognising which one was chosen and whether it
was the right choice.
Same SQL JOIN, three execution strategies. Nested-loop is naive but optimal when one side is tiny or has a useful index — every row of the smaller side does an indexed lookup against the larger. Hash join builds an in-memory hash table on the smaller side keyed by the join column, then streams the larger side and probes the hash; linear in both sides, fast on equality joins, requires the small side to fit in memory. Merge join sorts both sides on the join column and then walks them in lockstep; linear once sorted, indispensable for column-store OLAP engines and for range joins. The optimiser picks based on table sizes, indexes available, and the join condition. Reading EXPLAIN output is, almost always, the act of figuring out which strategy was chosen — and whether it was the right one.
The 1979 Selinger paper. The optimiser-as-component, with statistics-based cost estimates and dynamic-programming join-order search, was invented for IBM's System R by Patricia Selinger and described in a 1979 SIGMOD paper that is the most-cited paper in database history ("Access Path Selection in a Relational Database Management System"). Every relational query optimiser since — Oracle's, PostgreSQL's, SQL Server's, Snowflake's, BigQuery's — descends from Selinger's design. The paper is forty-seven years old in 2026 and is still required reading in graduate database courses for the same reason Codd's is. Both turned out to be definitive on first publication.
The four properties that make money safe to move.
A bank transfer reads like one operation but is at least two: subtract from the source account, add to the destination account. If the database crashes after the subtraction but before the addition, the money has vanished. If the addition succeeds but the subtraction fails, money has been duplicated. Two simultaneous transfers from the same account, racing each other, can read the same balance and double-spend it. Multiply these failure modes across millions of transfers per day and the financial system stops working. The transaction concept, articulated by IBM's Jim Gray in a foundational 1981 paper, is the machinery that prevents this — and the four-letter acronym ACID (Atomicity, Consistency, Isolation, Durability) is the contract every relational database makes with its users.
Each letter is a precise guarantee. Atomicity: a transaction either commits all of its operations or none of them. No half-applied transactions can ever be observed. Consistency: a transaction that begins in a valid database state ends in a valid database state — referential integrity holds, constraints are satisfied, data types are preserved. Isolation: concurrent transactions execute as if they ran one at a time, in some serial order, even though the engine actually overlaps their work. Durability: once a transaction has been told it committed, the data survives any subsequent crash, power loss, or hardware failure short of total disk loss. Atomicity and durability protect against failure; isolation and consistency protect against concurrency. Together they let an application reason about state changes as if the world stopped while each transaction ran.
The same logical operation — transfer €100 — without and with transactional guarantees. Without them, a power failure between the two updates leaves the database in an impossible state: money missing. With them, the database recovers either by replaying the whole transaction (if both updates were committed) or by rolling it back entirely (if not). The user sees one of two consistent results: the transfer happened, or it didn't. There is no "half-happened." Jim Gray made these properties precise; modern relational databases (and many non-relational ones — Spanner, FoundationDB, CockroachDB) implement them, with the techniques varying enormously underneath. The applications above sleep peacefully on top.
How durability is actually achieved · the write-ahead log
Durability and atomicity look like opposite problems — one wants
changes persisted, the other wants them reversible
— but the same mechanism handles both: the
write-ahead log (WAL). Before any change is made
to the actual data pages on disk, the engine writes a record of the
intended change to a sequential append-only log file, and
flushes that log record to physical storage with fsync().
Only after the log is durably on disk does the engine modify the
data pages — and those modifications can be lazy, written back at
leisure. If the database crashes, recovery reads the log forward
from the last checkpoint, replaying every committed transaction
whose changes might not have made it to the data pages, and rolling
back every uncommitted transaction whose partial changes might have
escaped to disk. The log is the source of truth; the data pages are
a cache.
The trick is that sequential writes to the log fsync orders of magnitude faster than scattered random writes to the data pages. The engine takes the cost of one cheap, sequential, durable log write per transaction and amortises the expensive data-page writes asynchronously, batching them and writing them back when convenient. On crash, the log tells recovery exactly what to replay forward (committed but not yet flushed to data pages) and what to undo (uncommitted changes that leaked to data pages). The same mechanism appears in filesystems (Chapter 4 §04, journaled ext4), in distributed databases (Raft and Paxos logs in Chapter 17), and in event-sourcing application architectures. Write the intent before doing the deed turns out to be one of the deepest patterns in systems engineering.
Isolation levels · the trade between safety and speed
Isolation in the strict sense — serializable isolation — means concurrent transactions behave indistinguishably from some serial execution of those transactions. It is the easiest to reason about and the most expensive to enforce. The SQL standard defines four isolation levels in decreasing order of strictness, each permitting more anomalies in exchange for more concurrency. The choice of level is a deliberate engineering trade: how much anomalous behaviour is the application willing to tolerate to gain throughput?
The isolation levels are defined by which anomalies they permit. Read Uncommitted (almost never used) lets you see another transaction's uncommitted changes — your transaction reads "dirty" data that may be rolled back. Read Committed (the PostgreSQL default) only shows committed data, but a row you read might be modified by another transaction before you read it again. Repeatable Read holds the rows you have already read stable, but new rows matching your filter can appear ("phantoms"). Serializable, the strictest, behaves as if every transaction ran by itself. Most production code uses Read Committed (Postgres) or Repeatable Read (MySQL InnoDB); Serializable is reserved for code where the cost of an anomaly outweighs the cost of throughput. Picking the right level is the working SQL programmer's most important concurrency decision — and one most programmers never explicitly think about, because the database default is usually adequate.
The data structure that turns O(N) into O(log N).
Without indexes, every WHERE clause is a full-table
scan. Find one user out of ten million by name? Read all ten
million rows. The query works correctly; it just takes minutes
instead of milliseconds. An index is an auxiliary
data structure that maps values of one or more columns to the rows
where those values appear. The structure makes lookups, range
scans, and sorts dramatically faster, at the cost of disk space
and slower writes (each insert or update has to keep the index in
sync). Two index structures dominate every relational database:
the B-tree and the hash index.
Each wins in a different regime.
B-tree · the workhorse
The B-tree, invented by Rudolf Bayer and Edward McCreight at Boeing in 1971, is the index of choice for relational databases — and for filesystems, key-value stores, and most other systems that keep ordered data on disk. It is a balanced tree designed for the memory hierarchy of Chapter 1: each node holds dozens or hundreds of keys (one disk page worth, typically 4 to 16 KB), the tree is shallow (depth O(logB N) for branching factor B), and every lookup or insertion traverses a small constant number of pages from root to leaf. A modern B-tree on a billion rows is typically only three to four levels deep; finding any single row is three or four page reads. Most pages are cached in RAM by the time the tree has been used for a while. Lookups become essentially free. Range scans are linear-time in the number of rows returned, because the leaves are linked left-to-right (the B+ tree variant — what every commercial database actually implements).
A B+ tree on the age column. Internal nodes route lookups by comparing against keys; leaf nodes hold the actual (key, row-pointer) pairs. Crucially, the leaves are linked left-to-right, so a range scan — "find every user aged 25 to 40" — does one tree descent to the first matching key (3 page reads at depth 3) and then walks leaves rightward emitting matches until it overshoots. The total cost is roughly the depth of the tree plus the number of matches divided by the leaf size. On a billion-row table that's typically 3–4 page reads plus a small linear scan, total time milliseconds. Without the index, the same query would scan the whole table — minutes. The B+ tree is the reason every "find by ID" or "show me orders from last week" feels instant; without it, the modern web does not work.
Hash index · O(1) lookup, no ranges
A hash index, by contrast, applies a hash function to the key and
uses the result as a lookup into an array. Equality lookups are
effectively O(1) — one hash, one array access — but the structure
has no ordering, so range queries
(WHERE id BETWEEN 100 AND 200) become impossible: the
hashed positions are scattered across the array, with no way to
walk neighbours. Hash indexes win when the queries are exclusively
equality lookups (typically primary-key joins, dictionary lookups,
session table by session ID); B-trees win everywhere else. In
PostgreSQL hash indexes exist but are rarely used in practice
because the modern B-tree is so well optimised that the
O(log N) cost is invisible in practice. In specialised systems
(Redis, in-memory KV stores, hash join intermediates), hash
indexes are the dominant choice precisely because no range
semantics are needed.
In practice nearly every secondary index in a relational database is a B-tree. Hash indexes appear in cases where every access is an exact-match equality on a single column — session tables, primary-key joins after the planner has decided on a hash join, in-memory caches like Redis. PostgreSQL has hash indexes (with WAL logging since version 10), but practitioners rarely create them explicitly because the B-tree's range capabilities are too valuable and the constant factor is small. The optimiser picks; the user mostly does not have to know. But the user does have to know that creating an index on a column the application queries against is the single most effective performance change available — and that creating too many indexes slows every write, because every index has to be kept in sync.
String concatenation as the attack vector that will not die.
SQL injection is the most consequential vulnerability class in the history of web security and the one with the simplest underlying cause. An application that builds a SQL query by concatenating user input into a string lets the user supply arbitrary SQL. The database, having no way to distinguish "code the developer wrote" from "code the user supplied," dutifully executes both. The result is the attacker reading any data, modifying any data, or in the worst case taking over the database server entirely. The vulnerability has been documented since the late 1990s, the fix has been universally available since the early 2000s, and SQL injection is still on the OWASP Top 10 in 2026. The reason is sociological as much as technical: every new generation of programmers has to discover for themselves that string concatenation is the wrong tool.
The mechanism is small enough to fit on a card. Suppose a login form takes a username and password and constructs the query by string concatenation:
The simplest SQL injection: an apostrophe in the input terminates the developer's quoted string, and OR '1'='1 turns the WHERE clause into a tautology that matches every row. The login check returns the first user — usually an admin — and the attacker is logged in. More elaborate payloads use UNION SELECT to exfiltrate data from other tables, DROP TABLE to destroy data, or "blind" techniques that infer data one bit at a time from timing or error messages. The 2007 TJX breach (94 million payment cards), the 2008 Heartland breach (130 million), the 2011 Sony PlayStation Network breach (77 million), and dozens of smaller incidents — including, in 2023, a major US municipal water utility — were all SQL injection. The cumulative direct cost runs into tens of billions of dollars.
The fix · parameterised queries · and only that
The defence is clean and absolute: never build queries by string concatenation. Use parameterised queries (also called prepared statements), where the SQL text and the data values are sent to the database as separate things. The database parses the SQL once, with placeholder slots, and then receives the actual values through a separate channel that does not interact with the parser. No matter what the attacker sends as a value — an apostrophe, a SQL keyword, a multi-statement payload — it is treated as data, not parsed as code. The predicate is always exactly what the developer wrote.
Parameterised queries solve SQL injection at the protocol level. The SQL text and the data values arrive at the database through different paths in the wire protocol; the parser only sees the SQL text with ? placeholders, and the values are bound to those placeholders after parsing has already produced the execution plan. The same attacker payload that bypassed login in Fig 13.10 — ' OR '1'='1 — becomes a literal string the database is asked to match exactly. No user has that name. The query returns zero rows. Every modern database driver supports prepared statements. Every modern web framework wraps them. The pattern is universally available; the only reason SQL injection still exists is that programmers still occasionally bypass the safe path.
Defence in depth. Parameterised queries close the primary vulnerability, but a serious application stacks more layers. ORMs (object-relational mappers — Django ORM, SQLAlchemy, ActiveRecord, Entity Framework) wrap parameterisation by default; programmers using them rarely write raw SQL at all, which makes a SQLi mistake harder to introduce. Least-privilege database accounts ensure that even a successful SQLi cannot DROP TABLE — the application's connection has only SELECT, INSERT, UPDATE on its own tables. Web Application Firewalls (Cloudflare, AWS WAF, ModSecurity) attempt to detect SQL-injection-shaped strings in HTTP requests and block them before they reach the application. Input validation rejects requests that don't match the expected shape (numeric IDs are numeric; UUIDs match the UUID regex). None of these alone is sufficient; together they make SQLi rare even when one layer fails.
Closing the chapter · seam to Chapter 14
Chapter 13 closes the data-at-rest half of Part IV. The relational model is fifty years old and shows no sign of being replaced. NoSQL systems came and went; most of them eventually added schemas and JOINs and ACID. The reasons are not nostalgia: relational algebra is mathematically complete, the transaction model is mathematically precise, and the optimisation tooling — Selinger's planner, the WAL, the B-tree — has had fifty years of battle-testing. SQL is one of the most durable interfaces in computing.
What it does not do, by itself, is keep data secret. The database stores rows, and anyone with read access to the rows can read the rows. To keep secrets — passwords, payment card numbers, private keys, medical records, any of the data the database is legally and ethically bound to protect — we need cryptography. We have already met it briefly, in Chapter 11 §04, as the substrate of TLS. Chapter 14 takes it apart at full mathematical depth: hash functions worked through, AES round by round, public-key cryptography with actual numbers, elliptic curves drawn, and the post-quantum standardisation now underway. Cryptography is, in a precise sense, the only mathematics in this book whose entire purpose is to be hard to reverse.