9.8. One-to-Many Relationships
Table of Contents
Understanding One-to-Many Relationships
One-to-many relationships are at the core of relational database design. You will use them in almost every real application, from blogs and shops to authentication systems.
In this chapter we focus on what makes one-to-many relationships unique, how to recognize them, and how to implement them in SQL, especially in PostgreSQL.
What Is a One-to-Many Relationship?
A one-to-many relationship exists when:
- One record in table A can be associated with many records in table B.
- Each record in table B is associated with exactly one record in table A.
You can think of it as:
- One author has many books.
- One user has many orders.
- One category has many products.
- One blog post has many comments.
We usually describe this as:
A (one) → B (many)
Conceptual Example
Imagine a simple blogging system:
- One user can write many posts.
- Each post is written by exactly one user.
Tables conceptually:
users(one side)posts(many side)
Relationship:
- User 1 → Post 1, Post 2, Post 3
- User 2 → Post 4
Every post belongs to a single user, but each user can have multiple posts.
Identifying One-to-Many in Real Projects
When you design a database, you first think about entities and their relationships.
You usually have a one-to-many relationship when:
- You can complete the sentence:
“One X has many Y”
and also
“Each Y belongs to exactly one X.”
Some common patterns:
| Domain | One side | Many side | Sentence |
|---|---|---|---|
| E-commerce | users | orders | One user has many orders |
| E-commerce | orders | order_items | One order has many order items |
| Blogging | posts | comments | One post has many comments |
| Education | courses | lessons | One course has many lessons |
| Company | departments | employees | One department has many employees |
| Social network | users | posts | One user has many posts |
If, instead, you can say “many X can belong to many Y,” that is a many-to-many relationship, which is handled differently and is covered in its own chapter.
How One-to-Many Is Implemented in SQL
Technically, a one-to-many relationship is implemented using a foreign key on the many side of the relationship.
Rule:
In a one-to-many relationship, the table on the many side contains a foreign key column that references the primary key of the table on the one side.
So for:
- One
userhas manyposts
We create:
userswith a primary keyidpostswith a foreign keyuser_idthat referencesusers(id)
Basic structure:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
content TEXT NOT NULL
);Here:
users.idis the primary key on the one side.posts.user_idis the foreign key on the many side.
One-to-Many: Parent and Child Tables
In a one-to-many relationship we often say:
- Parent table for the one side.
- Child table for the many side.
Typical structure:
| Role | Table | Key column | Description |
|---|---|---|---|
| Parent | users | id | One user row |
| Child | posts | user_id FK | Many posts, each linked to one user |
You can read it like this:
- A parent row in
userscan be linked to 0, 1, or many child rows inposts. - A child row in
postsmust be linked to exactly one parent row inusers(ifuser_idis NOT NULL).
Creating One-to-Many Relationships in SQL
Let us walk through a few examples in SQL to make this concrete.
Example 1: Users and Orders
Business rule
- One user can place many orders.
- Each order belongs to exactly one user.
Table design
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
full_name TEXT NOT NULL
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
total_cents INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);Explanation:
users.idis the primary key.orders.user_idis a foreign key that points tousers.id.- The one-to-many relationship is enforced by the database through this foreign key.
Inserting Related Data
-- Insert two users (parents)
INSERT INTO users (email, full_name)
VALUES
('alice@example.com', 'Alice'),
('bob@example.com', 'Bob');
-- Insert orders for Alice (id = 1)
INSERT INTO orders (user_id, total_cents)
VALUES
(1, 1500),
(1, 3200);
-- Insert one order for Bob (id = 2)
INSERT INTO orders (user_id, total_cents)
VALUES
(2, 5000);Here:
- User 1 (Alice) has 2 orders.
- User 2 (Bob) has 1 order.
Query: Get All Orders for a User
SELECT *
FROM orders
WHERE user_id = 1;
This returns all orders for user with id = 1.
Query: Join Parent and Children
SELECT
u.full_name,
o.id AS order_id,
o.total_cents,
o.created_at
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.id = 1;
This joins users (one side) with orders (many side) using the foreign key.
Cardinality and Optional Relationships
You can constrain one-to-many in different ways, for example:
- Mandatory child reference
A child must have a parent. - Optional child reference
A child may or may not have a parent.
This is controlled by whether the foreign key column can be NULL.
Mandatory Relationship (Most Common)
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
title TEXT NOT NULL
);
Here, every post must have a user_id. The database will reject a post without a user.
Optional Relationship
Sometimes a relationship is optional. For example, maybe an article can be written by the “system” without a specific author.
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
author_id INTEGER REFERENCES users(id), -- NULL allowed
title TEXT NOT NULL
);
Here, author_id can be NULL. This means:
- An article might not have an associated user.
- When present,
author_idmust be a validusers.id.
One-to-Many and Cascading Behavior
When you have one-to-many relationships, you must decide what happens when:
- The parent row is deleted.
- The parent key changes.
This is handled by foreign key actions such as ON DELETE and ON UPDATE.
Common ON DELETE Strategies
| Strategy | Behavior when parent is deleted |
|---|---|
ON DELETE RESTRICT | Prevent deletion if child rows exist |
ON DELETE NO ACTION | Similar to RESTRICT in many databases |
ON DELETE CASCADE | Automatically delete child rows |
ON DELETE SET NULL | Set the foreign key in children to NULL |
Rule of thumb:
Use ON DELETE CASCADE only when it is safe to remove all related child rows automatically, for example, for temporary or dependent data. For important business data, prefer RESTRICT or handle deletion manually.
Example: Posts and Comments
- One
posthas manycomments. - When a post is removed, you usually want to remove its comments too.
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL
);
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
content TEXT NOT NULL
);Now:
DELETE FROM posts WHERE id = 10;This will:
- Delete post 10.
- Automatically delete all comments whose
post_id = 10.
Example: Departments and Employees
- One department has many employees.
- You usually do not want to delete employees automatically if a department is removed, or you may even want to prevent deleting a department that still has employees.
CREATE TABLE departments (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
department_id INTEGER NOT NULL REFERENCES departments(id),
full_name TEXT NOT NULL
);
Here, if you try to delete a department that still has employees, the database will block the operation (default behavior similar to RESTRICT).
You can then:
- Move employees to another department first.
- Or decide to use
ON DELETE SET NULLif it is acceptable that employees temporarily have no department.
Practical Modeling Patterns
Let us look at some typical one-to-many designs you will meet as a backend developer.
Blog: Posts and Comments
Business rule
- A post can have many comments.
- Each comment belongs to exactly one post.
Tables
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL
);
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
author_name TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);Queries
Get all comments for a post:
SELECT *
FROM comments
WHERE post_id = 5
ORDER BY created_at;Get posts with their comment count:
SELECT
p.id,
p.title,
COUNT(c.id) AS comment_count
FROM posts p
LEFT JOIN comments c ON c.post_id = p.id
GROUP BY p.id, p.title
ORDER BY comment_count DESC;
Here, we are using GROUP BY and COUNT on the many side, which is a common pattern with one-to-many relationships.
E-commerce: Orders and Order Items
Business rule
- One order can have many order items.
- Each order item belongs to exactly one order.
Tables
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id INTEGER NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL,
unit_price_cents INTEGER NOT NULL
);Query: Get an Order with Its Items
SELECT
o.id AS order_id,
o.created_at,
oi.product_id,
oi.quantity,
oi.unit_price_cents
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.id = 42;Query: Calculate Order Total
SELECT
o.id AS order_id,
SUM(oi.quantity * oi.unit_price_cents) AS total_cents
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.id = 42
GROUP BY o.id;Again, you see the pattern:
- Join parent (one) with children (many).
- Group by the parent to summarize children.
Avoiding Common Mistakes
Mistake 1: Storing Lists in One Column
Sometimes beginners try to store multiple values in one column, for example:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL,
order_ids TEXT -- "1,2,3" or JSON
);This is not correct for a relational database, because:
- It is hard to query, filter, and enforce integrity.
- The database cannot check if those IDs actually exist in
orders.
Use a proper one-to-many relationship instead:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id)
);Mistake 2: Putting the Foreign Key on the Wrong Side
For a one-to-many relationship:
- The foreign key must be on the many side.
Wrong:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
last_order_id INTEGER REFERENCES orders(id) -- this does not model one-to-many
);Right:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id)
);
You might still have last_order_id as a derived convenience column, but it is not what defines the relationship.
Mistake 3: Forgetting Indexes on Foreign Keys
Foreign key columns are used often in queries and joins. Without an index, those queries can become slow as data grows.
In most databases:
- You should create an index on foreign key columns.
Example:
CREATE INDEX idx_orders_user_id ON orders(user_id);Some databases create indexes automatically for foreign keys, but not all. PostgreSQL does not create an index for foreign keys automatically, so you should do it explicitly for performance.
One-to-Many in Application Code
Although implementation details depend on the programming language and ORM, the underlying concept is always:
- A child object has a foreign key field pointing to the parent.
- The parent object can expose a collection of children.
Pseudo Python with an ORM-like syntax:
class User(Base):
id = Column(Integer, primary_key=True)
email = Column(String, unique=True)
class Post(Base):
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
title = Column(String)Conceptually:
Post.user_idis the foreign key.- A
Userobject can fetchpoststhrough that key.
Although ORMs make it more convenient, the database structure is still the same one-to-many pattern you learned here.
Summary
In this chapter you learned that:
- A one-to-many relationship links one parent row to many child rows.
- It is implemented by placing a foreign key in the many side table that references the primary key of the one side.
- Cardinality and optionality are controlled by
NOT NULLand foreign key actions likeON DELETE CASCADE. - You use one-to-many relationships for common patterns such as users and posts, orders and order items, departments and employees.
- Proper design avoids storing lists in a single column and ensures foreign keys are indexed.
You will use one-to-many relationships constantly when designing schemas and building backends, and they form the basis for more advanced relationship types you will see next.
Views: 8
KAHIBARO