Databases
a database is a data structure that survives a power cut, shared by programs that don’t trust each other, queried in a language older than most of its users. the relational model has been declared dead roughly once a decade since 1970 and has outlived every announced successor. π this page covers the model, the algebra underneath SQL, normalisation, the storage structures that make queries fast, and the machinery that keeps concurrent transactions honest.
the relational model
codd’s 1970 move: store everything as relations β sets of tuples over named, typed attributes β and let the system, not the programmer, decide how to navigate them.
- a relation is a set of tuples; a tuple is one row; an attribute is a named column with a domain. “table” is the implementation-flavoured word for the same thing.
- a superkey is any attribute set that uniquely identifies tuples; a candidate key is a minimal superkey; the primary key is the candidate key you commit to; a foreign key is an attribute set referencing another relation’s key.
- integrity constraints are the guarantees the DBMS enforces so your application doesn’t have to:
- domain: values come from the attribute’s type (
mark INTEGER,CHECK (year BETWEEN 1 AND 6)), - entity: no part of a primary key may be null β you cannot half-identify a row,
- referential: every foreign key value must exist in the referenced relation β no enrolments for students who don’t exist (Silberschatz, Abraham and Korth, Henry F. and Sudarshan, S., 2019).
- domain: values come from the attribute’s type (
the deep idea is data independence: queries are written against the logical schema, and the physical layout (indexes, partitions, storage order) can change underneath without breaking a single query.
relational algebra
SQL is syntax; relational algebra is the semantics the optimiser actually manipulates. six primitive operators generate the whole language (Silberschatz, Abraham and Korth, Henry F. and Sudarshan, S., 2019):
| operator | notation | meaning |
|---|---|---|
| selection | \(\sigma_{\theta}( R)\) | keep tuples satisfying predicate \(\theta\) |
| projection | \(\pi_{A_1,\dots,A_k}( R)\) | keep only the listed attributes (deduplicating) |
| union | \(R \cup S\) | tuples in either (schemas must match) |
| set difference | \(R \setminus S\) | tuples in \(R\) but not \(S\) |
| cartesian product | \(R \times S\) | every pairing of tuples |
| rename | \(\rho_{S(B_1,\dots)}( R)\) | relabel a relation/attributes (enables self-joins) |
everything else is sugar: the natural join is \(R \bowtie S = \pi_{\ldots}(\sigma_{R.A = S.A}(R \times S))\), and intersection is \(R \cap S = R \setminus (R \setminus S)\). the optimiser exploits algebraic identities β pushing a selection below a join is legal because \(\sigma_{\theta}(R \bowtie S) = \sigma_{\theta}( R) \bowtie S\) whenever \(\theta\) touches only \(R\)’s attributes, and it is usually the single biggest win in a query plan.
SQL, actually run
schema, inserts, a join, an aggregate, and a window function β the five things that cover most daily SQL. python’s built-in sqlite3 (engine version 3.50) executes all of it:
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("PRAGMA foreign_keys = ON") # off by default; REFERENCES is decorative without it
cur = con.cursor()
cur.executescript("""
CREATE TABLE students (
sid INTEGER PRIMARY KEY,
name TEXT NOT NULL,
year INTEGER CHECK (year BETWEEN 1 AND 6)
);
CREATE TABLE courses (
cid TEXT PRIMARY KEY,
title TEXT NOT NULL,
uoc INTEGER DEFAULT 6
);
CREATE TABLE enrolments (
sid INTEGER REFERENCES students,
cid TEXT REFERENCES courses,
mark INTEGER,
PRIMARY KEY (sid, cid) -- composite key
);
INSERT INTO students VALUES (1,'ada',3),(2,'grace',2),(3,'edsger',4),(4,'barbara',3);
INSERT INTO courses VALUES ('COMP3311','databases',6),('COMP3331','networks',6),
('COMP3231','operating systems',6);
INSERT INTO enrolments VALUES (1,'COMP3311',91),(1,'COMP3331',84),(2,'COMP3311',78),
(2,'COMP3231',88),(3,'COMP3331',95),(3,'COMP3231',72),
(4,'COMP3311',85);
""")
print("-- join: who is enrolled in what")
for row in cur.execute("""
SELECT s.name, c.title, e.mark
FROM enrolments e
JOIN students s ON s.sid = e.sid
JOIN courses c ON c.cid = e.cid
ORDER BY s.name, c.cid"""):
print(row)
print("-- aggregate: course averages, hardest first")
for row in cur.execute("""
SELECT cid, COUNT(*) AS n, ROUND(AVG(mark),1) AS avg_mark
FROM enrolments GROUP BY cid
HAVING COUNT(*) >= 2
ORDER BY avg_mark"""):
print(row)
print("-- window: rank within each course without collapsing rows")
for row in cur.execute("""
SELECT cid, sid, mark,
RANK() OVER (PARTITION BY cid ORDER BY mark DESC) AS rk,
ROUND(AVG(mark) OVER (PARTITION BY cid), 1) AS course_avg
FROM enrolments ORDER BY cid, rk"""):
print(row)
-- join: who is enrolled in what
('ada', 'databases', 91)
('ada', 'networks', 84)
('barbara', 'databases', 85)
('edsger', 'operating systems', 72)
('edsger', 'networks', 95)
('grace', 'operating systems', 88)
('grace', 'databases', 78)
-- aggregate: course averages, hardest first
('COMP3231', 2, 80.0)
('COMP3311', 3, 84.7)
('COMP3331', 2, 89.5)
-- window: rank within each course without collapsing rows
('COMP3231', 2, 88, 1, 80.0)
('COMP3231', 3, 72, 2, 80.0)
('COMP3311', 1, 91, 1, 84.7)
('COMP3311', 4, 85, 2, 84.7)
('COMP3311', 2, 78, 3, 84.7)
('COMP3331', 3, 95, 1, 89.5)
('COMP3331', 1, 84, 2, 89.5)
the window function is the one worth internalising: GROUP BY collapses rows to one per group, while OVER (PARTITION BY ...) computes the same aggregate but keeps every row β rank and group average side by side with the raw mark.
π
normalisation
redundancy is the disease; update, insertion, and deletion anomalies are the symptoms. functional dependencies are the diagnostic tool.
a functional dependency \(X \to Y\) holds when tuples agreeing on attribute set \(X\) must agree on \(Y\). armstrong’s axioms (reflexivity, augmentation, transitivity) generate all implied dependencies.
a worked decomposition
start with one wide relation recording marks, one row per enrolment:
\begin{equation*} \text{enrol}(\underline{\text{sid}}, \underline{\text{cid}}, \text{sname}, \text{ctitle}, \text{dept}, \text{head}, \text{mark}) \end{equation*}
with dependencies
\begin{align*} \text{sid} &\to \text{sname} \\ \text{cid} &\to \text{ctitle}, \text{dept} \\ \text{dept} &\to \text{head} \\ \text{sid}, \text{cid} &\to \text{mark} \end{align*}
- 1NF demands atomic values and no repeating groups β storing a student’s courses as a comma-separated string violates it; one row per enrolment fixes it. the relation above is already 1NF.
- 2NF bans partial dependencies: non-key attributes depending on part of the composite key.
snamedepends onsidalone,ctitleanddeptoncidalone. redundancy is visible: ada’s name is repeated once per enrolment, and you cannot record a course before anyone enrols (insertion anomaly). decompose:
\begin{equation*} \text{student}(\underline{\text{sid}}, \text{sname}), \quad \text{course}(\underline{\text{cid}}, \text{ctitle}, \text{dept}, \text{head}), \quad \text{enrol}(\underline{\text{sid}}, \underline{\text{cid}}, \text{mark}) \end{equation*}
- 3NF bans transitive dependencies:
ciddeterminesdept,deptdetermineshead, soheadis stored once per course instead of once per department β change a head of department and you update many rows or corrupt the data. split again:
\begin{equation*} \text{course}(\underline{\text{cid}}, \text{ctitle}, \text{dept}), \quad \text{department}(\underline{\text{dept}}, \text{head}) \end{equation*}
- BCNF tightens 3NF: for every nontrivial \(X \to Y\), \(X\) must be a superkey. the classic residual case: \(\text{tute}(\text{sid}, \text{cid}, \text{tutor})\) where each tutor teaches exactly one course (\(\text{tutor} \to \text{cid}\)) and each student has one tutor per course (\(\text{sid}, \text{cid} \to \text{tutor}\)).
tutoris not a superkey, so BCNF says split into \(\text{tutorof}(\underline{\text{tutor}}, \text{cid})\) and \(\text{assigned}(\underline{\text{sid}}, \underline{\text{tutor}})\) β but now the dependency \(\text{sid}, \text{cid} \to \text{tutor}\) spans two tables and can no longer be checked without a join. BCNF is not always dependency-preserving; 3NF always is. every decomposition above is lossless (the shared attributes are keys of one side), which is the non-negotiable property (Silberschatz, Abraham and Korth, Henry F. and Sudarshan, S., 2019).
normalise until it hurts, denormalise until it works β but know which dependencies you are knowingly breaking.
indexing
why B+-trees and not binary trees
disks and SSDs are read in pages (4β16 KiB), and each random page fetch is the expensive unit. a balanced binary tree on \(10^8\) keys is \(\log_2 10^8 \approx 27\) levels deep β 27 page fetches if each node lands on a different page, which they do, because a binary node is a few dozen bytes rattling around alone. the fix: make each node exactly one page wide and branch as hard as the page allows (Silberschatz, Abraham and Korth, Henry F. and Sudarshan, S., 2019).
a B+-tree keeps all records (or record pointers) in the leaves, threaded into a sorted linked list; internal nodes hold only separator keys and child pointers, and every leaf sits at the same depth.
fanout arithmetic. take a 4 KiB page, 8-byte keys, 8-byte pointers. an internal node holding \(n\) child pointers needs \(n-1\) keys: \(8n + 8(n-1) \le 4096\) gives \(n \approx 256\). for \(N = 10^8\) keys with 256-entry leaves:
- leaves: \(\lceil 10^8 / 256 \rceil = 390{,}625\)
- level above: \(\lceil 390{,}625 / 256 \rceil = 1{,}526\)
- above that: \(\lceil 1{,}526 / 256 \rceil = 6\), then a root of 6 pointers.
four levels for a hundred million keys, and since the root and second level (about 7 pages) live permanently in the buffer pool, a cold lookup costs roughly two page reads β against 27 for the binary tree. the same structure answers range queries by walking the leaf chain, which a hash index cannot do.
the other index shapes
- hash indexes map key to bucket in expected \(O(1)\) β faster than a B+-tree for pure equality lookups, useless for ranges,
ORDER BY, or prefix matches, because the hash deliberately destroys ordering. - covering indexes: if an index on
(cid, mark)contains every column a query touches, the engine answers from the index alone and never visits the table β an “index-only scan”. the fastest table read is the one that doesn’t happen. - indexes are not free: every
INSERT,UPDATE, andDELETEmust maintain all of them, so write-heavy tables want few, carefully chosen indexes. π
query processing
a join \(R \bowtie S\) (with \(|R| = n\), \(|S| = m\) tuples; \(B_R\), \(B_S\) pages) can be executed three classic ways (Silberschatz, Abraham and Korth, Henry F. and Sudarshan, S., 2019):
| algorithm | in-memory cost | i/o cost (pages) | when it wins |
|---|---|---|---|
| nested loop | \(O(nm)\) | \(B_R + n \cdot B_S\) | tiny inner relation, or an index on it |
| block nested loop | \(O(nm)\) | \(B_R + B_R \cdot B_S\) (worst) | small memory, no order, no index |
| hash join | \(O(n + m)\) | \(\approx 3(B_R + B_S)\) (grace) | equality joins, enough memory to partition |
| sort-merge join | \(O(n\log n + m\log m)\) | sort cost \(+\ B_R + B_S\) | inputs already sorted, or output must be |
- nested loop is the naive double
for; with an index on the inner join key it becomes index nested loop and is excellent for small outer relations. - hash join builds a hash table on the smaller relation, probes with the larger; when neither fits in memory, both are partitioned to disk by hash first (grace hash join).
- sort-merge sorts both on the join key and zips them; it is the reason
ORDER BYon the join key is sometimes free afterwards.
the optimiser picks between them using table statistics β cardinalities, histograms, index availability β which is why a stale ANALYZE can turn a millisecond plan into a minute-long one.
transactions and ACID
a transaction is a group of statements that must behave as one indivisible action:
- atomicity β all or nothing; crash mid-transfer and the money is in exactly one account. implemented with a write-ahead log and rollback.
- consistency β every commit moves the database between constraint-satisfying states.
- isolation β concurrent transactions cannot observe each other’s intermediate garbage.
- durability β once committed, survives power loss; the WAL is fsynced before “committed” is reported.
isolation levels and their anomalies
full serialisability costs concurrency, so SQL defines discounted tiers. what each level permits:
| level | dirty read | non-repeatable read | phantom |
|---|---|---|---|
| read uncommitted | possible | possible | possible |
| read committed | prevented | possible | possible |
| repeatable read | prevented | prevented | possible |
| serialisable | prevented | prevented | prevented |
- dirty read: seeing another transaction’s uncommitted write.
- non-repeatable read: re-reading a row and finding it changed under you.
- phantom: re-running a predicate query and finding new rows that now match.
the table is the standard’s contract, not a description of any real engine: postgres’s “repeatable read” is snapshot isolation, which also prevents phantoms but admits write skew β two transactions each reading the condition the other is about to falsify (two doctors both going off-call because “at least one other doctor is on call”). snapshot isolation passes the table above and still is not serialisable β a genuinely non-obvious fact, explained beautifully in kleppmann’s designing data-intensive applications (https://dataintensive.net/).
2PL vs MVCC
- two-phase locking: acquire shared/exclusive locks as you go (growing phase), release only at the end (shrinking phase β in practice, at commit: strict 2PL). guarantees serialisability, but readers block writers and vice versa, and deadlocks must be detected and a victim aborted.
- multi-version concurrency control: writers create new versions instead of overwriting; each transaction reads the snapshot that was current when it began. readers never block writers, which is why postgres, mysql/innodb, and oracle all live here. the honest price: garbage from dead versions (postgres’s vacuum), and plain snapshot isolation admits the anomalies above β postgres’s
SERIALIZABLElevel layers serialisation-conflict detection (SSI) on top and aborts one of the offenders, trading blocking for retries.
distribution: CAP and the NoSQL taxonomy
once data spans machines, the network becomes part of the database. the CAP theorem: under a network partition, a distributed store must sacrifice either consistency (every read sees the latest write) or availability (every request gets a non-error answer). the folk version β “pick two of three” β is misleading, because partition tolerance is not optional: partitions happen. the real content is the C-vs-A choice during the partition, and kleppmann argues even that framing hides more than it reveals (please stop calling databases CP or AP).
the “NoSQL” label bundles four genuinely different data models. sorting the stub’s shopping list into it:
| family | data model | exemplars | reach for it when |
|---|---|---|---|
| key-value | opaque blob per key | redis, dynamodb | caches, sessions, counters β access is only ever by key |
| document | nested JSON-ish trees | mongodb, couchdb | aggregates read/written whole, schema still settling |
| column-family | wide rows, sorted within partitions | cassandra, hbase | write-heavy append streams, time-series at scale |
| graph | nodes + edges, first-class | neo4j | queries that are mostly multi-hop relationship traversal |
postgres and mysql are the relational workhorses the rest get compared against β and postgres’s JSONB quietly covers a large slice of the document use-case with transactions included. as for QRKDB, the last name on the stub’s list: i can find no trace of it anywhere, including in my own notes, so it stays here as a monument to writing down context, not just names.
π
the deeper divide than SQL-vs-NoSQL is the consistency model: single-node transactions are a solved problem; cross-partition transactions are where the distributed-systems dragons live.
see also
- internet networks β the layer your database listens on
- linux β fsync, pages, and the kernel under the WAL
- data structures & algorithms β B-trees and hashing in their natural habitat
- memory β the cache hierarchy that makes page-sized thinking mandatory
References
Silberschatz, Abraham and Korth, Henry F. and Sudarshan, S. (2019). Database System Concepts, McGraw-Hill Education.
Backlinks (5)
1. Event Driven /wiki/se/architecture-design/event-driven/
event-driven architecture inverts the direction of knowledge: instead of the caller knowing who must react, the reactor knows what it cares about. producers announce facts; consumers subscribe. the whole style is the observer pattern with a broker in the middle and a network underneath β which is exactly where the interesting failure modes come from.
events, commands, queries
three message species, constantly confused:
- event: a fact about the past, named in past tense β
OrderPlaced,PaymentFailed. immutable, owned by the producer, zero expectation about who (if anyone) reacts. broadcasting is safe because nothing is being asked. - command: a request for the future β
PlaceOrder. imperative mood, addressed to exactly one handler, can be rejected. commands express intent; events record outcome. compare the command pattern, which reifies exactly this. - query: a question, side-effect free, wants an answer now.
the classic smell is the command in event’s clothing: SendWelcomeEmailRequested published as an “event” that exactly one consumer must process, or the producer silently depends on a specific reaction β you have built rpc with extra steps and none of rpc’s error handling.
π
2. Internet Networks /wiki/ccs/networking/
the internet is a triumph of indirection: no layer trusts the one below to be reliable, timely, or even present, and yet a packet leaves your laptop, crosses a dozen autonomous systems owned by companies that actively dislike each other, and arrives. π this page walks the stack bottom-up, then follows one HTTP request through DNS, TCP, and TLS to see every layer earn its keep.
3. Wiki /wiki/
Knowledge is a paradox. The more one understand, the more one realises the vastness of his ignorance.
4. Linux /wiki/ccs/linux/
unix is less an operating system than a worldview: everything is a file, every program does one thing, and text streams are the universal interface. linux is the worldview’s most successful implementation β a monolithic kernel started by a finnish undergraduate in 1991, now running most of the internet, every android phone, and the top 500 supercomputers without exception. π this page is the trunk; the sharpened tools each get their own branch: