Связь «многие ко многим» проще моделировать, если представить каждую пару отдельной записью. Один студент может проходить несколько курсов, а один курс — объединять нескольких студентов. Связующая таблица хранит каждую такую пару вместо списка идентификаторов курсов в строке студента.
Используйте пространство для ER-диаграмм Boardmix, чтобы разобраться со связью до реализации базы данных. В примере начнём со студентов и курсов, добавим связующую таблицу, а затем учтём деталь, которую легко упустить: один курс может запускаться повторно.

Определите смысл одной строки Enrollment
Представим учебный центр с курсами «Основы работы с данными» и «Визуальное планирование». Студент S01 проходит оба курса, а S02 — только «Основы работы с данными». На концептуальном уровне это связь «многие ко многим»: у студента может быть несколько курсов, у курса — несколько студентов.
В реляционной модели отдельные пары представляет связующая таблица. Документация Microsoft о связях «многие ко многим» описывает тот же подход: каждая строка промежуточной таблицы содержит внешние ключи на записи с обеих сторон. Между получившимися таблицами возникают связи «один ко многим», хотя исходное отношение по-прежнему остаётся связью «многие ко многим».
| student_id | course_id | Смысл записи |
|---|---|---|
| S01 | C10 | Студент S01 записан на «Основы работы с данными». |
| S01 | C20 | Тот же студент записан и на «Визуальное планирование». |
| S02 | C10 | Другой студент записан на «Основы работы с данными». |
Эти три строки не дублируют записи студентов. Сведения о студенте остаются в Student, сведения о курсе — в Course, а Enrollment хранит их связь. Исправление опечатки в имени не должно требовать изменения каждой записи на курс.
Нарисуйте две связи «один ко многим»
Поместите Enrollment между Student и Course. Для первого, упрощённого варианта действуют такие правила:
- У одного студента может быть от нуля до множества записей на курсы.
- Каждая запись на курс относится ровно к одному студенту.
- У одного курса может быть от нуля до множества записей студентов.
- Каждая строка Enrollment относится ровно к одному курсу.
Ноль здесь важен. Новый студент может ещё никуда не записаться, а только что созданный курс — пока не иметь участников. Но сама запись на курс без одного из двух концов не имеет смысла, поэтому в этой модели оба внешних ключа обязательны.
Не оставляйте на физической диаграмме таблиц дополнительную прямую линию между Student и Course, если у неё нет явно иного смысла. Иначе рисунок может выглядеть как требование реализовать и связующую таблицу, и ещё одну прямую связь. Концептуальная схема показывает исходное отношение, а схема таблиц — способ его представления.
Храните свойства участия в Enrollment
Для каждого значения спросите: оно описывает студента, курс или участие этого студента в этом курсе? Ответ помогает выбрать таблицу для поля.
| Поле | Таблица | Причина |
|---|---|---|
| student_name | Student | Имя не меняется для каждой записи на курс. |
| course_title | Course | Название относится к курсу, а не к отдельному участнику. |
| enrolled_at | Enrollment | Показывает, когда появилась эта связь. |
| enrollment_status | Enrollment | Студент может отказаться от одного курса, продолжая другой. |
| completion_result | Enrollment | Результат относится к участию конкретного студента, а не ко всем слушателям курса. |
Связующая таблица не обязательно ограничивается двумя идентификаторами. Здесь название Enrollment отражает понятие записи на курс со статусом и историей. Можно использовать и StudentCourse, но имя таблицы не должно скрывать смысл строки.
Явно определите жизненный цикл записи. При отказе от курса можно менять статус или хранить отдельные события. Удаление строки уничтожит историю связи. Правила хранения зависят от приложения: диаграмма должна показывать согласованную модель, а не молча считать любой отказ основанием для удаления.
Выбирайте ключ отдельно от правила уникальности
Если на каждую пару «студент — курс» допускается только одна запись, составным первичным ключом может быть (student_id, course_id). Строку определяет сочетание значений; отдельные столбцы не обязаны быть уникальными.
Другой вариант — отдельный первичный ключ enrollment_id и ограничение уникальности пары. Новый идентификатор сам по себе не помешает дважды записать S01 на C10. Если повторные пары недопустимы, укажите это правило отдельно.
Документация PostgreSQL об ограничениях различает первичные ключи, ограничения уникальности и внешние ключи. В нашем примере внешние ключи требуют существования указанных студента и курса; уникальность запрещает повторную пару; первичный ключ идентифицирует строку. Эти задачи связаны, но не взаимозаменяемы.
Диаграмма описывает правила, а база данных должна обеспечивать их выполнение. Линия с отметкой «1» или заметка «уникально» не препятствует записи неверных данных, если соответствующее ограничение не реализовано.
Учтите повторные запуски одного курса
Исходная пара становится слишком строгой, когда «Основы работы с данными» проходят весной и осенью. Если S01 может пройти курс повторно, (student_id, course_id) уже не отражает нужное правило. Но полная отмена уникальности разрешит и случайные дубли для одного запуска.
Добавьте CourseOffering — отдельный запуск курса в определённые сроки. У курса C10 могут быть запуск O101 весной и O102 осенью. Каждый запуск относится к одному курсу, а у курса может быть несколько запусков. Теперь Enrollment соединяет Student с CourseOffering.
Новой парой становится (student_id, offering_id). S01–O101 и S01–O102 — разные записи, обе могут быть допустимы. Повторная S01–O101 остаётся дублем, если студент может записаться на один запуск только один раз.
Дату начала и другие особенности конкретного запуска храните в CourseOffering, а не в Course. Название описывает сам курс, а запуск — когда он проходит. Если допускаются несколько попыток в рамках одного запуска, выделите это дополнительное понятие в модели вместо постоянного ослабления ключа записи.
Проверьте модель на конкретных записях в Boardmix
На доске Boardmix создайте четыре блока сущностей: Student, Enrollment, CourseOffering и Course. Поставьте Enrollment между студентом и запуском курса, а CourseOffering соедините с Course. Отметьте первичные и внешние ключи рядом с полями, а уникальность пары «студент — запуск» вынесите в отдельную заметку.
Под схемой разместите три карточки: S01–O101, S01–O102 и повторную S01–O101. Первые две отметьте как разные связи, а повторную пару — как дубль по принятому правилу. Так поведение модели легче обсудить, чем по одним названиям полей.

Откройте доску Boardmix и разберите несколько записей из своей предметной области. Может ли новая основная запись пока не иметь связей? Допустима ли повторная пара? Где должны храниться свойства самого участия?
Ответы могут изменить диаграмму. Для этого учебного центра важен не только вывод о необходимости дополнительной таблицы. Запись связывает студента с конкретным запуском, а не с абстрактным курсом раз и навсегда.