У связи «студент — курс» есть атрибут «дата зачисления». Где хранить его в нормализованной схеме?

У связи «студент — курс» есть атрибут «дата зачисления». Где хранить его в нормализованной схеме?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Атрибут «дата зачисления» нужно хранить в отдельной таблице связи между студентами и курсами, потому что он описывает не студента и не курс по отдельности, а конкретное их сочетание. Такая таблица обычно содержит внешние ключи на обе сущности, составной первичный ключ из них и атрибут даты.

Исторический контекст

Реляционная модель отделяет факты о самостоятельных сущностях от фактов об их отношениях. Это предотвращает дублирование данных и аномалии обновления, возникающие, когда свойства связи помещают в одну из таблиц сущностей.

Для отношений «многие ко многим» отдельная таблица связи нужна не только для хранения самих пар. Она также является естественным местом для атрибутов, относящихся к каждой паре: даты зачисления, роли участника, количества или статуса.

Постановка проблемы

Один студент может быть записан на несколько курсов, а один курс может включать многих студентов. Поэтому дата зачисления не может корректно находиться только в таблице студентов или только в таблице курсов: в обоих случаях для одной сущности потребовались бы повторяющиеся значения.

Хранение даты в таблице студентов приводит к потере информации о зачислении на разные курсы либо к множественным столбцам. Хранение её в таблице курсов аналогично не позволяет выразить разные даты зачисления разных студентов. Попытка записывать дату в произвольной таблице также создаёт риск противоречивых и дублирующихся фактов.

Подробное решение

Нужно создать таблицу связи, например enrollment, с внешними ключами student_id и course_id, атрибутом enrolled_at и составным первичным ключом (student_id, course_id).

CREATE TABLE enrollment ( student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, enrolled_at DATE NOT NULL, PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) );

Составной первичный ключ запрещает повторно записать одну и ту же пару студента и курса. Внешние ключи гарантируют, что зачисление ссылается на существующие сущности, а NOT NULL не допускает неполную связь.

Если одна и та же пара может появляться несколько раз, например после отмены и повторного зачисления, в ключ нужно включить дополнительный идентификатор попытки или период действия. Это уже другая бизнес-модель: простого составного ключа из двух внешних ключей будет недостаточно.

Отдельный суррогатный идентификатор для строки связи допустим, но он не заменяет ограничение уникальности пары, если бизнес-правило запрещает повторное зачисление. Иначе база позволит создать две строки с одинаковыми студентом и курсом.

Ситуация из практики

В образовательной системе сначала добавили enrolled_at в таблицу студентов. Решение было простым, но не отражало многократное обучение: студент мог одновременно посещать несколько курсов, а дата в одной строке имела неоднозначный смысл.

Рассматривались два варианта. Набор отдельных столбцов вроде course_1, date_1, course_2, date_2 требовал заранее ограничить число курсов и усложнял запросы. Хранение списка курсов и дат в одном текстовом поле экономило таблицы, но нарушало атомарность значений и лишало базу нормальных внешних ключей.

Выбрали таблицу enrollment с составным ключом и внешними ключами. В результате запросы по истории обучения стали обычными реляционными операциями, повторные зачисления контролируются ограничениями, а добавление нового курса не требует изменения схемы.

Что кандидаты часто упускают

  1. Достаточно ли внешних ключей без первичного ключа или UNIQUE?

Нет. Внешние ключи проверяют существование студента и курса, но не уникальность самой пары. Без дополнительного ограничения можно несколько раз записать одно и то же зачисление, что приведёт к дублированию факта и неоднозначным датам.

  1. Почему дату зачисления нельзя считать атрибутом курса?

Потому что у одного курса разные студенты могут быть зачислены в разные даты. Функционально дата определяется парой «студент — курс», а не одним курсом. Поэтому она должна находиться в отношении, ключ которого включает оба идентификатора.

  1. Когда таблица связи должна получить собственный идентификатор?

Собственный идентификатор полезен, если строка связи сама становится объектом, на который ссылаются другие таблицы, либо если допускаются несколько периодов или попыток для одной пары. Но даже при наличии такого идентификатора нужно отдельно задать UNIQUE для тех атрибутов, чью уникальность требует бизнес-правило.