SQL-Engineering-Handbook

04 — Unique, Primary & Foreign Key Indexes

Introduction

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.

Learning Objectives

Business Motivation

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).

Why This Exists

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.

Production Use Cases

Architecture Discussion

Production Use Cases (continued)

Syntax

-- 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);

Syntax Breakdown

Visual Explanation

Clustered vs. non-clustered index

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)

ASCII Diagram

        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))

Execution Flow

  1. On 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.
  2. On 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.
  3. Both checks are index seeks, not scans, provided the required index exists.

Engineering Notes

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.

Performance Notes

Storage Considerations

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.

Optimizer Notes

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.

ANSI SQL Notes

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.

MySQL Notes

PostgreSQL Notes

SQL Server Notes

Oracle Notes

Edge Cases

Best Practices

Anti-patterns

Common Mistakes

Interview Questions

  1. In InnoDB, what physically is the primary key index?
  2. Why does a foreign key without a supporting index cause slow deletes on the parent table?
  3. Does PostgreSQL auto-create an index for a foreign key’s referencing column? What are the operational implications if you forget?
  4. Can a UNIQUE column contain more than one NULL value? Why?

Summary

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.

Practice

See 12_PRACTICE_PROBLEMS.md, Intermediate section, Problems 8–9.

Further Reading

See resources/documentation.md.