| Lesson 11 | Resolving many-to-many relationships |
| Objective | Implement a many-to-many relationship with an associative table, two foreign keys and an appropriate uniqueness rule. |
To implement a many-to-many relationship in relational tables, create an associative table that records each pairing. For students and classes, use Enrollment: each row identifies one student enrolled in one class. Student and Class retain their own facts, while Enrollment holds the references and facts about the enrollment.
The business relationship remains many-to-many. Its table representation uses two one-to-many relationships: Student to Enrollment, and Class to Enrollment. This resolves the storage problems examined in Lesson 10.
In this example, ClassID identifies a particular scheduled class offering, and a student can have at most one Enrollment row for that offering. Students may exist before enrolling, and classes may exist before anyone enrolls. If your system records repeated attempts, terms or enrollment history separately, define those identities before choosing the key.
A student with no classes is represented by a Student row and no Enrollment rows, not an Enrollment row with a NULL ClassID. Required foreign keys enforce the references in existing enrollment rows; they do not force every parent to have a child row.
The following SQL uses explicit identifiers rather than vendor-specific automatic numbering. Run it in an empty practice schema. Grade is nullable because a new enrollment may not yet have a grade.
CREATE TABLE Student (
StudentID INT NOT NULL PRIMARY KEY,
LastName VARCHAR(60) NOT NULL,
FirstName VARCHAR(60) NOT NULL
);
CREATE TABLE Class (
ClassID INT NOT NULL PRIMARY KEY,
ClassName VARCHAR(120) NOT NULL,
ClassDept VARCHAR(100)
);
CREATE TABLE Enrollment (
StudentID INT NOT NULL,
ClassID INT NOT NULL,
EnrolledOn DATE NOT NULL,
Grade VARCHAR(5),
CONSTRAINT pk_enrollment PRIMARY KEY (StudentID, ClassID),
CONSTRAINT fk_enrollment_student
FOREIGN KEY (StudentID) REFERENCES Student(StudentID),
CONSTRAINT fk_enrollment_class
FOREIGN KEY (ClassID) REFERENCES Class(ClassID)
);
CREATE INDEX ix_enrollment_class ON Enrollment(ClassID);
The primary key prevents a duplicate pair. It does not make StudentID individually unique or ClassID individually unique. Repeated individual identifiers are essential to representing many-to-many associations.
The example does not request cascading deletes. With foreign-key enforcement enabled, a referenced parent cannot simply be deleted while its enrollments remain. Choose retention and deletion policies deliberately; deleting an enrollment itself does not delete its parents.
Many database engines support the composite primary key with an index whose leading column is StudentID. In such an engine, another index on StudentID alone may be unnecessary. The ClassID index supports lookups from the other direction; inspect your engine's indexes and query plans before adding more.
INSERT INTO Student (StudentID, LastName, FirstName) VALUES
(53985, 'Jones', 'Cindy'),
(62292, 'Cruz', 'Leo');
INSERT INTO Class (ClassID, ClassName, ClassDept) VALUES
(854, 'Finance', 'Business'),
(768, 'Statistics', 'Math');
INSERT INTO Enrollment (StudentID, ClassID, EnrolledOn, Grade) VALUES
(53985, 854, '2026-09-01', 'A'),
(53985, 768, '2026-09-01', 'B'),
(62292, 854, '2026-09-02', 'A');
SELECT s.FirstName, s.LastName, c.ClassName, e.Grade
FROM Enrollment AS e
JOIN Student AS s ON s.StudentID = e.StudentID
JOIN Class AS c ON c.ClassID = e.ClassID
ORDER BY s.StudentID, c.ClassID;
| FirstName | LastName | ClassName | Grade |
|---|---|---|---|
| Cindy | Jones | Statistics | B |
| Cindy | Jones | Finance | A |
| Leo | Cruz | Finance | A |
This is a three-row subset of the earlier lessons' enrollment example, with illustrative dates and grades added. Cindy appears twice in the report because she has two enrollments. Her name is still stored once in Student. Likewise, Finance is stored once in Class even though two students take it.
An associative entity[1] does not have to use a composite primary key. A separate EnrollmentID can be useful when other tables must reference a particular enrollment. It does not, by itself, prevent duplicate student/class pairs.
The following is an alternative Enrollment definition, not a second table to create alongside the first. It uses the same parent tables. Supply unique EnrollmentID values, or adapt that column to your engine's identity or sequence syntax.
CREATE TABLE Enrollment (
EnrollmentID INT NOT NULL PRIMARY KEY,
StudentID INT NOT NULL,
ClassID INT NOT NULL,
EnrolledOn DATE NOT NULL,
Grade VARCHAR(5),
CONSTRAINT uq_enrollment_pair UNIQUE (StudentID, ClassID),
CONSTRAINT fk_enrollment_student
FOREIGN KEY (StudentID) REFERENCES Student(StudentID),
CONSTRAINT fk_enrollment_class
FOREIGN KEY (ClassID) REFERENCES Class(ClassID)
);
Under the same business rule, both designs enforce the same pair uniqueness. Adding more enrollment attributes is not, by itself, a reason to replace the composite key[2]. If multiple attempts for the same class offering are allowed, the row identity and uniqueness rule must include the relevant attempt or event; merely adding a surrogate key leaves that rule undefined.
A composite primary key[3] identifies the pairing; foreign keys validate its members. Neither constraint alone expresses every business rule. The next lesson develops mandatory and optional participation.
Practice choosing the associative entity and placing its keys in the Resolving Many-to-Many Relationships exercise.