Senior 6 min · March 05, 2026

Primary Key & Foreign Key — Index Took Down Checkout

A missing foreign key index caused 30-second orders and 100% CPU—read how this silent killer nearly took down checkout and how to prevent it.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • A primary key uniquely identifies each row in a table and must be unique and non-null.
  • A foreign key links rows across tables by storing the parent's primary key value.
  • Primary keys are always unique; foreign keys can have duplicates and NULLs.
  • Missing indexes on foreign key columns cause full table scans on JOINs.
  • ON DELETE RESTRICT protects data integrity; ON DELETE CASCADE can be dangerous.
  • Biggest mistake: using a mutable real-world value (like email) as primary key.
Plain-English First

Imagine a school library. Every book has a unique barcode — no two books share the same one. That barcode is the primary key: a one-of-a-kind label that identifies exactly one record. Now imagine a student borrows a book. The checkout slip has the student's ID on it — not the student's full name, address, and photo, just the ID. That ID pointing back to the student record is the foreign key. It's how two separate lists stay connected without duplicating all the data.

Every app you've ever used — Instagram, Spotify, your banking app — stores its data in a database. Users, posts, songs, transactions: all of it lives in tables. But here's the thing: data almost never exists in isolation. A post belongs to a user. A transaction belongs to an account. An order belongs to a customer. The entire system only works because there's a mechanism that links these tables together reliably and without chaos. That mechanism starts with primary keys and foreign keys.

Without them, you'd face a nightmare. Imagine storing a customer's name, address, and phone number on every single order they ever placed. When they move house, you'd have to update hundreds of rows. Miss one? Now your database is lying to you. Or imagine deleting a customer record but leaving all their orphaned orders sitting in the database pointing to nobody. Primary keys and foreign keys exist specifically to prevent these disasters — they enforce identity and relationships at the database level, so your data stays truthful.

By the end of this article you'll understand what a primary key is and why every table needs one, what a foreign key is and how it stitches tables together, how to write the SQL to create both from scratch, the classic mistakes beginners make and exactly how to fix them, and the interview questions that trip people up. You'll walk away ready to design a real, properly connected database.

What Is a Primary Key — And Why Every Table Needs One

A primary key is a column (or combination of columns) in a database table whose value uniquely identifies every single row. Think of it like a passport number. Two people can share the same name, birthday, even the same hometown — but no two people share a passport number. That uniqueness is exactly what a primary key guarantees.

Why does this matter so much? Because databases need a reliable way to say 'I mean THIS exact row, not any other.' When you want to update a customer's email address, you can't search by name — what if two customers are both called 'James Brown'? You need one column that is guaranteed to be different for every row. That's your primary key.

A valid primary key follows three rules: it must be unique across all rows, it must never be NULL (empty), and it should never change once assigned. That last rule is important. If you use someone's email address as a primary key and they change their email, every reference to that key across your whole database breaks. This is why databases typically use a simple auto-incrementing integer — like 1, 2, 3, 4 — as the primary key. It's meaningless outside the database, which is exactly what you want: stable, dumb, permanent.

create_customers_table.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- Create a customers table with a proper primary key
-- AUTO_INCREMENT means the database assigns the next number automatically
-- You never insert a value for customer_id yourself
CREATE TABLE customers (
    customer_id   INT           NOT NULL AUTO_INCREMENT,  -- the primary key column
    full_name     VARCHAR(100)  NOT NULL,                 -- customer's full name
    email_address VARCHAR(150)  NOT NULL,                 -- must be provided
    join_date     DATE          NOT NULL,                 -- when they signed up

    -- This line officially declares customer_id as the primary key
    PRIMARY KEY (customer_id)
);

-- Insert three customers — notice we do NOT provide customer_id
-- The database fills it in for us: 1, 2, 3
INSERT INTO customers (full_name, email_address, join_date)
VALUES
    ('Alice Mercer',  'alice@example.com',  '2023-01-15'),
    ('James Brown',   'jbrown@example.com', '2023-03-22'),
    ('James Brown',   'jb2@example.com',    '2024-07-01');  -- same name, different person

-- Retrieve all customers to see the auto-assigned IDs
SELECT customer_id, full_name, email_address FROM customers;

-- Try inserting a duplicate primary key to see the protection in action
-- This will FAIL — the database will reject it
INSERT INTO customers (customer_id, full_name, email_address, join_date)
VALUES (1, 'Hacker Person', 'hack@example.com', '2024-01-01');
-- ERROR: Duplicate entry '1' for key 'PRIMARY'
Output
-- Result of SELECT:
+-------------+-------------+-------------------+
| customer_id | full_name | email_address |
+-------------+-------------+-------------------+
| 1 | Alice Mercer | alice@example.com |
| 2 | James Brown | jbrown@example.com|
| 3 | James Brown | jb2@example.com |
+-------------+-------------+-------------------+
3 rows in set
-- Result of duplicate INSERT attempt:
ERROR 1062 (23000): Duplicate entry '1' for key 'customers.PRIMARY'
Pro Tip: Never Use Real-World Data as a Primary Key
It's tempting to use an email address or phone number as a primary key because they seem unique. Don't. People change emails. Phone numbers get recycled. Use a meaningless auto-incrementing integer (INT AUTO_INCREMENT in MySQL, SERIAL in PostgreSQL) — it's stable, fast to index, and has no real-world baggage.
Production Insight
Auto-increment primary keys can overflow if not sized correctly. An INT (4 bytes) maxes out at 2,147,483,647. For high-traffic tables, use BIGINT to avoid production outages.
Monitoring the sequence high-water mark avoids surprise primary key exhaustion.
Rule: choose data type based on growth, not yesterday's volume.
Key Takeaway
A primary key must be unique, non-null, and stable.
Use surrogate auto-incrementing integers instead of natural keys.
The primary key is the table's identity; never change it.

What Is a Foreign Key — The Bridge Between Tables

Once every table has its own primary key, you need a way to say 'this row in Table A belongs to that row in Table B.' That's exactly what a foreign key does. A foreign key is a column in one table that stores the primary key value from another table. It's the bridge.

Back to the library analogy: when a student borrows a book, the checkout record doesn't copy out the student's full name, address, grade, and teacher. It just stores the student's ID number. That ID number is a foreign key — it points back to the full student record. To get the student's name, you follow the pointer.

This design is called normalization, and it's a fundamental principle of good database design. Store each piece of information in exactly one place. When Alice Mercer moves house, you update her address in one row of the customers table. Every order, every message, every record that references her customer_id automatically reflects the new address. No hunting, no inconsistency.

The foreign key also acts as a guardian. By default, a database with a foreign key constraint will refuse to let you insert an order for a customer_id that doesn't exist in the customers table. It also prevents you from deleting a customer who still has active orders. This is called referential integrity — the database enforces that your references always point to something real.

create_orders_table_with_foreign_key.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
-- We already have a customers table with customer_id as the primary key.
-- Now we create an orders table that links to it via a foreign key.

CREATE TABLE orders (
    order_id      INT            NOT NULL AUTO_INCREMENT,  -- primary key for this table
    customer_id   INT            NOT NULL,                  -- foreign key: references customers
    order_date    DATE           NOT NULL,
    total_amount  DECIMAL(10,2)  NOT NULL,
    order_status  VARCHAR(20)    NOT NULL DEFAULT 'pending',

    -- Declare order_id as this table's own primary key
    PRIMARY KEY (order_id),

    -- Declare the foreign key relationship
    -- customer_id in THIS table must match a customer_id in the customers table
    CONSTRAINT fk_orders_customer
        FOREIGN KEY (customer_id)
        REFERENCES customers (customer_id)
        ON DELETE RESTRICT   -- prevent deleting a customer who has orders
        ON UPDATE CASCADE    -- if customer_id changes, update it here too
);

-- Insert a valid order for Alice (customer_id = 1) — this works
INSERT INTO orders (customer_id, order_date, total_amount)
VALUES (1, '2024-02-10', 59.99);

-- Insert another valid order for James Brown (customer_id = 2) — this works
INSERT INTO orders (customer_id, order_date, total_amount)
VALUES (2, '2024-03-05', 120.00);

-- Try to insert an order for customer_id = 999 — does not exist in customers
-- The foreign key constraint will BLOCK this
INSERT INTO orders (customer_id, order_date, total_amount)
VALUES (999, '2024-04-01', 45.00);
-- ERROR: Cannot add or update a child row: foreign key constraint fails

-- Join the two tables to see orders with the customer's name
-- This is the power of the foreign key — we can reunite the data
SELECT
    o.order_id,
    c.full_name       AS customer_name,
    o.order_date,
    o.total_amount,
    o.order_status
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;  -- follow the foreign key bridge
Output
-- Result of JOIN query:
+----------+---------------+------------+--------------+--------------+
| order_id | customer_name | order_date | total_amount | order_status |
+----------+---------------+------------+--------------+--------------+
| 1 | Alice Mercer | 2024-02-10 | 59.99 | pending |
| 2 | James Brown | 2024-03-05 | 120.00 | pending |
+----------+---------------+------------+--------------+--------------+
2 rows in set
-- Result of invalid INSERT (customer 999 does not exist):
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails
(`mydb`.`orders`, CONSTRAINT `fk_orders_customer` FOREIGN KEY (`customer_id`)
REFERENCES `customers` (`customer_id`))
Watch Out: ON DELETE and ON UPDATE Actually Matter
Most tutorials skip ON DELETE and ON UPDATE, but they change everything. ON DELETE RESTRICT (the safest default) stops you deleting a parent row that has children. ON DELETE CASCADE automatically deletes all child rows when the parent is deleted — useful for things like deleting a user and all their messages, but dangerous if misapplied to financial records. Always think carefully about which behaviour you want before writing the constraint.
Production Insight
Foreign key constraints can cause deadlocks in high-concurrency environments. When multiple transactions insert child rows and delete parent rows, lock contention on the parent table escalates.
Use indexed foreign keys and keep transactions short.
Rule: always index foreign keys and consider lock ordering to prevent deadlocks.
Key Takeaway
A foreign key links tables without duplicating data.
It enforces referential integrity at the database level.
Always match data types between FK and PK columns.

Composite Keys, Natural Keys, and When to Break the Rules

So far we've used a single auto-incrementing integer as the primary key, and that covers 90% of real-world cases. But you'll occasionally encounter two variations worth knowing about.

A composite primary key uses two or more columns together to form the unique identifier. Imagine a table tracking which students are enrolled in which courses. A student can enrol in many courses, and a course can have many students — but a specific student can only be enrolled in a specific course once. So the combination of (student_id, course_id) is what must be unique. Neither column alone is the primary key; together they are.

A natural key uses a real-world piece of data that happens to be unique — like a national insurance number, ISBN, or vehicle registration plate. Some teams prefer these because they carry meaning. The risk, as mentioned earlier, is that real-world data can change or have edge cases (two editions of a book with the same ISBN is a real problem). A surrogate key — the auto-incremented integer — has no meaning outside the database and never changes, which is why most modern systems prefer it.

The rule of thumb: use a surrogate key by default. Add a UNIQUE constraint on natural data (like email addresses) if you need to enforce uniqueness there too. That way you get the stability of a surrogate key AND the uniqueness guarantee on the real-world field.

composite_key_and_unique_constraint.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
-- Example 1: Composite Primary Key
-- The enrolments table tracks student-course pairings
-- A student cannot enrol in the same course twice
CREATE TABLE enrolments (
    student_id  INT  NOT NULL,   -- foreign key pointing to students table
    course_id   INT  NOT NULL,   -- foreign key pointing to courses table
    enrol_date  DATE NOT NULL,
    grade       CHAR(2),         -- NULL until the course is completed

    -- The PRIMARY KEY is the COMBINATION of both columns
    -- Neither column alone is unique; the pair is
    PRIMARY KEY (student_id, course_id),

    CONSTRAINT fk_enrolment_student
        FOREIGN KEY (student_id) REFERENCES students (student_id),

    CONSTRAINT fk_enrolment_course
        FOREIGN KEY (course_id) REFERENCES courses (course_id)
);

-- This inserts fine — student 1 enrolling in course 10
INSERT INTO enrolments (student_id, course_id, enrol_date)
VALUES (1, 10, '2024-09-01');

-- This also inserts fine — student 1 enrolling in a DIFFERENT course
INSERT INTO enrolments (student_id, course_id, enrol_date)
VALUES (1, 11, '2024-09-01');

-- This FAILS — student 1 is already in course 10 (duplicate composite key)
INSERT INTO enrolments (student_id, course_id, enrol_date)
VALUES (1, 10, '2024-10-01');
-- ERROR: Duplicate entry '1-10' for key 'PRIMARY'

-- -------------------------------------------------------
-- Example 2: Surrogate key + UNIQUE constraint on email
-- The surrogate key (customer_id) is the real primary key
-- The UNIQUE constraint stops duplicate emails without making email the PK
CREATE TABLE members (
    member_id     INT          NOT NULL AUTO_INCREMENT,
    email_address VARCHAR(150) NOT NULL,
    full_name     VARCHAR(100) NOT NULL,

    PRIMARY KEY (member_id),

    -- Enforce email uniqueness separately — best of both worlds
    CONSTRAINT uq_members_email UNIQUE (email_address)
);

-- Valid insert
INSERT INTO members (email_address, full_name)
VALUES ('carol@example.com', 'Carol Danes');

-- This fails — email already exists
INSERT INTO members (email_address, full_name)
VALUES ('carol@example.com', 'Carol Smith');
-- ERROR: Duplicate entry 'carol@example.com' for key 'uq_members_email'
Output
-- After the two valid enrolment inserts:
+------------+-----------+------------+-------+
| student_id | course_id | enrol_date | grade |
+------------+-----------+------------+-------+
| 1 | 10 | 2024-09-01 | NULL |
| 1 | 11 | 2024-09-01 | NULL |
+------------+-----------+------------+-------+
-- Duplicate composite key error:
ERROR 1062 (23000): Duplicate entry '1-10' for key 'enrolments.PRIMARY'
-- Duplicate email error:
ERROR 1062 (23000): Duplicate entry 'carol@example.com' for key 'uq_members_email'
Interview Gold: Surrogate vs Natural Key
Interviewers love asking 'should you use an email address as a primary key?' The strong answer is no — use a surrogate key (auto-increment integer or UUID) for stability, then add a UNIQUE constraint on the email column. This separates identity from data and future-proofs the table against changes in the real world.
Production Insight
Composite primary keys can hurt performance when used as foreign keys because they require multi-column indexes. Use a surrogate key for the primary table and a unique constraint on the composite columns.
In many-to-many relationships, the join table benefits from a surrogate PK plus unique constraint on the two FK columns.
Rule: keep primary keys simple (single integer) for optimal join performance.
Key Takeaway
Composite keys are useful for junction tables.
Surrogate keys are simpler for foreign key references.
Add unique constraints on natural combinations instead of making them the PK.

ON DELETE and ON UPDATE: Choosing the Right Referential Action

When you create a foreign key, you have to decide what happens when the parent row is deleted or its primary key is updated. The database offers several options, and each has real consequences.

  • RESTRICT: prevents the operation entirely if any child rows exist. This is the safest default for financial or critical data — it forces you to handle children explicitly.
  • CASCADE: automatically deletes or updates all child rows. Useful when children are owned by the parent (e.g., user and their messages) but dangerous if misapplied to data that shouldn't be silently removed.
  • SET NULL: sets the foreign key column in child rows to NULL. Useful for optional relationships (e.g., an order with no assigned salesperson), but can leave orphaned rows.
  • SET DEFAULT: sets the foreign key column to a predefined default value. Rarely used in practice because of the tight coupling.
  • NO ACTION: similar to RESTRICT in most databases, but the check is deferred until commit in some systems (like PostgreSQL with BEGIN...COMMIT).

The choice depends entirely on your data semantics. For example, deleting a customer who has orders should be blocked (RESTRICT) or you should archive instead. Deleting a blog post should also delete its comments (CASCADE). There's no universal right answer.

referential_actions_examples.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
-- Example 1: ON DELETE CASCADE — when child rows should be removed with the parent
CREATE TABLE user_messages (
    message_id INT AUTO_INCREMENT,
    user_id    INT NOT NULL,
    content    TEXT,
    PRIMARY KEY (message_id),
    CONSTRAINT fk_user_messages
        FOREIGN KEY (user_id)
        REFERENCES users (user_id)
        ON DELETE CASCADE
);

-- Deleting a user will automatically delete all their messages
DELETE FROM users WHERE user_id = 42;

-- Example 2: ON DELETE SET NULL — for optional relationships
CREATE TABLE orders (
    order_id      INT AUTO_INCREMENT,
    customer_id   INT,                    -- nullable, because salesperson can be unassigned
    salesperson_id INT,                   -- foreign key to employees table
    PRIMARY KEY (order_id),
    CONSTRAINT fk_orders_salesperson
        FOREIGN KEY (salesperson_id)
        REFERENCES employees (employee_id)
        ON DELETE SET NULL
);

-- Deleting an employee will set salesperson_id to NULL in affected orders
DELETE FROM employees WHERE employee_id = 7;

-- Example 3: ON UPDATE CASCADE — ensures child rows follow the parent's new PK
CREATE TABLE order_items (
    order_id   INT NOT NULL,
    product_id INT NOT NULL,
    quantity   INT,
    PRIMARY KEY (order_id, product_id),
    CONSTRAINT fk_order_items_order
        FOREIGN KEY (order_id)
        REFERENCES orders (order_id)
        ON UPDATE CASCADE
);

-- If the order_id changes in the parent, it will be updated here too
Output
-- All three examples demonstrate the different behaviours.
-- The key is to choose based on data ownership, not convenience.
Cascade Danger: Unintended Data Loss
ON DELETE CASCADE can wipe out thousands of child rows in a single DELETE statement. Always test cascading deletes on a staging copy before applying to production. A single misconfigured CASCADE can cascade through multiple tables, deleting records you never intended to remove. Use CASCADE only for owned child data (e.g., user messages), never for shared or financial records.
Production Insight
ON DELETE CASCADE can cascade through multiple tables in a single DELETE, quickly wiping out unrelated data if foreign key chains exist.
SET NULL can leave orphaned rows that break business logic if not handled properly in application code.
Rule: use RESTRICT by default; CASCADE only for clearly owned child data; SET NULL for optional relationships with proper application null-handling.
Key Takeaway
Referential actions determine what happens to child rows when a parent is deleted or updated.
RESTRICT is the safest default for financial data.
CASCADE can be dangerous if misapplied; always understand the dependency graph.

Performance Considerations: Indexing Foreign Keys and Avoiding Common Pitfalls

A foreign key column without an index is the silent killer of JOIN performance. When you run a query that joins on the foreign key, the database engine must scan the entire child table to find matching rows if there's no index. As the table grows, this full table scan becomes a bottleneck.

But there's more: foreign key constraints themselves add overhead. On every INSERT into the child table, the database must verify that the foreign key value exists in the parent table. That's a lookup. If the foreign key is indexed, this lookup is fast (often an index unique scan). Without an index, it triggers a scan on the parent's primary key index (already indexed) but if the foreign key itself is not indexed, the validation step on the child side for certain operations like DELETE on the parent may require checking the child table for referencing rows — and that's where the missing index hurts.

The rule: always create an index on every foreign key column immediately after creating the constraint. Most databases like PostgreSQL and SQL Server do NOT auto-index foreign keys. MySQL/InnoDB does, but MySQL is the exception.

However, indexes come with write overhead. Every INSERT, UPDATE, or DELETE on the table must update all indexes. For high-write tables, batch operations can reduce the overhead. Use EXPLAIN to verify index usage in your queries.

index_foreign_key_performance.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- Before index: slow JOIN due to full table scan
EXPLAIN SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;
-- Output shows 'Type: ALL' on orders table — full table scan

-- Create the missing index
CREATE INDEX idx_orders_customer_id ON orders (customer_id);

-- After index: index lookup is used
EXPLAIN SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;
-- Output shows 'Type: ref' or 'Index lookup' — fast

-- Verify index exists
SHOW INDEX FROM orders;
Output
-- Before index: rows=50000 (full table), Extra: Using where
-- After index: rows=5, Extra: Using index condition
-- The difference is dramatic: from full scan to nearly instant.
Index Every Foreign Key — Immediately
Add the index as part of the same migration script that creates the foreign key. Don't rely on MySQL's auto-indexing (which is database-specific). In PostgreSQL, SQL Server, and Oracle, you must create the index explicitly. A missing index on a foreign key is the #1 cause of performance problems in systems that start small and grow.
Production Insight
Missing foreign key indexes are the #1 cause of slow JOIN queries in production databases. Always add an index immediately after creating a FK constraint.
On high-write tables, each FK check requires a lookup; batch inserts can reduce overhead.
Rule: create FK indexes as part of your migration script, not as an afterthought.
Key Takeaway
Index every foreign key column to avoid full table scans.
Use EXPLAIN to verify index usage.
Foreign key constraints add write overhead; batch writes to mitigate.
● Production incidentPOST-MORTEMseverity: high

The Unindexed Foreign Key That Took Down Checkout

Symptom
Users reported that placing an order took over 30 seconds; some orders failed with a timeout error. CPU usage on the database spiked to 100% during peak hours.
Assumption
The DBA assumed that foreign key constraints automatically create indexes on the referencing column. That's true for MySQL/InnoDB but not for PostgreSQL or SQL Server.
Root cause
The orders table had a foreign key on customer_id but no explicit index. Every INSERT and JOIN triggered a full table scan on orders to validate the foreign key constraint, because the database engine had to check for existing references when inserting or deleting parent rows.
Fix
CREATE INDEX idx_orders_customer_id ON orders (customer_id); After adding the index, order placement time dropped from 30 seconds to 50 milliseconds.
Key lesson
  • Foreign key constraints do NOT automatically create indexes in all databases.
  • Always create an explicit index on every foreign key column, especially in PostgreSQL and SQL Server.
  • Monitor query performance after adding foreign keys; a missing index is a silent performance killer.
Production debug guideSymptom-Action Guide for Referential Integrity Errors4 entries
Symptom · 01
INSERT fails with 'foreign key constraint fails'
Fix
Check if the parent record exists: SELECT * FROM parent_table WHERE pk_column = <value>. If not, insert it first.
Symptom · 02
DELETE fails with 'foreign key constraint fails'
Fix
Identify child records blocking the delete: SELECT * FROM child_table WHERE fk_column = <value>. Either delete or update them first, or use ON DELETE CASCADE.
Symptom · 03
JOIN query is unexpectedly slow
Fix
Check for missing index on the foreign key column: EXPLAIN SELECT ... Look for full table scan on the child table. Create index if missing.
Symptom · 04
UPDATE on primary key column fails
Fix
If ON UPDATE RESTRICT, update is blocked. Use ON UPDATE CASCADE or update child rows first. Check constraint definition: SHOW CREATE TABLE.
★ Quick Debug Cheat Sheet for Foreign Key IssuesCommon foreign key failures and immediate commands to diagnose and fix them.
INSERT fails with ERROR 1452 (23000)
Immediate action
Stop and identify the missing parent record.
Commands
SELECT * FROM parent_table WHERE pk_column = <value>;
If not found, INSERT the parent record first, then retry the child INSERT.
Fix now
Use a transaction: BEGIN; INSERT INTO parent ...; INSERT INTO child ...; COMMIT;
DELETE fails with ERROR 1451 (23000)+
Immediate action
Find child records referencing the parent.
Commands
SELECT * FROM child_table WHERE fk_column = <value>;
If there are child records, decide: either delete them first, or temporarily disable FK check (not recommended in production).
Fix now
Set ON DELETE CASCADE if safe, or manually delete children before parent.
Slow JOIN queries on foreign key column+
Immediate action
Check execution plan for full table scan.
Commands
EXPLAIN SELECT ... FROM orders JOIN customers ON orders.customer_id = customers.customer_id;
Look for 'Seq Scan' or 'Table Scan' on orders. If found, add index: CREATE INDEX idx_orders_customer_id ON orders(customer_id);
Fix now
Add the index and re-run the query.
UPDATE on primary key column fails with constraint violation+
Immediate action
Check foreign key constraint definition.
Commands
SHOW CREATE TABLE child_table;
If ON UPDATE RESTRICT is set, you must update child rows first or change to CASCADE.
Fix now
Use ON UPDATE CASCADE if appropriate, or update child fk values to match the new pk.
Primary Key vs Foreign Key
Feature / AspectPrimary KeyForeign Key
PurposeUniquely identifies each row in its own tableLinks a row in this table to a row in another table
Where it livesIn the table it identifiesIn the child/referencing table
Must be unique?Yes — always, no exceptionsNo — many rows can share the same foreign key value
Can it be NULL?Never — NULL is forbiddenYes, if the relationship is optional (e.g. an order with no assigned salesperson)
Can there be duplicates?No — each value must appear exactly onceYes — multiple orders can reference the same customer_id
Auto-generated?Commonly yes, via AUTO_INCREMENT or SERIALNo — you supply the value from the parent table's primary key
What happens on violation?INSERT/UPDATE is rejected with a duplicate key errorINSERT is rejected; DELETE of parent is blocked (RESTRICT) or cascades (CASCADE)
Typical column nametable_name + _id, e.g. customer_id in customersSame name as the parent PK, e.g. customer_id in orders
Number allowed per tableExactly one primary key (can span multiple columns)Many foreign keys allowed — a table can reference several other tables

Key takeaways

1
A primary key is a column whose value is unique and never NULL for every row
it's the table's permanent identity badge, ideally an auto-incrementing integer that has no real-world meaning.
2
A foreign key is a column that stores the primary key value from another table
it's the bridge that links related data across tables without duplicating information.
3
Foreign key constraints enforce referential integrity at the database level
the database itself will block you from inserting orphaned records or deleting parents that have dependent children, not just your application code.
4
Use ON DELETE RESTRICT for financial or critical data (safe default), ON DELETE CASCADE for owned child data like user messages, and ON DELETE SET NULL for optional relationships
the choice you make here has real consequences in production.
5
Always create an explicit index on every foreign key column
missing indexes are the #1 cause of slow JOINs in production databases.

Common mistakes to avoid

3 patterns
×

Using a mutable real-world value (like email) as a primary key

Symptom
UPDATE statements cascade-fail across multiple tables when a user changes their email, or you get mysterious orphaned records.
Fix
Always use a surrogate key (INT AUTO_INCREMENT or UUID) as the primary key. Add a UNIQUE constraint on the email column separately if you need uniqueness there. The primary key should be meaningless, permanent, and auto-assigned.
×

Forgetting to index the foreign key column

Symptom
JOIN queries on large tables become painfully slow as the table grows, even though the query looks correct.
Fix
Most databases (PostgreSQL, SQL Server) do NOT automatically create an index on a foreign key column — only MySQL/InnoDB does. Always create an explicit index: CREATE INDEX idx_orders_customer_id ON orders (customer_id); This turns a full table scan into a lightning-fast lookup every time you JOIN the two tables.
×

Inserting child rows before parent rows (or dropping tables in the wrong order)

Symptom
ERROR 1452 on INSERT or ERROR 1451 on DROP/DELETE even though the data looks correct.
Fix
Always insert the parent record first (e.g. insert the customer before inserting their order). When dropping tables, drop child tables before parent tables, or temporarily disable foreign key checks: SET FOREIGN_KEY_CHECKS = 0; DROP TABLE orders; DROP TABLE customers; SET FOREIGN_KEY_CHECKS = 1; Re-enable checks immediately after — never leave them off.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between a primary key and a unique key?
Q02SENIOR
Can a table have a foreign key that references itself?
Q03SENIOR
What is referential integrity, and how do PRIMARY KEY and FOREIGN KEY co...
Q01 of 03JUNIOR

What is the difference between a primary key and a unique key?

ANSWER
Both enforce uniqueness, but a table can only have ONE primary key and it cannot contain NULLs, whereas you can have multiple UNIQUE constraints on a table and UNIQUE columns can contain NULL in most databases — though NULL behaviour varies by database engine. The primary key is also the default target for foreign key references and is typically clustered (in some database systems).
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Can a foreign key reference a column that is not a primary key?
02
Can a table have more than one foreign key?
03
What is the difference between a primary key and a unique key?
🔥

That's Database Design. Mark it forged?

6 min read · try the examples if you haven't

Previous
ER Diagrams
4 / 16 · Database Design
Next
Database Relationships