← Lessons Designing a database
0 / 5 units
中文
Lesson 4

How to design a database that stays correct

A database is where an app keeps its information. If it is designed badly, data starts contradicting itself, gets lost or becomes slow to find. In this lesson you break a badly designed database on purpose, then fix it.

Relational design is about integrity first and performance second: normalise to remove anomalies, model cardinality explicitly, index for your access paths, and use transactions to keep invariants true under failures and concurrency.

Unit 1

From one big spreadsheet to proper tables

A small shop keeps every order in one big sheet. It looks fine, but the same information is copied again and again (the orange cells). Use the buttons to see what goes wrong, then split it into tables.

A single denormalised table with repeating facts. Trigger the three classic anomalies (update, delete, insert), then normalise to 3NF and repeat the same operations.

about the customerdepends on customer about the productdepends on product about the order itselfdepends on the order key
Schema (PostgreSQL)
CREATE TABLE customers (
  id     bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name   text NOT NULL,
  email  text NOT NULL UNIQUE
);

CREATE TABLE products (
  id     bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name   text NOT NULL,
  price  numeric(10,2) NOT NULL CHECK (price >= 0)
);

CREATE TABLE orders (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  customer_id bigint NOT NULL REFERENCES customers(id),
  product_id  bigint NOT NULL REFERENCES products(id),
  unit_price  numeric(10,2) NOT NULL  -- price at purchase time, on purpose
);
The idea in one line: store each fact in exactly one place. Customers go in a customers table and products in a products table. An order just points to them by number (customer_id, product_id). These pointers are called foreign keys, and each row's own unique number is its primary key.
Normal forms in practice
  • 1NF: atomic values with no repeating groups (no products = "Mouse, Keyboard").
  • 2NF / 3NF: every non-key column depends on the key, the whole key, and nothing but the key. email depends on the customer, not on the order, so it moves out.
  • Deliberate denormalisation: orders.unit_price copies the price on purpose. It records a historical fact (what was charged), not the current price.
  • Denormalise for reads only with a clear owner of truth (materialised views, caches, or columns maintained by triggers).
Unit 2

Relationships: one or many?

Things in a database are connected. The key question is always "how many of these go with how many of those?" Pick the answer, and the diagram shows how the tables connect.

Choose the cardinality, and the diagram shows where the foreign keys live and which constraints enforce it. Advanced mode adds self-referencing relationships.

DDL
(answer to see the schema)
Unit 3

Indexes: finding things fast

A table with a million users. Without an index, the database must look at every row. An index works like the alphabet tabs in a phone book: it jumps almost straight to the right place.

A sequential scan is O(n) over the heap. A B-tree index lookup is O(log n) page reads, with a fan-out of hundreds, so only 3–4 levels even for very large tables. Indexes cost write amplification and storage.

Without an index

Reads every row, top to bottom.Seq Scan on users · Filter: email = … One square = 10,000 rows.
Rows read: 0

With an index on email

Follows the alphabet tabs down to the right page.Index Scan using users_email_idx · B-tree, 3 levels.
Pages read: 0
Press "Run the search" to start both searches together.

Can the database use the index for this search?

Composite index: which predicates can use it?

There is an index on email. Decide for each search.

CREATE INDEX ON people (last_name, first_name); Think of it as sorted by last name, then first name inside each last name.

Trade-off: every index makes searches faster, but each insert or update must also update the index, and the index takes up space. Add indexes for the searches your app actually runs often.
Rules of thumb: index foreign keys and your hot WHERE / ORDER BY paths. In composite indexes, put equality columns before range columns. Use partial indexes (WHERE deleted_at IS NULL) and covering indexes (INCLUDE) where they help. Check plans with EXPLAIN (ANALYZE, BUFFERS), and drop indexes that nothing reads.
Unit 4

Transactions: all or nothing

Alice sends Bob $100. That's two changes: take $100 from Alice, then give $100 to Bob. What if the server crashes between them?

A transfer is two writes that must preserve one invariant: the total balance stays constant. Inject a crash between the writes, with and without a transaction.

Alice
$500
Bob
$200
Try the crash with the transaction switched off first, then switched on.
AAll or nothing: every step happens, or none do.
CRules stay true, such as "money is never created or lost".
ITwo people at once don't mess each other up.
DOnce it says "done", it survives a power cut.
AAtomicity: undo log / MVCC; ROLLBACK on failure.
CConsistency: constraints, FKs, CHECKs hold at commit.
IIsolation: the level chosen trades anomalies for concurrency.
DDurability: WAL is fsynced before COMMIT returns.

Concurrency: two withdrawals at once

Two sessions each withdraw $100 from Alice's $500 at the same moment. The correct result is $300.

Unit 5

Check yourself