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 eleven pages long, packed with set-theoretic notation, and proposed something that sounded 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. The existing approach later came to be called navigational: programmers wrote code that walked pointer to pointer through tree-shaped or network-shaped data structures. Codd proposed replacing all of that with a single abstraction: the relation, a table of rows in no fixed order (to a mathematician, a set of tuples), queryable by a small algebra of operations that returned more relations.
Two systems embodied the navigational approach. 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, the network-model standard drawn up by committee in 1969, modelled data as a graph, so to find a customer's orders you traversed a "set" relationship from customer to orders, following pointers. Both worked well enough; neither would define the era that followed. IBM still ships IMS in 2026, but as a niche survivor, not the model everyone else built on. Codd's complaint was that they were brittle: the schema (which kinds of records exist and which fields each has), 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.
IBM was unenthusiastic. The internal IMS team viewed Codd's work as academic and unmarketable; an IBM prototype called System R was begun in 1974 but did not ship as a product until 1981 (as SQL/DS). In the meantime, in 1977, a 32-year-old programmer named Larry Ellison read Codd's paper and founded a company to build a relational database before IBM did: Software Development Laboratories (renamed Oracle in 1982 after the product). Oracle V2 shipped in 1979, two years before IBM's.
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 led to Stonebraker's successor project, Postgres (1986), which became the open-source PostgreSQL (1996), and, through its alumni, to Sybase, which Microsoft licensed and turned into SQL Server. So DB2, PostgreSQL and SQL Server, major databases in production in 2026, descend from one of two 1970s prototypes, System R at IBM San Jose or Ingres at Berkeley, and Oracle was built from IBM's published papers on System R; others, like MySQL, SQLite, Snowflake and ClickHouse, were written fresh to speak the SQL that System R introduced.
Codd's 1970 paper inspired two prototypes, System R at IBM and Ingres at Berkeley, and the major relational databases still running descend from one or the other: DB2 and, through IBM's published SQL, Oracle; PostgreSQL and, through Ingres's people, Sybase and SQL Server.
How it works
- IBM's side. System R produced SQL; IBM shipped it as SQL/DS (1981) and announced DB2 in 1983 (on sale from 1985), after Oracle had shipped its own version, built from IBM's published papers, in 1979.
- Berkeley's side. Ingres led to Postgres and PostgreSQL (version 18 in 2025); Ingres alumni founded Sybase, which Microsoft licensed for SQL Server.
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. In the years that followed he grew increasingly frustrated that commercial databases, IBM's included, implemented incomplete versions of his model and used "relational" as a marketing term for systems that were not quite. In 1985, in two Computerworld pieces (the first asking "Is Your DBMS Really Relational?"), he published twelve rules, thirteen counting rule 0, listing properties any system claiming the title must satisfy. Few commercial systems met them all, and the gap between Codd's mathematics and the working systems descended from his idea (SQL tables that allow duplicate rows, which Codd's sets forbid, among others) 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, relational algebra, is a small collection of operators that take one or two relations and produce a new one. The crucial property, the one that makes SQL composable, is closure: the result of any operation is itself a relation, and so can be the input to the next. The algebra needs only six fundamental operators, and most working SQL is built from three operations: selection (σ), projection (π) and join (⨝), the last of them derived from the others.
Selection keeps the rows that pass a test, projection keeps the columns you name, and a join pairs the rows of two tables that agree on a value. Each takes tables and returns a table, so the output of one can feed the next: that closure is what lets SQL queries nest.
How it works
- In SQL. σ is the
WHEREclause, π the column list afterSELECT, ⨝ aJOIN … ON.
Each of the other operators buys a specific question. Union (∪) stacks two relations of the same shape and drops duplicates: "everyone who is a customer or an employee." Difference (−) keeps the rows of the first that are absent from the second, which is how you ask for an absence: feed it all customers and all customers-who-ordered, and what falls out is exactly the customers who never ordered. Cross product (×) pairs every row of one relation with every row of the other; on its own it is a firehose, but bolt a selection onto it and you have rebuilt the join from parts. Rename (ρ) relabels a column or relation so a table can be joined to itself (an employee to their manager, both rows drawn from the same table under two names). These six (σ, π, ∪, −, ×, ρ) are enough: Codd also described a second way to ask for data, by stating in logic which rows you want (the relational calculus), and in 1972 showed that an algebra of this kind can fetch whatever rows any such statement describes. That is what relationally complete means. Counting, totals and chains of unknown length (every manager above someone, however many levels up) sit outside it; SQL added them later.
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: keep only the status column and ten paid orders are ten identical rows, which a set would collapse into one. Codd considered it
a flaw, the source of a long-running tension between SQL's behaviour
and the formal theory underneath; modern SQL standards have largely
lived with it. Most working SQL programmers never know the distinction
exists; query optimisers (the part of the database that picks how to run a query, Section 03) care, because removing duplicates means sorting the rows or building a lookup table to spot repeats, and when the query already guarantees there are none (its columns include each row's unique id, say) they skip that work.
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. Because every expression yields a relation, the algebra is compositional: expressions nest without bound. That is what lets the database parse a complex SQL statement into a tree of relational operators, and lets the optimiser rewrite that tree algebraically into an equivalent but faster one.
The optimiser leans heavily on two rewrite rules. Selection pushdown says that if a test p uses only the columns of one table, R, the engine can filter R first and join afterwards: σp(R ⨝ S) = σp(R) ⨝ S, the same rows with far fewer of them to join (Fig 13.3, in Section 03, shows it). Projection pushdown lets the engine drop the columns the query never uses before the join, but it must keep the join columns, since without them there would be nothing left to match on. 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 the algebra but a deliberately English-shaped declarative language, meant to be writable by analysts, not just programmers. The compiler parses the English into a relational-algebra tree, runs an optimiser over the tree, and produces an execution plan.
The language has two main halves. DDL (Data Definition
Language) changes the schema: CREATE TABLE,
ALTER TABLE, DROP TABLE,
CREATE INDEX. DML (Data Manipulation
Language) reads and changes the data: SELECT,
INSERT, UPDATE, DELETE. An
application runs DDL at deployment, when the schema changes (which
should be rare), and DML on every request it handles. A typical
statement looks deceptively close to English:
The SQL is parsed into an algebra tree, and the optimiser rewrites the tree before anything runs: here it moves the age filter below the join, so only adults are joined, then picks how to run each node. The plan, not the SQL, decides how fast the query is.
How it works
- The choices. Statistics on table sizes and value spread decide each step: an index on age (a lookup shortcut, Section 05) pays only if few rows pass, because each match costs a separate jump (when most rows pass, as in a typical users table, scanning wins), and a hash join is chosen when both inputs are large (Fig 13.4).
- Seeing it.
EXPLAINin PostgreSQL or MySQL prints the plan the engine chose.
Three join algorithms
A JOIN in SQL is a single keyword. Underneath, it can be
executed three ways, and the gap between them is not small: the naïve
strategy (for every row of one table, scan the whole other table)
costs N×M row-comparisons, which on two million-row tables is a
trillion of them, hours of work. The other two strategies buy their way
out of that multiplication, one with a hash table and one with a sort, and
the optimiser's job is to pick the cheapest for the tables in front of it.
The same equality join, three ways: a nested loop compares every pair, a hash join buckets one side and looks each key of the other up once, and a merge join walks two sorted lists together. Five rows a side: 25 comparisons against about ten; a million a side: a trillion against a few million.
How it works
- Nested loop. Work grows with the product of the two table sizes, yet it is the right choice when one side is tiny or the inner side has an index on the join key (Section 05), so each outer row costs one index lookup.
- Hash join. Work grows with the sum of the two sizes, for equality joins, if the smaller side's hash table fits in memory; otherwise it is split into partitions on disk.
- Merge join. Also the sum, after sorting, so it wins when the inputs already arrive sorted, for example from an index (Section 05). The optimiser picks among the three from table sizes, indexes and the join condition.
The 1979 Selinger paper. Patricia Selinger and her colleagues at IBM designed the cost-based optimiser for System R. It estimates each plan's cost from statistics about the tables, and finds the cheapest join order by working out the best way to join each small group of tables first and building up from there. Their 1979 paper at SIGMOD, the main database research conference ("Access Path Selection in a Relational Database Management System"), is among the most cited on query optimisation, and every cost-based optimiser since (Oracle's, PostgreSQL's, SQL Server's, the cloud warehouses') builds on it. Forty-seven years on, in 2026, the paper is still required reading in graduate database courses for the same reason Codd's is: both were 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 can read the same balance and double-spend it. The transaction concept, articulated by Jim Gray in a 1981 paper, is the machinery that prevents this. ACID (Atomicity, Consistency, Isolation, Durability) is the contract every relational database makes with its users.
Atomicity: a transaction (a group of operations the database treats as one unit, ended by a COMMIT that makes it final) either commits all of its operations or none of them. Consistency: a transaction that begins in a valid database state ends in one, with its rules intact (every order still belongs to a customer who exists, every amount is a number). 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 protects against concurrency; consistency is what the other three exist to preserve.
A transfer is two updates. If the power fails between them, a database without transactions wakes up with Alice debited and Bob unpaid; inside a transaction, BEGIN to COMMIT, recovery finds no commit record and undoes the debit, so the transfer happened in full or not at all.
How it works
- How recovery knows. Every change goes to the write-ahead log before the table (Fig 13.6); at restart, the changes of any transaction with no commit record are undone.
- Who. Jim Gray set out the transaction concept in 1981 (Turing Award 1998); Theo Härder and Andreas Reuter named it ACID in 1983.
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 touching the data pages (the fixed-size blocks on disk that hold the tables' rows), the engine appends a record of the intended
change to a sequential log file and forces it onto physical storage (the fsync() system call). Only then does the engine modify the data
pages, lazily. If the database crashes, recovery reads the log forward from the last checkpoint (a marker in the log: every change before it is already safely in the data pages, so recovery can start there), 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.
Every change is written to the log and forced to disk before the commit is acknowledged; the data pages catch up later, whenever convenient. After a crash, recovery replays the log: committed changes the pages missed are redone, and uncommitted ones that reached them are undone.
How it works
- Why it is fast. Appending to one file is sequential; updating data pages means writes scattered across the disk. A commit costs one log flush, and the pages catch up in the background, at checkpoints.
- Redo and undo. This is the ARIES scheme (IBM, 1992), the model for DB2 and SQL Server; MySQL's InnoDB engine follows the same redo-and-undo shape. PostgreSQL's log is redo-only: it keeps old and new versions of each row (MVCC, explained below), so an aborted transaction's versions simply stay invisible.
- The same idea elsewhere. Journaling file systems (Chapter 4) and the replicated logs of Raft and Paxos (Chapter 17) write the intent before doing the deed.
Isolation levels · the trade between safety and speed
Isolation as defined above, with transactions appearing to run one at a time, is called serializable. It is the easiest to reason about and the most expensive to enforce. The SQL standard defines four isolation levels; each step down from serializable permits more anomalies in exchange for more concurrency: a dirty read sees another transaction's uncommitted change, a non-repeatable read sees a row change between two reads of it, and a phantom sees new rows appear.
Which anomalies each level allows, and one re-read run: the same read returns 50 under read committed and 100 under repeatable read.
How it works
- The defaults. PostgreSQL runs at read committed, MySQL's InnoDB at repeatable read; serializable is kept for code where an anomaly costs more than lost throughput.
- The table is a minimum. PostgreSQL's repeatable read blocks phantoms too: it gives each transaction one snapshot of the data (explained below). Only serializable also stops write skew: two transactions each check a rule against the same snapshot and both act, so together they break it (two doctors each go off call, each seeing the other still on duty).
What the standard does not say is how an engine provides these levels, and there are two families. The pessimistic family is locking: a transaction takes shared locks on what it reads (other readers may share them; a writer must wait) and exclusive locks on what it writes (everyone else waits), holds them until commit, and anyone who would conflict waits. The discipline is called two-phase locking, because every lock is taken before any is released. The other family is MVCC, multi-version concurrency control: a writer never destroys a version of a row that a reader may still need (PostgreSQL appends a new version stamped with its transaction id; InnoDB updates in place and keeps the old one in an undo log), and every reader is handed a snapshot (the set of versions visible as of one instant), so a reader takes the old version while a writer adds the new one. Readers never block writers and writers never block readers. PostgreSQL uses MVCC throughout: read committed takes a fresh snapshot per statement, repeatable read one per transaction, and serializable also watches for concurrent transactions that read data another was changing. When two of them overlap in a way that might give a result no one-after-the-other order could, it aborts (cancels and rolls back) one, even when the overlap was harmless. MySQL's InnoDB mixes the families: MVCC snapshots for reads, plus locks on ranges of rows that stop phantoms appearing.
The data structure that turns O(N) into O(log N).
Without indexes, every WHERE clause is a full-table
scan. To find one user out of ten million by name, the engine reads
all ten million rows. The query is correct; it just takes seconds instead of milliseconds. (Engineers write this cost O(N): the work grows in step with the number of rows, N. The structures below cut it to O(log N), one more step each time the table grows by a fixed factor (about a hundredfold, for the B-tree below), or O(1), about the same whatever the size.) An index is an auxiliary
data structure that maps values of one or more columns to the rows
where those values appear. It 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 are the classic choices: the
B-tree and the hash index.
B-tree · the workhorse
The B-tree, invented by Rudolf Bayer and Edward McCreight at Boeing in 1970, is the index of choice for relational databases, 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 hundreds of keys (one disk page, typically 4 to 16 KB), so the tree is shallow (each extra level lets the tree reach a hundred or more times as many keys), and every lookup or insertion reads one page per level from root to leaf, the keys in each upper page acting as signposts (below this key go left, at or above it go right), with the real keys and their rows in the leaves. On a billion rows that is typically three or four levels, so three or four page reads; once the tree has been in use, most of those pages sit in RAM and a lookup costs almost nothing. A range scan (every key from, say, 20 to 40) costs one descent to the first key plus a walk that grows with the number of rows returned, because the leaves are linked left-to-right (the B+ tree variant, which every commercial database implements).
A lookup descends from the root, one page per level, to the leaf holding the first key in range; the scan then walks the linked leaves left to right, emitting keys until one passes the end. Here: three page reads down and two along (five in all), finding 22, 30 and 38; 45 stops it.
How it works
- Inserts. A full leaf splits in two and pushes a key up to its parent; when the root splits, the tree grows a level at the top, so every leaf stays at the same depth.
- Why the links. Only the leaves hold keys with row pointers; linking them turns a range into one descent plus a walk, the cost being the depth plus the matches divided by the leaf size.
Hash index · O(1) lookup, no ranges
A hash index applies a hash function to the key and uses the
result as a position in an array. An equality lookup (find the row whose key equals a given value) costs about the same few steps however big the table is: one hash, one array access, and a quick check of whatever else landed in the same slot. But the structure has no
ordering, so range queries (WHERE id BETWEEN 100 AND 200)
cannot use the index: the hashed positions are scattered across the
array, with no way to walk neighbours (and a range can hold values
nobody could look up one by one, such as every name from Al to Am). Hash indexes win when every query is an equality lookup (a session table read by session ID, Redis and other in-memory key-value stores); B-trees win everywhere else. PostgreSQL has hash indexes, but they are rarely used, because the modern B-tree is so well optimised that its few extra page reads disappear into the noise.
Both indexes find a single key in a step or two; a range is where they part. The B-tree keeps keys in order, so 40 to 45 sit side by side, while hashing scatters them across the buckets and the only way to find them is to look in every one.
How it works
- B-tree. O(log N) to find a key, plus k to read k neighbours; it also serves
ORDER BYand prefix matches such asLIKE 'Al%'. - Hash. O(1) on average for equality, nothing for ranges or order (the toy hash here is 37 × id mod 8; real ones scramble harder). PostgreSQL's hash indexes have been crash-safe (WAL-logged) since version 10.
String concatenation as the attack vector that will not die.
SQL injection is one of the most damaging vulnerability classes in the history of web security, and the one with the simplest 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 attacker can then read or modify any data, or in the worst case take 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, the standard list of the worst web risks, 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.
Suppose a login form takes a username and password and builds the
query by string concatenation. SQL writes a text value between
apostrophes (name='alice') and reads it up to the next
apostrophe; once the pieces are glued together, the database receives
one line and cannot tell which apostrophes the user typed:
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 from whether the page changes at all. The 2008 Heartland breach (130 million payment cards), the 2011 Sony PlayStation Network breach (77 million), the 2023 MOVEit breach (personal data on tens of millions), and dozens of smaller incidents were all SQL injection. (The 2007 TJX breach of 94 million cards is often lumped in, but its front door was different: attackers cracked the WEP encryption on store wireless networks and walked in from the parking lot.) 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, then receives the values through a channel that does not touch the parser. Whatever the attacker sends as a value (an apostrophe, a SQL keyword, a multi-statement payload) stays data and is never parsed as code.
With a parameterised query the SQL text, with a placeholder where each value goes, is parsed once into a plan, and the values travel separately and are bound into it afterwards. Since a value never reaches the parser, nothing in it can change what the query does: an apostrophe is only a letter.
How it works
- On the wire. PostgreSQL's extended query protocol and MySQL's prepared statements send the statement and its parameters as separate messages; a database library that only emulates this has to escape every value itself (mark each apostrophe so the parser reads it as a letter).
- Values only. A placeholder can hold a value but not a table or column name, so code that varies those must pick from a fixed list.
- Reuse. The parsed statement can be kept and run again for each new value, which saves the parse.
Defence in depth. Parameterised queries close the primary vulnerability, but a serious application stacks more layers, because one query still built by hand anywhere in the program reopens the hole. ORMs (object-relational mappers, libraries such as Django ORM and ActiveRecord that let code read and write rows as ordinary objects while they write the SQL) use parameterised queries by default, so programmers using them rarely write raw SQL at all. Least-privilege database accounts ensure that even a successful injection cannot DROP TABLE, because the application's connection has only SELECT, INSERT, UPDATE on its own tables. Web Application Firewalls (Cloudflare, AWS WAF, ModSecurity) try to spot 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, dates are dates). None of these replaces parameterised queries; they catch what slips through when a query somewhere was built by concatenation after all.
Closing the chapter · seam to Chapter 14
The relational model is fifty years old and shows no sign of being replaced. NoSQL systems (databases that gave up fixed tables or SQL for speed or flexibility) rose to challenge it, and many of them later added schemas, joins or transactions. Relational algebra is relationally complete and composes without limit, the transaction model is mathematically precise, and the machinery underneath (Selinger's planner, the write-ahead log, the B-tree) has been tested in production for decades.
What the relational model does not do, by itself, is keep data secret. The database stores rows, and anyone with read access to the rows can read them. To keep secrets (passwords, payment card numbers, medical records, any of the data the database is legally and ethically bound to protect), we need cryptography, which Chapter 14 takes apart at full mathematical depth.