Course topics

By WebNest Studio Engineering Team

Database Design Tutorial

Relationship Cardinality in Database Design

Cardinality describes how many instances of one entity can be associated with how many instances of another. Every relationship in a schema is one of three basic shapes — one-to-one, one-to-many, or many-to-many — and the shape determines exactly how the relationship must be implemented physically: as a foreign key on one side, a foreign key on the "many" side, or an entirely separate join table.

Getting cardinality wrong is one of the most expensive mistakes to fix later, because it usually means the physical structure of a table needs to change, not just a constraint. Deciding cardinality correctly during design — by asking "can there be more than one?" in both directions — avoids that rework.

Why it matters

Cardinality is how many rows on one side can relate to how many on the other, and each case has exactly one correct table structure. Modelling a many-to-many relationship as one-to-many is among the most common design mistakes, and it shows up later as data that simply cannot be recorded.

One-to-One

A one-to-one relationship means each row in table A relates to at most one row in table B, and vice versa. It is implemented by putting a foreign key with a UNIQUE constraint on one of the two tables (often the one that is optional or less frequently accessed). One-to-one relationships are commonly used to split a large or sensitive table — for example, separating rarely-needed profile details or sensitive data from a frequently-queried core users table.

One-to-Many

A one-to-many relationship means one row in table A can relate to many rows in table B, but each row in table B relates to only one row in table A. This is the most common relationship shape (one customer has many orders, one department has many employees) and is implemented with a plain foreign key on the "many" side, as described in the relationships topic.

Many-to-Many and the Join Table

A many-to-many relationship means rows on both sides can relate to multiple rows on the other side — one order can contain many products, and one product can appear on many orders. Relational databases cannot express many-to-many directly with a single foreign key; it must be resolved with a join table (also called an associative or junction table) that holds a foreign key to each side, and typically uses the combination of both as its composite primary key.

  • One-to-one: FK with UNIQUE constraint on one side (e.g. users.id ↔ user_profiles.user_id UNIQUE).
  • One-to-many: plain FK on the "many" side (e.g. orders.customer_id).
  • Many-to-many: a join table with FKs to both sides (e.g. order_items with order_id and product_id).

Example

All three cardinalities modeled in one schema: users↔profile, author↔posts, students↔courses

SQL
-- One-to-one: each user has exactly one extended profile
CREATE TABLE users (
    id     BIGINT PRIMARY KEY,
    email  VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE user_profiles (
    user_id    BIGINT PRIMARY KEY REFERENCES users(id),
    bio        TEXT,
    avatar_url VARCHAR(500)
);

-- One-to-many: one author writes many posts
CREATE TABLE posts (
    id         BIGINT PRIMARY KEY,
    author_id  BIGINT NOT NULL REFERENCES users(id),
    title      VARCHAR(200) NOT NULL
);

-- Many-to-many: students enroll in many courses, courses have many students
CREATE TABLE students (
    id    BIGINT PRIMARY KEY,
    name  VARCHAR(150) NOT NULL
);

CREATE TABLE courses (
    id    BIGINT PRIMARY KEY,
    title VARCHAR(150) NOT NULL
);

CREATE TABLE enrollments (
    student_id  BIGINT NOT NULL REFERENCES students(id),
    course_id   BIGINT NOT NULL REFERENCES courses(id),
    enrolled_on DATE NOT NULL DEFAULT CURRENT_DATE,
    PRIMARY KEY (student_id, course_id)
);
Output
user_profiles.user_id being both the primary key and a foreign key
enforces exactly one profile per user (one-to-one). posts.author_id
allows an author to have many posts, but each post has exactly one
author (one-to-many). enrollments resolves the many-to-many between
students and courses; the composite primary key (student_id, course_id)
prevents the same student from enrolling in the same course twice.

Common Mistakes

  • Trying to model a many-to-many relationship with a single foreign key on one of the two tables, which can only express one-to-many.
  • Forgetting the UNIQUE constraint on a one-to-one foreign key, which silently allows it to become a one-to-many relationship.
  • Storing a comma-separated list of related IDs in a single column instead of creating a proper join table, which breaks referential integrity and makes queries and indexing painful.
  • Not adding extra attributes (like enrolled_on, or order line quantity) that belong to the relationship itself, rather than to either entity, onto the join table.

Exercise

A user has at most one profile. Enforce that one-to-one relationship in the profiles table so that the second insert below, a second profile for user 1, is rejected. Then count the profiles.

Expected output: profiles then 1

SQL · starter code
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);

-- TODO: profiles (id, user_id, bio) where each user can have only one profile

INSERT INTO users (id, name) VALUES (1, 'Asha');
INSERT INTO profiles (id, user_id, bio) VALUES (1, 1, 'Developer');
INSERT INTO profiles (id, user_id, bio) VALUES (2, 1, 'Second profile');

SELECT COUNT(*) AS profiles FROM profiles;
Show hints
  • A foreign key alone allows many profiles per user.
  • Adding UNIQUE to the foreign key column limits each user to one row.
Show solution
SQL · solution
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);

CREATE TABLE profiles (
  id INTEGER PRIMARY KEY,
  user_id INTEGER NOT NULL UNIQUE REFERENCES users(id),
  bio TEXT
);

INSERT INTO users (id, name) VALUES (1, 'Asha');
INSERT INTO profiles (id, user_id, bio) VALUES (1, 1, 'Developer');
INSERT INTO profiles (id, user_id, bio) VALUES (2, 1, 'Second profile');

SELECT COUNT(*) AS profiles FROM profiles;

Quick quiz

1. An author writes many posts, and each post has one author. Where does the foreign key go?

  • A. In the authors table
  • B. In a junction table
  • C. In both tables
  • D. In the posts table
Show answer

D. The foreign key is always on the "many" side of a one-to-many relationship.

2. How is a one-to-one relationship enforced?

  • A. With a foreign key that also has a UNIQUE constraint
  • B. With a junction table
  • C. With two foreign keys
  • D. It cannot be enforced
Show answer

A. The unique constraint stops a second child from pointing at the same parent.

3. Students take many courses and courses have many students. What is needed?

  • A. A course_id column in students
  • B. A third table with one row per student and course pair
  • C. A student_id column in courses
  • D. A list of ids in a text column
Show answer

B. A junction table turns one many-to-many relationship into two one-to-many relationships.

Interview questions

How do you model a many-to-many relationship?

Show answer

With a junction table, also called a join or associative table. It has a foreign key to each of the two tables, and its primary key is usually the pair of those keys, which prevents the same pairing being recorded twice. The junction table can also carry attributes of the relationship itself, such as the date a student enrolled in a course or the grade obtained.

When would you split a table into two with a one-to-one relationship?

Show answer

When a group of columns is optional and usually empty, when it is large and rarely read, so that keeping it apart makes the main table smaller, or when it needs different access rules, such as sensitive personal details. The second table uses the first table's key as both its primary key and a foreign key. Otherwise a single table is simpler and avoids a join.

Key Points to Remember

  • One-to-one uses a foreign key with a UNIQUE constraint; one-to-many uses a plain foreign key on the "many" side.
  • Many-to-many relationships require a join table with foreign keys to both entities, since a single foreign key column cannot express them.
  • A composite primary key on the join table (both foreign keys together) prevents duplicate associations.
  • Attributes that describe the relationship itself (quantity, enrollment date, price at time of purchase) belong on the join table, not on either entity.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering