Keys in SQL
A key is a uniqueness promise that the engine physically enforces: when you declare one, the database builds a unique index over those columns, and every INSERT/UPDATE probes that index before committing the row — so the cost of a key is not just "no duplicates," it is a B-tree the engine must keep sorted and search on every write. Everything else (primary, foreign, composite, candidate) is a role layered on top of that one mechanism.
The hierarchy: superkey → candidate → primary
The taxonomy comes from functional dependency, not from syntax. Start with a real employee instance and ask which column-sets can identify a row.
| id | ssn | dept_id | name | |
|---|---|---|---|---|
| 1 | ava@co.com | 111-22-3333 | 10 | Ava |
| 2 | ben@co.com | 444-55-6666 | 10 | Ben |
| 3 | cole@co.com | 777-88-9999 | 20 | Cole |
- Superkey = any column-set whose values are unique across all rows.
{id},{email},{ssn},{id, email},{id, name}are all superkeys — including the bloated ones carrying extra baggage. - Candidate key = a superkey with no removable column (minimal/irreducible).
{id},{email},{ssn}are candidates;{id, email}is not — dropemailand{id}still identifies rows, soemailwas dead weight. - Primary key = the one candidate you elect to be the row's official identity. The rest are alternate keys, enforced with
UNIQUE.
So every primary key is a candidate key is a superkey — but not the reverse. The grader's note flagged this missing layer: "candidate key" only means something relative to "superkey."
DDL: declaring each key (and what the engine does)
Correct, compilable DDL. A composite primary key, two alternate keys, and a foreign key with an explicit referential action.
-- Parent table
CREATE TABLE department (
dept_id INT PRIMARY KEY,
name VARCHAR(60) NOT NULL
);
-- Child table
CREATE TABLE employee (
id INT GENERATED ALWAYS AS IDENTITY, -- surrogate PK
email VARCHAR(120) NOT NULL,
ssn CHAR(11),
dept_id INT NOT NULL,
CONSTRAINT pk_employee PRIMARY KEY (id), -- elected candidate
CONSTRAINT uq_email UNIQUE (email), -- alternate key, NOT NULL here
CONSTRAINT uq_ssn UNIQUE (ssn), -- alternate key, NULLs allowed
CONSTRAINT fk_dept FOREIGN KEY (dept_id)
REFERENCES department (dept_id)
ON DELETE RESTRICT
ON UPDATE CASCADE
);
-- Composite (multi-column) key on a junction table:
CREATE TABLE employee_project (
emp_id INT REFERENCES employee (id),
project_id INT,
role VARCHAR(40),
PRIMARY KEY (emp_id, project_id) -- the PAIR must be unique, not each column
);What the engine actually creates: PRIMARY KEY builds a unique index and adds an implicit NOT NULL; each UNIQUE builds its own unique index; the FOREIGN KEY creates no index on the child by default — it only validates that dept_id exists in the parent's PK index on each write.
Worked trace: a write touches every key
Run this against the schema above and follow the engine. department holds (10,'Eng') and (20,'Sales'); employee already holds rows 1–3 from the table earlier.
INSERT INTO employee(email, ssn, dept_id) VALUES ('dave@co.com','111-22-3333', 10)- Engine assigns
id = 4from the identity sequence; probespk_employeeindex for4→ absent → OK. - Probes
uq_emailindex for'dave@co.com'→ absent → OK. - Probes
uq_ssnindex for'111-22-3333'→ found on row 1 (Ava) → violation. The whole statement aborts:ERROR: duplicate key value violates unique constraint "uq_ssn". No partial row is written. - Fix the SSN and retry:
... VALUES ('dave@co.com','000-00-0001', 10). Now the FK fires: probedepartmentPK index fordept_id = 10→ found → OK. Row 4 commits. - Later:
DELETE FROM department WHERE dept_id = 10. TheON DELETE RESTRICTaction scans for children referencing 10, finds rows 1, 2, 4 → blocks the delete. WithON DELETE CASCADEinstead, those three employee rows would be deleted with the parent.
The takeaway from the trace: a single insert did three index probes plus a FK lookup. That is the real cost of a richly-keyed table — and the reason key choice is a performance decision, not just a correctness one.
Referential actions: what happens to children when the parent changes
The ON DELETE / ON UPDATE clause is the part of foreign keys the glossary version omitted, and it is where most production incidents live.
| Action | On parent DELETE / UPDATE | Use when |
|---|---|---|
RESTRICT / NO ACTION | Reject the operation if any child references the row | Default, safest: you want to be forced to deal with orphans explicitly |
CASCADE | Delete/update the child rows too | Child is owned by parent (order → order_items); deleting the order should delete its lines |
SET NULL | Set the child's FK column to NULL (column must be nullable) | Child can outlive parent (employee → manager who quits) |
SET DEFAULT | Set the FK to its column default | Rare; the default value must itself exist in the parent |
NO ACTION vs RESTRICT: both reject, but NO ACTION defers the check to end-of-statement (so within one statement a transient violation can heal), while RESTRICT rejects immediately. In most cases the behaviour is identical.
Pitfalls
- Treating
UNIQUEas a duplicate-blocker for NULLs. In standard SQL and Postgres,NULLis never equal toNULL, so aUNIQUEcolumn accepts many NULL rows. Your "unique" email column will happily store 50 rows withemail IS NULL. Postgres 15+ offersUNIQUE NULLS NOT DISTINCTto opt out; older engines need a partial index or a sentinel value. A primary key sidesteps this entirely because it forbids NULL. - Forgetting that foreign keys do not index the child.
ON DELETE CASCADEwith an unindexed FK column turns every parent delete into a full table scan of the child to find rows to cascade — fine on 1k rows, a multi-second lock on 50M. AddCREATE INDEX ON employee(dept_id);yourself. - Composite-key column order.
PRIMARY KEY (emp_id, project_id)builds one index sorted byemp_idfirst. A query filtering only onproject_idcannot use it (it's a non-leading column) — same rule as any composite B-tree. Order the columns by how you query, most-selective leading column that you filter on first. - Natural keys that aren't actually stable. Choosing
emailorssnas the primary key means every place that references it stores that value, and a person changing email forces a cascading update across the whole schema. This is why surrogate keys (IDENTITY/SERIAL/UUID) are the default for primary keys, with natural keys kept asUNIQUEalternates. - Assuming the PK is auto-clustered everywhere. In MySQL/InnoDB the primary key is the clustered index (rows are physically stored in PK order), so a random UUID PK scatters writes and bloats secondary indexes. In Postgres the heap is unordered and the PK is just another index — a UUID PK is far less painful. Same declaration, very different physical behaviour.
Takeaways
- One mechanism underlies all keys: a unique index the engine probes on every write. "Primary," "unique," "foreign," "composite" are roles on top of it.
- Superkey ⊃ candidate ⊃ primary. Candidate = minimal superkey; primary = the elected candidate; the rest become
UNIQUEalternate keys. UNIQUEallows multiple NULLs (NULL ≠ NULL); a primary key allows none. That single difference is the most common real bug.- Foreign keys cost a parent-index probe per write and need a manual index on the child for
CASCADEto be cheap; choose theON DELETEaction deliberately.
Sources: ISO/IEC 9075 (SQL standard) definitions of candidate, primary, and foreign keys; C. J. Date, An Introduction to Database Systems (superkey/candidate-key minimality); Silberschatz, Korth & Sudarshan, Database System Concepts (keys and referential integrity); PostgreSQL 16 docs (CREATE TABLE constraints, NULLS NOT DISTINCT, referential actions); MySQL 8.0 Reference Manual (InnoDB clustered index, foreign-key indexing). Re-authored and deepened for this guide: replaced the definition-only glossary with the superkey→candidate→primary hierarchy, compilable DDL, a traced multi-index write, referential actions, and engine-level pitfalls.
🎯 STRICT STANDOUT: Why / worked / when-not / failure / drills — Keys in SQL
Why this concept exists (judgment chain)
Every key is a uniqueness promise backed by a unique index probed on write. Superkey ⊃ candidate ⊃ primary is FD theory; FK actions and UNIQUE NULL semantics are where production integrity fails.
Worked example with numbers or traced steps
employee candidates: {id}, {email}, {ssn}; elect id as PK; UNIQUE email/ssn.
INSERT duplicate ssn → unique index probe fails; whole statement aborts.
FK ON DELETE RESTRICT blocks DELETE department with children;
CASCADE would delete employees; SET NULL needs nullable FK.
UNIQUE(email) allows many NULL emails (NULL≠NULL) unless NULLS NOT DISTINCT.
Child FK unindexed → CASCADE parent delete sequential-scans child.
When NOT to use / named alternative
Do not use natural email/SSN as PK if values change — surrogate PK + UNIQUE alternate. Do not CASCADE delete from shared lookup tables. Avoid UUID PKs as InnoDB clustered keys without sequential alternative (random insert scatter). Skip redundant UNIQUE that duplicates PK.
Failure / ops fingerprint
Fingerprint: 50 NULL “unique” emails; DELETE parent hangs on 50M child without FK index; cascade wipe of orders. Ops: always CREATE INDEX on FK columns; choose ON DELETE deliberately; monitor constraint violation rates.
Hostile-panel drills (defend the decision)
Q1. Superkey vs candidate vs primary?
Model answer: Superkey = any unique column set; candidate = minimal superkey; primary = elected candidate; others → UNIQUE alternates.
Q2. Does UNIQUE ban multiple NULLs?
Model answer: Standard/PG: no — NULL≠NULL. Use NULLS NOT DISTINCT (PG15+) or partial unique index.
Q3. Cost of one INSERT with PK+2 UNIQUE+FK?
Model answer: Probe each unique index + parent PK for FK — multi-index write path; keys are a performance decision.
🤖 Don't fully get this? Learn it with Claude
Stuck on Keys in SQL? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Keys in SQL** (Databases) and want to truly understand it. Explain Keys in SQL from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Keys in SQL** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Keys in SQL** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Keys in SQL** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.