Database modelling
Primary keys and foreign keys
Our customers table contains an id column.
Run:
1234SELECT id, full_name, emailFROM customersORDER BY idLIMIT 5;You should see:
1234567 id | full_name | email----+------------+----------------------- 1 | Customer 1 | Customer1@Example.com 2 | Customer 2 | Customer2@Example.com 3 | Customer 3 | Customer3@Example.com 4 | Customer 4 | Customer4@Example.com 5 | Customer 5 | Customer5@Example.comEach customer has a different id.
The id column is the primary key of the customers table.
Primary keys
A primary key identifies each row in a table.
Because a primary key is used to identify a particular row, its values must be:
unique
not
NULL
For example, customer 3 can be identified by id = 3, and no other row in customers can have the same id.
If we try to insert another customer with an id of 3:
1234567891011121314INSERT INTO customers ( id, email, full_name, country, created_at)VALUES ( 3, 'AnotherCustomer@Example.com', 'Another Customer', 'US', '2025-01-01');PostgreSQL rejects the insert because a row with that primary-key value already exists.
The primary key therefore doesn't just help us identify rows. PostgreSQL also enforces the rule that every primary-key value uniquely identifies one row.
Foreign keys
Now look at the orders table:
1234SELECT id, customer_id, statusFROM ordersORDER BY idLIMIT 5;You should see:
1234567 id | customer_id | status----+-------------+----------- 1 | 2 | delivered 2 | 3 | delivered 3 | 4 | delivered 4 | 5 | delivered 5 | 6 | deliveredEach order contains a customer_id.
For example, order 1 has customer_id = 2. This tells us that the order belongs to customer 2.
The customer_id column in orders is a foreign key that references customers.id.
The reference is:
1orders.customer_id → customers.idA simplified view looks like this:
The primary key identifies the customer.
The foreign key stores the ID of the customer associated with an order.
Foreign keys enforce the relationship
A foreign key doesn't just describe a relationship between tables. PostgreSQL also enforces it.
For example, there is no customer with an id of 999999.
If we try to create an order for that customer:
12345678910111213141516INSERT INTO orders ( customer_id, status, total_amount, payment_reference, placed_at, shipped_at)VALUES ( 999999, 'processing', 49.99, 'pay_invalid_customer', '2025-01-01', NULL);PostgreSQL rejects the insert because customer_id = 999999 doesn't match a valid referenced row in customers.
Without the foreign key, the database could contain an order that refers to a customer that doesn't exist. The foreign key prevents this and keeps the relationship between the tables consistent. This is called referential integrity.
Referencing and referenced tables
In this relationship:
1orders.customer_id → customers.idorders contains the foreign key, so it is the referencing table.
customers contains the key being referenced, so it is the referenced table.
You may also hear these described informally as the child and parent tables.
A foreign key doesn't have to reference a primary key
In our database, orders.customer_id references the primary key customers.id.
That is the most common pattern, but a foreign key can also reference columns whose values are guaranteed to be unique through an appropriate unique constraint.
The important point is that the referenced value must identify a valid row.
Primary keys create an index
When you create a primary key, PostgreSQL automatically creates a unique B-tree index for it. That is why customers.id already has an index even though we didn't create one separately.
The primary key and the index serve different purposes.
The primary key expresses and enforces the rule that each customer must have a unique, non-NULL identifier. The index gives PostgreSQL an efficient structure for enforcing that uniqueness and looking up rows by the key.
A foreign key is different. PostgreSQL does not automatically create an index on the column containing the foreign key. For example, orders.customer_id is a foreign key, but it doesn't automatically receive an index just because of that relationship.
You saw why indexing such a column can sometimes be useful in the indexes and joins chapters.