Primary Keys: Naming Every Row
Two customers named Anna Kim walk into the Marquee in the same month, and Lora's notebook quietly falls apart: a booking says "Anna Kim, seat G7" — which Anna? The data is all there, and it still can't answer. Databases solve this with a primary key: one column whose value uniquely names each row, forever. Give each Anna her own number, make bookings mention the number instead of the name, and the ambiguity is gone for good.
The primary key is the first rule the reader adds to a table — Chapter 1 called them constraints in passing, and this is the flagship. It looks like a small bookkeeping habit. It is actually the foundation the entire relational model stands on: the next two topics, and most of the rest of the book, hang off rows having reliable names.
Why Names and Emails Fail
The tempting move is to pick something the customer already has. Name? Two Anna Kims just demonstrated the problem — real-world attributes collide. Email, then — emails are unique, surely? Mostly, today. But emails change: Anna switches providers, and now every booking pointing at her old address points at nobody — or worse, at whoever registers that address next year. A key must be two things at once: unique (no two rows share it) and stable (it never changes for the life of the row). Real-world attributes keep failing one test or the other.
So databases mint an artificial one: customer_id, a number with no meaning outside the table. Customer 214 is customer 214 because the database said so on the day her row was created, and nothing about her life — name, email, address — can ever unsettle it. Think of membership card numbers: two Anna Kims hold two different card numbers, and the desk asks for the card, not the name. Mapped; from here on, the real term is primary key.
Declaring It
Here is the Marquee's third table, customers, with its key declared:
CREATE TABLE customers ( customer_id INTEGER PRIMARY KEY, name TEXT, email TEXT UNIQUE, joined_on DATE );
Two new words. PRIMARY KEY on customer_id tells the database: this column names the rows — every row must have a value here, and no two rows may share one. UNIQUE on email is a lighter promise: no two customers may register the same address, but email is not the row's name — it stays a changeable fact about Anna, merely one that can't be duplicated. Unique-but-not-the-key is exactly where emails belong.
And the database enforces the key the way it enforced types on the last page. Watch:
INSERT INTO customers (customer_id, name, email, joined_on) VALUES (214, 'Anna Kim', 'anna.k@example.com', '2026-05-02'); INSERT INTO customers (customer_id, name, email, joined_on) VALUES (214, 'Ben Ferro', 'ben.f@example.com', '2026-05-03');
The first statement stores the first row without complaint. The second tries to create another customer with customer_id 214 — and the database refuses it: 214 is taken, and a name shared by two rows names neither. The whole statement fails, nothing is stored, and the table keeps exactly one customer 214. This is "the database says no" again, now guarding identity instead of types.
Who Hands Out the Numbers?
Not you — and that is by design. In practice nobody types 214 into an INSERT; you leave the id out, and the engine mints the next free number itself. Ask for a new customer row, get back "that's customer 517," and move on. Automatic numbering removes the last human hands from the one column where a collision would be fatal.
One honest sentence on naming: the engines agree completely on the idea and not at all on the spelling — PostgreSQL calls it SERIAL (or the standard GENERATED … AS IDENTITY), SQLite calls it AUTOINCREMENT, and SQL Server calls it IDENTITY. Same feature, three names; when you meet any of them in a real schema, read them all as "the engine numbers this column."
What auto-numbering promises is only this: each new row gets a number no existing row has. It does not promise the numbers are consecutive — a refused INSERT can burn a number, leaving gaps — and you should never read chronology or importance into them. Which brings us to the last idea.
Keys Are Boring on Purpose
Customer 214 is not the 214th customer, not a VIP, not anything. The id carries no meaning — and that is its entire virtue. Meaningful things change: names get corrected, emails get replaced, addresses move. Meaningless things never need to. A key with zero content is a key with zero reasons to ever be updated, and "never changes" was half the job description.
So resist any urge toward clever keys — initials plus birth year, say, which collides and leaks personal data. The professional default is the boring one: <thing>_id INTEGER PRIMARY KEY, numbered by the engine. Every table in the Marquee schema follows it — film_id, screening_id, customer_id, booking_id — and nearly every schema you meet at work will too. Boring is not the beginner version; boring is the craft.
- "Just use the email as the id." Emails change and get re-registered by other people — and a key must never change. UNIQUE on email is right (no duplicates); PRIMARY KEY on email is a trap that springs the day Anna switches providers.
- "The id tells you something." It doesn't, and it mustn't. Customer 500 isn't guaranteed to be the 500th to join, isn't more important, isn't anything — ids are names, not data. Meaning is what the other columns are for.
- "Every table deserves a clever, memorable key." Every table deserves a boring key.
<thing>_id INTEGER, minted by the engine, is the professional default across the industry — not a beginner crutch you'll outgrow. - "Auto-numbering means no gaps." It only promises no repeats. Refused inserts can burn numbers, so 214 may be followed by 216 — harmless, normal, and no reason to "fix" anything.
- Foreign keys — the very next page — point at primary keys. No reliable row names, no relationships, no relational model: this page is load-bearing for everything after it.
- "Unique and stable" is a design instinct that transfers everywhere — account systems, file naming, order numbers. You will recognize good and bad identifiers for the rest of your career.
Knowledge Check
Why is a customer's email a poor choice of primary key, even though emails are (mostly) unique?
- Because a primary key must be a number, not text
- Because emails change and get re-registered — a key must never change
- Because emails are too long to compare quickly
- Because several customers in one household might share a single family email address
What exactly does declaring PRIMARY KEY on customer_id make the database refuse?
- Two customers with the same name
- A row that duplicates an existing customer_id, or has none at all
- Two customers registering the same email address
- Deleting a customer whose row still has bookings pointing at it from other tables
The engine auto-numbers customer_id. Which of these does that actually promise?
- The ids are consecutive, with no gaps
- A lower id always means an earlier signup
- Each new row receives an id no existing row has
- The same keyword works identically on every engine
Why is a meaningless id the best kind of key?
- Because nothing about the customer's life can ever force it to change
- Because plain numbers are faster for the database to search
- Because it hides customers' personal information from staff
- Because meaningless ids are never shown to anyone, so mistakes don't matter
You got correct