Build the mental model
A primary key is the value that uniquely identifies one specific row in a table — no two rows share it, and it's never left empty. In a users table, users.id = 42 always means exactly one user, forever.
| Key type | Description |
|---|---|
| Natural key | a real-world, business-meaningful value you already have, like an email |
| Surrogate key | a value generated purely for identification, like an auto-incrementing integer or a UUID |
Neither is automatically better: natural keys can change (people change emails), while surrogate keys are stable but carry no business meaning on their own. Real systems often use both patterns together.
A foreign key is different: it's a column in one table that stores the primary key value of a row in another table, creating a link. orders.user_id is expected to match some row's id in the users table.
Storing just a pointer
Instead of duplicating a user's entire profile inside every order, the order just stores a pointer — user_id — back to where that user's full information actually lives.
- Primary Key
- The value that uniquely identifies one row in a table; never empty, never duplicated.
- Foreign Key
- A column that stores another table's primary key value, creating a link between two rows.
- Surrogate Key
- A value generated purely for identification (an auto-incrementing integer or UUID), with no meaning outside the database.
FOREIGN KEY LINK: ORDERS TO USERS
---------------------------------
FOREIGN KEY LINK: ORDERS TO USERS
------------------------------------
users orders
+----+-------+ +-----+---------+----------+
| id | name | | id | user_id | item |
+----+-------+ +-----+---------+----------+
| 1 | Nilar | | 101 | 2 | Keyboard |
| 2 | Zaw | | 102 | 1 | Monitor |
| 3 | Hnin | | 103 | 3 | Headset |
+----+-------+ +-----+---------+----------+
users.id (primary key) <- orders.user_id (foreign key)Connect it to a real scenario
When modeling any new feature, ask two separate questions: 'what uniquely identifies one row here?' (a primary key candidate) and 'does this row need to point back to a row somewhere else?' (a foreign key candidate).
A frequent real-world tension: use an email as a primary key, or generate a surrogate id instead? Many production systems choose the surrogate id so a later email change doesn't ripple into every referencing table.
The lookup pattern
Given an order, following its user_id to find the matching row in users is the pattern behind the SQL course's real JOIN syntax. Recognizing it by eye helps you read an unfamiliar schema fast.
Try the working example
const users = [
{ id: 1, name: "Nilar" },
{ id: 2, name: "Zaw" },
{ id: 3, name: "Hnin" },
];
const orders = [
{ id: 101, user_id: 2, item: "Keyboard" },
{ id: 102, user_id: 1, item: "Monitor" },
{ id: 103, user_id: 3, item: "Headset" },
];
function findOrderOwner(orderId, ordersTable, usersTable) {
const order = ordersTable.find((o) => o.id === orderId);
if (!order) return null;
const user = usersTable.find((u) => u.id === order.user_id);
return user ? { item: order.item, owner: user.name } : null;
}
console.log(findOrderOwner(101, orders, users));
console.log(findOrderOwner(103, orders, users));
console.log(findOrderOwner(999, orders, users));{ item: 'Keyboard', owner: 'Zaw' }
{ item: 'Headset', owner: 'Hnin' }
null5-minute try-it
Add a new order with a user_id that doesn't match any user to the orders array, run findOrderOwner on it, predict the result first, then check.
One important caution
Assuming a foreign key value always matches an existing row — without a real constraint enforcing it, 'orphaned' foreign keys pointing at deleted rows are a common real bug
Picking a natural key like email as a primary key and then hard-coding that assumption everywhere, making it painful when that value legitimately needs to change
Wikipedia: Foreign key — How Databases Work