Index ยท How it works

2 min read
18 min read
Rapid overview

How it works

The relational model

  • Table (relation) โ€” a set of rows. Row (tuple) โ€” one record. Column (attribute) โ€” a typed field.
  • Primary key (PK) โ€” uniquely identifies a row; not null, unique. Often a surrogate id (auto-increment / bigint / uuid) rather than a natural key.
  • Foreign key (FK) โ€” a column that references another table's PK; the database enforces referential integrity (you can't reference a row that doesn't exist, and ON DELETE decides what happens to children).
  • Constraints โ€” NOT NULL, UNIQUE, CHECK, DEFAULT โ€” push invariants into the database so bad data can't get in regardless of which app writes it.
  • Schema โ€” the set of table/column/constraint definitions (DDL). Changing it = a migration.

ACID โ€” what a transaction guarantees

A transaction groups statements so they succeed or fail as a unit (BEGIN โ€ฆ COMMIT / ROLLBACK).

  • Atomicity โ€” all-or-nothing; a failure rolls the whole thing back.
  • Consistency โ€” constraints hold before and after; the DB never persists a state that violates them.
  • Isolation โ€” concurrent transactions don't step on each other (tunable, see isolation levels below).
  • Durability โ€” once committed, it survives a crash (write-ahead log / fsync).

The classic example: transferring money โ€” debit one account and credit another must both happen or neither.

SQL in one breath

-- DDL (structure)
CREATE TABLE orders (
  id          bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  customer_id bigint NOT NULL REFERENCES customers(id),
  total       numeric(10,2) NOT NULL CHECK (total >= 0),
  status      text NOT NULL DEFAULT 'pending',
  created_at  timestamptz NOT NULL DEFAULT now()
);

-- DML (data)
SELECT c.name, COUNT(*) AS order_count, SUM(o.total) AS revenue
FROM   customers c
JOIN   orders o ON o.customer_id = c.id
WHERE  o.created_at >= '2026-01-01'
GROUP  BY c.name
HAVING SUM(o.total) > 1000
ORDER  BY revenue DESC
LIMIT  20;

Logical evaluation order is not the written order โ€” it's roughly FROM โ†’ JOIN โ†’ WHERE โ†’ GROUP BY โ†’ HAVING โ†’ SELECT โ†’ ORDER BY โ†’ LIMIT. That's why you can't use a SELECT alias in WHERE (the alias doesn't exist yet) but you can in ORDER BY.

Joins

  • INNER JOIN โ€” only rows matching in both tables.
  • LEFT (OUTER) JOIN โ€” all left rows; right side is NULL when there's no match (use to find "customers with no orders": LEFT JOIN โ€ฆ WHERE o.id IS NULL).
  • CROSS JOIN โ€” Cartesian product. Usually accidental (a forgotten join condition) and a performance disaster โ€” n ร— m rows.

Normalization vs denormalization

Normalization removes redundancy so each fact lives in exactly one place:

  • 1NF โ€” atomic columns, no repeating groups (no comma-separated lists in a cell).
  • 2NF โ€” no partial dependency on part of a composite key.
  • 3NF โ€” no transitive dependency (non-key columns depend only on the key, "the key, the whole key, and nothing but the key").

Normalized schemas avoid update anomalies (change a customer's name once, not in 10 000 order rows). Denormalization deliberately re-introduces redundancy (a cached order_count, a duplicated column) to avoid expensive joins on read-heavy paths โ€” at the cost of having to keep the copies in sync. Normalize first; denormalize as a targeted optimization with evidence.

See also