backend · 7 read

How to Model Many-to-Many Relationships (Junction Tables)

A clear guide to modelling many-to-many relationships in SQL: what a junction table is, how to build one, and how to store extra data about the relationship.

A many-to-many relationship is one where each side can link to many of the other: a student takes many courses, and each course has many students; an order contains many products, and each product appears on many orders. SQL tables cannot express that directly, so you model it with a third table that sits between the two. It is one of the most common patterns in database design, and getting it right early saves a lot of pain later.

Why you can't do it with a foreign key

A plain foreign key handles one-to-many. An order belongs to one customer, so orders carries a customer_id. That works because each order has exactly one customer.

It falls apart for many-to-many. You cannot put a course_id on the students table, because a student has many courses, and you cannot put a student_id on courses for the same reason. Cramming a list of IDs into a single column breaks first normal form and makes the data impossible to query or index properly. The relationship needs its own home.

The junction table

That home is a junction table (also called an associative, join, or link table). It holds a foreign key to each side, and each row represents one link.

CREATE TABLE students (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(120) NOT NULL
);

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

CREATE TABLE enrollments (
  student_id BIGINT NOT NULL,
  course_id  BIGINT NOT NULL,
  PRIMARY KEY (student_id, course_id),
  FOREIGN KEY (student_id) REFERENCES students(id),
  FOREIGN KEY (course_id)  REFERENCES courses(id)
);

Two things make this correct:

  • The composite primary key (student_id, course_id) guarantees the same student cannot be enrolled in the same course twice.
  • The two foreign keys keep the data honest: you cannot enroll a student or course that does not exist, and deleting one can be made to clean up its links.

Storing data about the relationship

The junction table is not just a pair of IDs. Any fact that describes the link itself, rather than either side alone, belongs here. When did the student enrol? What is their role on a project? That data depends on both sides at once, so it has nowhere else to live:

CREATE TABLE enrollments (
  student_id  BIGINT NOT NULL,
  course_id   BIGINT NOT NULL,
  enrolled_at DATE NOT NULL,
  grade       CHAR(2),
  PRIMARY KEY (student_id, course_id),
  FOREIGN KEY (student_id) REFERENCES students(id),
  FOREIGN KEY (course_id)  REFERENCES courses(id)
);

enrolled_at and grade are facts about this particular enrolment, not about the student or the course in general. This is exactly where they should sit.

Reading across it

To list a student's courses, you JOIN through the junction table:

SELECT c.title, e.enrolled_at
FROM enrollments e
JOIN courses c ON c.id = e.course_id
WHERE e.student_id = 42;

Index the foreign keys so these lookups stay fast. The composite primary key already indexes (student_id, course_id); add an index on (course_id, student_id) too if you often query from the course side, so "who is in this course" is just as quick.

A common mistake to avoid

Do not give the junction table a single surrogate id and then forget to enforce uniqueness on the pair. If you prefer a surrogate key, that is fine, but keep a UNIQUE (student_id, course_id) constraint, or you will end up with duplicate links and no way to tell which is real. This pattern shows up throughout the database design examples guide, from order lines to social-media follows.

Frequently asked questions

How do you represent a many-to-many relationship in SQL?

With a third table, called a junction or associative table, that holds a foreign key to each of the two related tables. Each row links one record on each side, so a student can have many courses and a course can have many students, all through rows in the junction table.

What should the primary key of a junction table be?

Usually a composite primary key made of the two foreign keys together, for example (student_id, course_id). That enforces uniqueness so the same pair cannot be linked twice. Some teams add a separate surrogate id as well, which is fine, but keep a unique constraint on the pair.

Can a junction table hold extra columns?

Yes, and that is one of its strengths. Data that describes the relationship itself, such as the date a student enrolled or their role on a project, belongs on the junction table, because it depends on both sides at once rather than on either one alone.

What is the difference between a junction table and a join?

They are different things with similar names. A junction table is a permanent table in your schema that stores the relationship. A JOIN is a query operation that combines rows from tables at read time. You use a JOIN to read across a junction table.


Modelling relationships that are getting tangled, or reviewing a schema before it locks in? That is database consulting work. The full database design examples guide walks through more real schemas.

Need help with backend?

Let's discuss your project.

Book a discovery call