A composite key is a primary key made of two or more columns whose combination uniquely identifies each row
Column order matters: the leftmost column drives index efficiency — put the most-queried column first
Every column in a composite key must be NOT NULL — NULL bypasses uniqueness checks and lets duplicates slip through
Composite keys in parent tables propagate to child tables as multi-column foreign keys, increasing FK verbosity at each level
The hybrid pattern (surrogate PK + UNIQUE constraint on natural composite) gives you compact FKs without sacrificing data integrity
Missing secondary indexes on trailing columns causes full index scans — a silent performance killer under production load
Plain-English First
Imagine your school has hundreds of students, and two of them are named 'John Smith'. A single name isn't enough to find the right one — you need the name AND the class AND the year together. That combination of details is exactly what a composite key is: multiple columns working as a team to uniquely identify one row, because no single column is up to the job alone.
Every database table needs a way to say 'this row, and only this row.' Most tutorials jump straight to auto-incrementing IDs and call it done. But the real world is messier — enrollment records, order line items, airport routes, and game leaderboards all have natural uniqueness that lives across multiple columns, not one magic number.
A composite key solves the problem of natural multi-column uniqueness. Instead of bolting on an artificial surrogate ID just to have 'a primary key', you declare the combination of columns that already makes a row unique as the key itself. This keeps your schema honest, enforces business rules at the database level where they can't be bypassed, and often makes foreign key relationships self-documenting.
By the end of this article you'll know exactly when a composite key is the right tool, how to define one in SQL with proper constraints, how it interacts with foreign keys in child tables, and — critically — when NOT to use one.
What a Composite Key Actually Is (And Why It Exists)
A composite key is a primary key made up of two or more columns. Together those columns must be unique across every row in the table. Individually, each column is allowed to repeat — it's only the combination that must be distinct.
The classic case is a junction table. Say you're modelling student course enrolments. A student can enrol in many courses. A course can have many students. The enrolment record sits in the middle. What makes one enrolment unique? Not the student alone — they take many courses. Not the course alone — it has many students. The pair (student_id, course_id) is what's unique. That pair IS the composite key.
This isn't just an academic convenience. Declaring it as the primary key means the database engine enforces the rule automatically. You can't accidentally enrol the same student in the same course twice. No application-layer check needed. The constraint lives at the lowest, most reliable layer of your stack.
Composite keys also appear in tables that model real-world uniqueness spanning multiple dimensions — think (airport_code, flight_number, departure_date) for a flight schedule, or (warehouse_id, product_sku) for inventory levels. The multi-column uniqueness isn't a workaround; it's an accurate model of reality.
create_enrolment_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
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
57
58
59
60
61
62
-- Scenario: A university enrolment system.-- A student can enrol in many courses, a course has many students.-- The COMBINATION of student_id + course_id must be unique.CREATETABLEstudents (
student_id INTNOTNULL,
full_name VARCHAR(120) NOTNULL,
email VARCHAR(255) NOTNULLUNIQUE,
enrolled_on DATENOTNULL,
PRIMARYKEY (student_id) -- single-column PK on the parent table
);
CREATETABLEcourses (
course_id INTNOTNULL,
course_name VARCHAR(200) NOTNULL,
credit_hours TINYINTNOTNULL,
PRIMARYKEY (course_id) -- single-column PK on the parent table
);
CREATETABLEenrolments (
student_id INTNOTNULL,
course_id INTNOTNULL,
enrolment_date DATENOTNULL,
grade CHAR(2), -- nullable until graded-- The composite primary key: the PAIR must be uniquePRIMARYKEY (student_id, course_id),
-- Foreign keys back to parent tables keep referential integrity intactCONSTRAINT fk_enrolment_student
FOREIGNKEY (student_id) REFERENCESstudents(student_id)
ONDELETECASCADE,
CONSTRAINT fk_enrolment_course
FOREIGNKEY (course_id) REFERENCEScourses(course_id)
ONDELETERESTRICT
);
-- Seed some data so we can see the constraint in actionINSERTINTO students VALUES (1, 'Maria Reyes', 'maria@uni.edu', '2024-09-01');
INSERTINTO students VALUES (2, 'David Okafor', 'david@uni.edu', '2024-09-01');
INSERTINTO courses VALUES (101, 'Intro to Databases', 3);
INSERTINTO courses VALUES (102, 'Algorithms I', 4);
-- Valid enrolments: different (student_id, course_id) pairsINSERTINTO enrolments VALUES (1, 101, '2024-09-05', NULL);
INSERTINTO enrolments VALUES (1, 102, '2024-09-05', NULL);
INSERTINTO enrolments VALUES (2, 101, '2024-09-06', NULL);
-- This will FAIL — same student, same course: duplicate composite key-- INSERT INTO enrolments VALUES (1, 101, '2024-09-10', NULL);-- ERROR 1062 (23000): Duplicate entry '1-101' for key 'enrolments.PRIMARY'SELECT
s.full_name AS student,
c.course_name AS course,
e.enrolment_date
FROM enrolments e
JOIN students s ON s.student_id = e.student_id
JOIN courses c ON c.course_id = e.course_id
ORDERBY s.full_name, c.course_name;
Here's where things get interesting — and where most tutorials drop the ball. When a table with a composite primary key becomes the parent in a relationship, the child table must reference the entire composite key. You can't pick just one column from the parent's composite key and call it a foreign key.
Think about an order management system. An order line item doesn't just belong to a product — it belongs to a specific product within a specific order. If your order_items table references order_id and product_id, both of those columns together form the foreign key back to a composite-keyed parent.
This is powerful because it keeps relationships semantically precise. The database engine won't let an order_item row reference a non-existent (order_id, product_id) combination. That's a business rule enforced for free.
The tradeoff is verbosity. Child tables start accumulating columns quickly. An order_item might carry order_id and product_id from the parent, plus its own attributes like quantity and unit_price. If that order_item then becomes a parent to another table — say, a returns table — the returns table has to carry those same keys forward. This key propagation is called 'wide foreign keys' and is the main argument for replacing composite keys with surrogate IDs in complex multi-level hierarchies.
composite_fk_order_system.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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
-- Scenario: E-commerce order system.-- An order has many line items. Each line item is uniquely identified-- by the order it belongs to AND the product on that line.CREATETABLEorders (
order_id INTNOTNULL,
customer_email VARCHAR(255) NOTNULL,
order_date DATENOTNULL,
PRIMARYKEY (order_id)
);
CREATETABLEproducts (
product_id INTNOTNULL,
product_name VARCHAR(200) NOTNULL,
unit_price DECIMAL(10,2) NOTNULL,
PRIMARYKEY (product_id)
);
-- order_line_items uses a composite PK: one order can't have the-- same product listed twice (you'd just increase the quantity instead)CREATETABLEorder_line_items (
order_id INTNOTNULL,
product_id INTNOTNULL,
quantity SMALLINTNOTNULLCHECK (quantity > 0),
unit_price DECIMAL(10,2) NOTNULL, -- price at time of purchase, may differ from product tablePRIMARYKEY (order_id, product_id), -- composite PKCONSTRAINT fk_lineitem_order
FOREIGNKEY (order_id) REFERENCESorders(order_id) ONDELETECASCADE,
CONSTRAINT fk_lineitem_product
FOREIGNKEY (product_id) REFERENCESproducts(product_id) ONDELETERESTRICT
);
-- A returns table must carry BOTH columns of the parent composite keyCREATETABLEreturns (
return_id INTNOTNULL AUTO_INCREMENT,
order_id INTNOTNULL, -- \ Both columns needed
product_id INTNOTNULL, -- / to reference parent
return_reason VARCHAR(500),
returned_on DATENOTNULL,
PRIMARYKEY (return_id),
-- The composite FK references the composite PK of order_line_itemsCONSTRAINT fk_return_lineitem
FOREIGNKEY (order_id, product_id)
REFERENCESorder_line_items(order_id, product_id)
ONDELETECASCADE
);
-- Insert demo dataINSERTINTO orders VALUES (1001, 'alice@shop.com', '2024-11-01');
INSERTINTO products VALUES (5, 'Mechanical Keyboard', 89.99);
INSERTINTO products VALUES (9, 'USB-C Hub', 34.50);
INSERTINTO order_line_items VALUES (1001, 5, 1, 89.99);
INSERTINTO order_line_items VALUES (1001, 9, 2, 34.50);
INSERTINTO returns VALUES (NULL, 1001, 9, 'Wrong colour delivered', '2024-11-08');
-- Confirm the return links correctly back through the chainSELECT
r.return_id,
p.product_name,
r.return_reason,
r.returned_on
FROM returns r
JOIN order_line_items oli ON oli.order_id = r.order_id
AND oli.product_id = r.product_id
JOIN products p ON p.product_id = oli.product_id;
Watch Out: FK Column Order Must Match Parent PK Order
When you reference a composite key as a foreign key, the column names in the child table don't have to match the parent — but they must be in the same ORDER as the parent's primary key definition. If the parent declares PRIMARY KEY (order_id, product_id) and your FK lists (product_id, order_id), most databases will throw an error or silently build the wrong constraint. Always match the sequence exactly.
Production Insight
Composite FKs propagate through child tables — each level adds all parent key columns.
A 3-column composite PK at level 1 becomes 3 extra columns in every child, grandchild, and great-grandchild table.
Rule: if your hierarchy is 3+ levels deep, replace the composite PK with a surrogate at the top level to stop column explosion.
Key Takeaway
Child tables must reference the ENTIRE composite key — you cannot pick one column from a composite PK as a FK.
Each hierarchy level propagates all composite columns forward, causing 'wide FK' bloat in deep trees.
Punchline: if your hierarchy is 3+ levels deep, replace the composite PK with a surrogate at the top to stop column explosion.
Handling Composite Keys in Child Tables
IfParent has composite PK, child is one level deep
→
UseCarry all parent PK columns as a composite FK — clean and semantically precise
IfParent has composite PK, child needs its own children
→
UseEvaluate: will the grandchild carry 4+ inherited columns? If yes, consider surrogate PK at parent level
IfFK column order doesn't match parent PK order
→
UseFix immediately — mismatched order causes wrong constraint or silent errors
IfChild table has more FK columns than business columns
→
UseThe composite key is too wide — switch to surrogate PK + UNIQUE constraint at the parent
Composite Keys vs Surrogate Keys — Choosing the Right Tool
This is the real design decision you'll face in production, and it's genuinely contextual — there's no universally correct answer.
A surrogate key is an artificial identifier the database generates (AUTO_INCREMENT, SERIAL, UUID) that has no business meaning. It exists purely to be a key. A composite key, by contrast, is assembled from data that already exists in your domain.
Composite keys shine when the natural uniqueness is stable (won't change), is always known at insert time, and when the combination is exactly what you'd join on in queries anyway. Junction tables are the textbook case. Airport route tables (origin_airport, destination_airport) are another.
Surrogate keys win when the natural identifiers are long strings (bad for index performance), when they might change over time (a renamed username would cascade through every child table), or when the hierarchy gets deep enough that propagating a composite key four levels down creates wide, unwieldy tables.
The hybrid pattern — using a surrogate key as the primary key but adding a UNIQUE constraint on the natural composite — gives you the performance of a compact single-column key plus the data integrity of enforced natural uniqueness. It's often the pragmatic compromise.
hybrid_key_pattern.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
-- Scenario: Airport route pricing.-- Routes are naturally identified by (origin, destination, airline_code).-- We add a surrogate PK for compact FK references, but enforce-- the natural composite uniqueness with a UNIQUE constraint.CREATETABLEroutes (
route_id INTNOTNULL AUTO_INCREMENT,
origin CHAR(3) NOTNULL, -- IATA airport code e.g. 'JFK'
destination CHAR(3) NOTNULL, -- IATA airport code e.g. 'LAX'
airline_code CHAR(2) NOTNULL, -- IATA airline code e.g. 'AA'
base_price DECIMAL(8,2) NOTNULL,
-- Surrogate PK: compact, stable, easy for child tables to referencePRIMARYKEY (route_id),
-- Natural composite UNIQUE constraint: enforces business rule-- that no two rows can represent the same actual route on same airlineCONSTRAINT uq_route_natural
UNIQUE (origin, destination, airline_code)
);
-- Child table uses the compact surrogate — much cleaner than-- carrying three columns forward from the parentCREATETABLEflight_schedules (
schedule_id INTNOTNULL AUTO_INCREMENT,
route_id INTNOTNULL, -- single-column FK, nice and clean
departure_time TIMENOTNULL,
days_of_week VARCHAR(7) NOTNULL, -- e.g. 'MTWTFSS'PRIMARYKEY (schedule_id),
CONSTRAINT fk_schedule_route
FOREIGNKEY (route_id) REFERENCESroutes(route_id) ONDELETECASCADE
);
-- PopulateINSERTINTOroutes (origin, destination, airline_code, base_price)
VALUES
('JFK', 'LAX', 'AA', 249.00),
('JFK', 'LAX', 'DL', 229.00), -- same airports, different airline: valid
('LAX', 'ORD', 'UA', 189.00);
-- This would fail: same natural key already exists-- INSERT INTO routes (origin, destination, airline_code, base_price)-- VALUES ('JFK', 'LAX', 'AA', 199.00);-- ERROR: Duplicate entry 'JFK-LAX-AA' for key 'routes.uq_route_natural'SELECT route_id, origin, destination, airline_code, base_price
FROM routes
ORDERBY origin, destination, airline_code;
Interview Gold: Surrogate vs Composite Key Trade-offs
If an interviewer asks 'when would you choose a surrogate key over a composite key?', the winning answer hits three points: (1) when natural keys are mutable — a username change would cascade painfully; (2) when the composite is more than 2-3 columns, making FK references in child tables unwieldy; (3) when working with ORMs that assume a single-column integer PK. Show you know the trade-off, not just one side.
Production Insight
The hybrid pattern (surrogate PK + UNIQUE on natural composite) is the pragmatic default for most production schemas.
It gives you compact single-column FKs for clean joins while still enforcing natural uniqueness at the database level.
Rule: start with hybrid unless you have a strong reason — pure composite PKs are best reserved for junction tables.
Key Takeaway
Composite keys win when natural uniqueness is stable, low-cardinality, and exactly what you join on.
Surrogate keys win when identifiers are mutable, wide, or your ORM demands a single-column PK.
Punchline: the hybrid pattern (surrogate PK + UNIQUE constraint) is the pragmatic default — start there unless you have a strong reason not to.
Composite vs Surrogate Key Decision
IfJunction table or stable natural identifiers (IATA codes, ISBNs)
→
UseUse composite PK — the natural uniqueness IS the business rule
IfNatural keys are mutable strings or more than 3 columns
IfPerformance-critical joins on a high-traffic table
→
UseUse surrogate PK — single integer FK is smaller in every secondary index leaf
Querying and Indexing Composite Keys Efficiently
Understanding a composite key is one thing — writing queries that use it efficiently is another. The composite primary key creates a clustered index (in MySQL/InnoDB) or a similar B-tree structure, and how you query against it determines whether you get an index seek or a full scan.
The leftmost prefix rule is the key insight. With PRIMARY KEY (student_id, course_id), the database can satisfy queries that filter on student_id alone, or on (student_id, course_id) together. It cannot efficiently satisfy a query that filters on course_id alone — that would require a full table scan unless you create a separate index on course_id.
This asymmetry is why column order in your composite key definition is a performance decision, not just a naming convention. If your query patterns show you often need to find all enrolments for a course regardless of student, add a secondary index on (course_id) explicitly rather than changing the primary key order and breaking the more common student-first queries.
Also be careful with partial matches in composite FK lookups. Always provide all columns of the composite key in your WHERE clause when doing point lookups — providing only the trailing columns wastes the index entirely.
composite_key_query_patterns.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
-- Continuing with the enrolments table from Section 1.-- Demonstrating query patterns that use the composite index well vs. poorly.-- GOOD: Filters on the leading column (student_id) — uses the index efficiently-- Retrieves all courses for a specific studentSELECT
e.course_id,
c.course_name,
e.grade
FROM enrolments e
JOIN courses c ON c.course_id = e.course_id
WHERE e.student_id = 1; -- leading column: index seek-- Expected output:-- course_id | course_name | grade-- 101 | Intro to Databases | NULL-- 102 | Algorithms I | NULL-- GOOD: Filters on both columns — pinpoint lookup, fastest possibleSELECT grade
FROM enrolments
WHERE student_id = 1AND course_id = 101; -- full composite key: optimal-- Expected output:-- grade-- NULL-- POTENTIALLY SLOW: Filters only on the trailing column (course_id)-- The composite index (student_id, course_id) cannot be used efficiently hereSELECT student_id
FROM enrolments
WHERE course_id = 101; -- trailing column only: full scan on composite index-- Fix: Add a separate secondary indexCREATEINDEX idx_enrolments_course
ONenrolments (course_id); -- now this query gets its own index to use-- Verify with EXPLAIN that the index is being picked upEXPLAINSELECT student_id
FROM enrolments
WHERE course_id = 101;
Output
-- Before secondary index:
-- id | select_type | table | type | key | rows | Extra
-- 1 | SIMPLE | enrolments | index | PRIMARY | 3 | Using where; Using index
-- After adding idx_enrolments_course:
-- id | select_type | table | type | key | rows | Extra
-- 1 | SIMPLE | enrolments | ref | idx_enrolments_course | 1 | Using index
-- 'ref' is much better than 'index' — it means a targeted lookup, not a full scan
Pro Tip: Always EXPLAIN Your Composite Key Queries
Run EXPLAIN (MySQL/PostgreSQL) or EXPLAIN ANALYZE (PostgreSQL) on any query that filters or joins on a composite key. Look at the 'type' column in MySQL — 'ref' or 'eq_ref' means the index is being used well; 'index' means a full index scan; 'ALL' means no index at all. Fixing an 'ALL' on a high-traffic junction table can cut query time by orders of magnitude.
Production Insight
The leftmost prefix rule means trailing-column queries silently degrade to full scans under production load.
A junction table with 10M rows doing a full index scan instead of a seek can add seconds per query.
Rule: after defining a composite PK, EXPLAIN every query that touches it. Add secondary indexes for trailing-column access patterns.
Key Takeaway
The leftmost prefix rule: a composite index (A, B, C) can serve queries on A, (A, B), or (A, B, C) — but NOT on B alone or C alone.
Column order in your composite key is a performance decision driven by your most common query pattern.
Punchline: always EXPLAIN your composite key queries — a silent full scan on a 10M-row junction table will ruin your production latency.
Index Strategy for Composite Keys
IfQuery filters on the leading column only
→
UseComposite PK index handles it efficiently — no extra index needed
IfQuery filters on both columns together
→
UseComposite PK index is optimal — point lookup, fastest possible
IfQuery filters on the trailing column only
→
UseAdd a secondary index on that column — composite PK cannot help here
IfQuery filters on leading + trailing with a gap (col1 and col3 of a 3-col key)
→
UseOnly col1 uses the index — add a separate index on (col1, col3) for this pattern
● Production incidentPOST-MORTEMseverity: high
NULL in composite key lets duplicate inventory records bypass uniqueness constraint
Symptom
Inventory counts showed more units than physically existed. Customer orders were confirmed for stock that didn't exist. No database errors were logged — INSERTs succeeded every time.
Assumption
The team assumed a race condition in the application-layer stock decrement logic, or a caching bug in the inventory microservice.
Root cause
A migration made bin_location nullable to support bulk-arrival staging. Because SQL treats NULL != NULL for uniqueness checks, two rows with (warehouse_id=3, product_sku='WIDGET-X', bin_location=NULL) were both allowed. Each row reported its own stock count. The SUM() across both rows overstated available inventory.
Fix
Added NOT NULL constraint to bin_location with a sentinel value 'UNASSIGNED' for bulk arrivals. Added a CHECK constraint ensuring every bin_location is either a valid rack code or 'UNASSIGNED'. Ran a dedup query to merge duplicate rows before applying the constraint.
Key lesson
Every column in a composite key or UNIQUE constraint must be NOT NULL — NULL bypasses uniqueness in SQL
Schema migrations that relax nullability on key columns are high-risk — review them with the same scrutiny as dropping a constraint
After any migration that touches key columns, run a SELECT ... GROUP BY ... HAVING COUNT(*) > 1 to verify no duplicates crept in
Production debug guideCommon composite key failures and how to diagnose them5 entries
Symptom · 01
Queries filtering on the second or third column of a composite key are slow
→
Fix
Run EXPLAIN — you'll likely see 'index' (full scan) instead of 'ref'. Add a secondary index on the trailing column you're filtering by.
Symptom · 02
Duplicate rows exist despite a composite UNIQUE constraint
→
Fix
Check for NULL values in any column of the constraint. Run: SELECT col1, col2, COUNT() FROM t GROUP BY col1, col2 HAVING COUNT() > 1. NULL != NULL bypasses uniqueness.
Symptom · 03
Foreign key constraint fails on insert into child table
→
Fix
Verify the FK column order matches the parent PK column order exactly. Also confirm all FK columns are NOT NULL if the parent PK columns are NOT NULL.
Symptom · 04
ORM generates wrong SQL or throws 'no primary key found'
→
Fix
Many ORMs assume single-column PK. Check ORM docs for composite key support. If unsupported, switch to the hybrid pattern: surrogate PK + UNIQUE constraint.
Symptom · 05
Child table has too many columns from propagated composite FKs
→
Fix
The composite key is too wide for deep hierarchies. Replace with surrogate PK at the parent level and enforce natural uniqueness via UNIQUE constraint.
Composite Primary Key vs Surrogate Key + UNIQUE Constraint
Aspect
Composite Primary Key
Surrogate Key + UNIQUE Constraint
Business meaning
Key columns carry real domain meaning
PK is meaningless; meaning lives in the UNIQUE constraint
FK verbosity in child tables
Child must carry all composite columns
Child carries one compact integer column
Mutability risk
High — if a key column's value changes, cascades to all children
Low — surrogate never changes; only UNIQUE columns change
Index size
Larger — multi-column PK stored in every secondary index leaf
Smaller — single integer PK stored in secondary indexes
Self-documenting schema
Yes — the PK tells you what makes a row unique
Partial — you must inspect the UNIQUE constraint separately
ORM compatibility
Tricky — many ORMs expect a single-column PK
Smooth — single integer PK works with every ORM out of the box
Deep hierarchies, mutable identifiers, ORM-heavy projects
Data integrity enforcement
PK constraint handles uniqueness automatically
UNIQUE constraint handles it — same outcome, extra declaration
Key takeaways
1
A composite key is the right primary key when two or more columns together
and only together — guarantee row uniqueness; junction tables (many-to-many relationships) are the canonical example.
2
Column order in a composite key definition is a performance decision
put the most-queried column first because the database can only use the index efficiently from the leftmost column forward.
3
Every column in a composite key must be NOT NULL
NULL values bypass uniqueness checks and let logically duplicate rows slip past the constraint silently.
4
The hybrid pattern
surrogate PK plus a UNIQUE constraint on the natural composite — gives you compact FK references and clean ORM integration without sacrificing natural-key integrity enforcement.
Common mistakes to avoid
3 patterns
×
Including a nullable column in a composite key
Symptom
Rows that should clash are silently inserted as duplicates. In SQL, NULL is not equal to NULL, so (1, NULL) and (1, NULL) are treated as two distinct rows by the uniqueness check even though they look identical.
Fix
Every column participating in a composite key must be declared NOT NULL, always. If a value is genuinely optional, use a sentinel like 'UNASSIGNED' or 0 instead of NULL.
×
Forgetting the secondary index on the trailing column
Symptom
Queries that filter on the second or third column of a composite key run with a full index scan, hammering performance in production under load. EXPLAIN shows type='index' instead of 'ref'.
Fix
Use EXPLAIN after you define the schema. Add explicit secondary indexes on any column you'll query in isolation that isn't the leading column of the composite key.
×
Treating composite keys as interchangeable with composite unique constraints
Symptom
Confusion when an ORM or reporting tool fails to identify the 'primary key' correctly, leading to duplicate-row bugs or failed FK references. A composite UNIQUE constraint allows a surrogate PK to coexist; a composite PRIMARY KEY means those columns ARE the canonical identifier with no surrogate.
Fix
Decide deliberately at design time: use composite PRIMARY KEY for junction tables where the natural pair IS the identity. Use surrogate PRIMARY KEY + composite UNIQUE constraint when child tables need a compact FK reference or your ORM requires a single-column PK.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Can you explain the difference between a composite key and a composite u...
Q02SENIOR
You have a junction table with a composite primary key of (user_id, role...
Q03SENIOR
Why can't you use only the second column of a composite index in a WHERE...
Q01 of 03SENIOR
Can you explain the difference between a composite key and a composite unique constraint, and when you'd choose one over the other?
ANSWER
A composite primary key declares the combination of columns as the canonical row identifier — those columns ARE the key, and child tables must carry all of them as foreign keys. A composite unique constraint enforces the same uniqueness rule but allows a separate (usually surrogate) primary key to exist. Choose composite PK for junction tables where the natural pair is the identity. Choose surrogate PK + composite UNIQUE when child tables need a compact FK, when the natural key is mutable, or when your ORM requires a single-column PK.
Q02 of 03SENIOR
You have a junction table with a composite primary key of (user_id, role_id). A new requirement asks you to record the timestamp of when the role was assigned and allow a user to be assigned the same role multiple times at different dates. How does that change your key strategy?
ANSWER
The original composite key (user_id, role_id) enforces one assignment per user-role pair. To allow reassignment at different times, you must include the timestamp in the key: (user_id, role_id, assigned_at). Alternatively, switch to a surrogate PK (assignment_id) and keep (user_id, role_id, assigned_at) as a UNIQUE constraint. The surrogate approach is usually cleaner here because child tables (like audit logs) only need one column instead of three.
Q03 of 03SENIOR
Why can't you use only the second column of a composite index in a WHERE clause and still expect an efficient index seek? Walk me through what the database engine actually does.
ANSWER
A composite index (A, B) is a B-tree sorted first by A, then by B within each A group. When you query WHERE B = X, the engine cannot jump to B's position because B values are scattered across different A groups — there's no single sorted sequence of B values. It must scan every entry in the index (type='index' in MySQL EXPLAIN). This is the leftmost prefix rule: the index is only usable from the leading column forward. Fix: add a separate index on B alone if you need to query it independently.
01
Can you explain the difference between a composite key and a composite unique constraint, and when you'd choose one over the other?
SENIOR
02
You have a junction table with a composite primary key of (user_id, role_id). A new requirement asks you to record the timestamp of when the role was assigned and allow a user to be assigned the same role multiple times at different dates. How does that change your key strategy?
SENIOR
03
Why can't you use only the second column of a composite index in a WHERE clause and still expect an efficient index seek? Walk me through what the database engine actually does.
SENIOR
FAQ · 3 QUESTIONS
Frequently Asked Questions
01
Can a composite key contain more than two columns?
Yes — a composite key can span as many columns as needed to guarantee uniqueness. In practice, three columns is common (e.g., warehouse_id, product_sku, bin_location for inventory). Beyond three or four columns the key becomes unwieldy for child table FK references, and the hybrid surrogate + UNIQUE constraint pattern starts making more sense.
Was this helpful?
02
Does a composite primary key automatically create an index?
Yes, in every mainstream database (MySQL, PostgreSQL, SQL Server, Oracle). The PRIMARY KEY constraint implicitly creates a unique index on the combination of columns. In MySQL InnoDB, that index is clustered — meaning the table data is physically sorted by the composite key, which makes range scans on the leading column extremely fast.
Was this helpful?
03
What's the difference between a composite key and a compound key?
They mean the same thing — a key made of multiple columns. 'Composite key' is the more widely used term in modern documentation and interviews. 'Compound key' is older terminology you'll see in legacy materials and some university textbooks. Don't let the naming difference trip you up; the concept and implementation are identical.