Primary keys, unique constraints, and foreign keys are enforced using indexes under the hood — they aren’t just logical constraints, they’re physical structures with real performance implications. This file covers what each one actually builds, and where their behavior diverges from a plain secondary index.
A customers table with no index on email allows two customers to
register with the same email — a data integrity bug that surfaces as
duplicate accounts, split order histories, and support tickets. A unique
index on email prevents the bug at the database layer, not just in
application code (which can be bypassed by a second write path, a script,
or a race condition).
Constraints need enforcement mechanisms. A primary key constraint requires the database to check every insert against every existing value for uniqueness (and reject NULLs) — the only structure that makes that check fast is an index. The same logic applies to unique constraints. Foreign keys require checking that a referenced value exists in the parent table on every insert/update — again, expensive without an index.
order_id — the canonical row identifier.customers.email, users.username — enforce one row per
real-world entity.orders.customer_id → customers.id — enforce referential
integrity and speed up join queries.UNIQUE(tenant_id, email) — one email
per tenant, not globally unique.-- Primary key (also creates the clustered index in InnoDB)
CREATE TABLE customers (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
UNIQUE KEY uq_customers_email (email)
);
-- Foreign key — MySQL auto-creates a supporting index if absent
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers (id);
PRIMARY KEY on id both enforces uniqueness/non-null and determines
physical row storage order in InnoDB.UNIQUE KEY uq_customers_email (email) creates a secondary unique
index — distinct from the clustered primary key index.FOREIGN KEY clause enforces referential integrity; MySQL will
silently create customer_id an index if no compatible one exists,
since InnoDB requires it for the constraint check.customers (InnoDB clustered on id):
id=1 → { email: 'a@x.com', ... } (full row stored at leaf)
id=2 → { email: 'b@x.com', ... }
uq_customers_email (secondary unique index):
'a@x.com' → id=1
'b@x.com' → id=2
(lookup by email → get id → second lookup into clustered index)
Write: INSERT INTO orders (customer_id, ...) VALUES (999, ...)
│
┌────────────┴────────────┐
│ FK constraint check: │
│ does customers.id = 999 │
│ exist? │
└────────────┬────────────┘
Indexed lookup on customers.id
(fast — O(log n), not O(n))
INSERT/UPDATE of a unique-constrained column, the engine seeks
the existing index for the new value before committing — a match
raises a duplicate-key error.INSERT/UPDATE of a foreign-key column, the engine seeks the
parent table’s primary/unique index for the referenced value — a miss
raises a constraint violation.A foreign key without a supporting index on the child table’s column (the referencing column, not the referenced one) still enforces correctness but does so by scanning — MySQL InnoDB prevents this specific case by auto-creating an index, but not every engine or every constraint type does this automatically. Always verify.
In InnoDB, the clustered primary key index is the table — there’s no additional storage cost beyond the table itself. Every secondary index (including unique ones) is additional storage, sized by however many columns it covers.
Foreign key columns are frequently join columns — the same index that
supports the constraint check also accelerates
JOIN customers ON orders.customer_id = customers.id, which is why
“index your foreign keys” is close to a universal rule, not just a
constraint-performance detail.
PRIMARY KEY, UNIQUE, and FOREIGN KEY are all ANSI SQL standard
constraint declarations — the standard mandates their logical behavior
but not how they’re implemented internally.
PRIMARY KEY to exist for optimal storage; a table
without an explicit primary key gets an invisible auto-generated
clustering key, which loses the benefit of clustering on a
business-relevant column.PRIMARY KEY in SQL Server defaults to creating a clustered
index unless NONCLUSTERED is explicitly specified.UNIQUE constraint allows multiple NULLs.UNIQUE column contain more than one NULL value? Why?Primary keys, unique constraints, and foreign keys are all enforced via indexes, not free-standing logical rules. In InnoDB, the primary key defines physical row storage; unique constraints are index-backed uniqueness checks; foreign keys require an index on the referencing column to avoid full scans on every write and delete — a requirement MySQL enforces automatically but PostgreSQL, SQL Server, and Oracle do not.
See 12_PRACTICE_PROBLEMS.md, Intermediate section, Problems 8–9.