У связи «студент — курс» есть атрибут «дата зачисления». Где хранить его в нормализованной схеме?
Атрибут «дата зачисления» нужно хранить в отдельной таблице связи между студентами и курсами, потому что он описывает не студента и не курс по отдельности, а конкретное их сочетание. Такая таблица обычно содержит внешние ключи на обе сущности, составной первичный ключ из них и атрибут даты.
Реляционная модель отделяет факты о самостоятельных сущностях от фактов об их отношениях. Это предотвращает дублирование данных и аномалии обновления, возникающие, когда свойства связи помещают в одну из таблиц сущностей.
Для отношений «многие ко многим» отдельная таблица связи нужна не только для хранения самих пар. Она также является естественным местом для атрибутов, относящихся к каждой паре: даты зачисления, роли участника, количества или статуса.
Один студент может быть записан на несколько курсов, а один курс может включать многих студентов. Поэтому дата зачисления не может корректно находиться только в таблице студентов или только в таблице курсов: в обоих случаях для одной сущности потребовались бы повторяющиеся значения.
Хранение даты в таблице студентов приводит к потере информации о зачислении на разные курсы либо к множественным столбцам. Хранение её в таблице курсов аналогично не позволяет выразить разные даты зачисления разных студентов. Попытка записывать дату в произвольной таблице также создаёт риск противоречивых и дублирующихся фактов.
Нужно создать таблицу связи, например enrollment, с внешними ключами student_id и course_id, атрибутом enrolled_at и составным первичным ключом (student_id, course_id).
Составной первичный ключ запрещает повторно записать одну и ту же пару студента и курса. Внешние ключи гарантируют, что зачисление ссылается на существующие сущности, а NOT NULL не допускает неполную связь.
Если одна и та же пара может появляться несколько раз, например после отмены и повторного зачисления, в ключ нужно включить дополнительный идентификатор попытки или период действия. Это уже другая бизнес-модель: простого составного ключа из двух внешних ключей будет недостаточно.
Отдельный суррогатный идентификатор для строки связи допустим, но он не заменяет ограничение уникальности пары, если бизнес-правило запрещает повторное зачисление. Иначе база позволит создать две строки с одинаковыми студентом и курсом.
В образовательной системе сначала добавили enrolled_at в таблицу студентов. Решение было простым, но не отражало многократное обучение: студент мог одновременно посещать несколько курсов, а дата в одной строке имела неоднозначный смысл.
Рассматривались два варианта. Набор отдельных столбцов вроде course_1, date_1, course_2, date_2 требовал заранее ограничить число курсов и усложнял запросы. Хранение списка курсов и дат в одном текстовом поле экономило таблицы, но нарушало атомарность значений и лишало базу нормальных внешних ключей.
Выбрали таблицу enrollment с составным ключом и внешними ключами. В результате запросы по истории обучения стали обычными реляционными операциями, повторные зачисления контролируются ограничениями, а добавление нового курса не требует изменения схемы.
Нет. Внешние ключи проверяют существование студента и курса, но не уникальность самой пары. Без дополнительного ограничения можно несколько раз записать одно и то же зачисление, что приведёт к дублированию факта и неоднозначным датам.
Потому что у одного курса разные студенты могут быть зачислены в разные даты. Функционально дата определяется парой «студент — курс», а не одним курсом. Поэтому она должна находиться в отношении, ключ которого включает оба идентификатора.
Собственный идентификатор полезен, если строка связи сама становится объектом, на который ссылаются другие таблицы, либо если допускаются несколько периодов или попыток для одной пары. Но даже при наличии такого идентификатора нужно отдельно задать UNIQUE для тех атрибутов, чью уникальность требует бизнес-правило.