Table Movie { Title varchar (256) pk ReleaseDa...
Создано: 20 сентября 2026 г.
Отвечено с использованием GPT-5.6 Thinking от Chat01
Создано: 20 сентября 2026 г.
Отвечено с использованием GPT-5.6 Thinking от Chat01
Table Movie {
Title varchar (256) pk
ReleaseDate date
Director varchar (128)
Genre varchar (1024)
WasWatched boolean
}
Table Movie_Cast {
Title varchar (256) pk
Last_Name varchar (64) [pk]
First_Name varchar (64) [pk]
}
Table Cast {
LastName varchar (64) [pk]
firstName varchar (64) [pk]
birthday date
gender varchar (32)
}
Ref{
Movie.Title< Movie_Cast.Title
}
Ref: Cast.(firstName, LastName) < Movie_Cast.(First_Name, Last_Name)
Сделай задание, распиши подробно. Диаграммы в первом и втором задании делай через dbdiogramio
Ниже — готовое подробное решение, которое можно использовать как основу для отчёта. Я сделаю две схемы в формате DBML для dbdiagram.io: первая — результат нормализации SPBU_DB_2, вторая — исправленная и расширенная БД фильмов. Это соответствует смыслу задания: сначала требуется привести исходную таблицу к НФБК, затем проанализировать текущую схему фильмов и на основании найденных проблем спроектировать новую. В условии отдельно указано, что предполагается использование естественных ключей, поэтому id serial, movie_id, person_id и подобные искусственные ключи я не использую. (DB2026[2].docx) (DB2026[2].docx)
В исходной таблице находятся данные о клиенте, его отделении, договоре и платежах: клиент, тип клиента, страна, отделение, номер договора, дата заключения, статус, сумма, аванс и до трёх платежей. На странице с исходными данными видно, что один и тот же клиент встречается несколько раз для разных договоров, например ООО «Автомобили». (SPBU_DB_2.pdf)
Исходное отношение можно записать так:
textR( Клиент, ТипКлиента, Страна, ОтделениеКлиента, НомерДоговора, ДатаЗаключения, СтатусДоговора, СуммаДоговора, Аванс, Оплата1, Оплата2, Оплата3 )
Первая проблема — дублирование информации о клиенте. Например, ООО «Автомобили» встречается в таблице более одного раза. Вместе с каждым договором повторяются тип клиента, страна и отделение. Более того, для одного и того же клиента страна записана как Россия и как РФ, то есть уже появилась аномалия обновления данных. (SPBU_DB_2.pdf)
Вторая проблема — поля:
textОплата 1 Оплата 2 Оплата 3
представляют собой повторяющуюся группу. Формально отдельные значения в ячейках являются атомарными, однако сама структура ограничивает число платежей тремя. Если понадобится четвёртая оплата, придётся менять структуру таблицы и добавлять новую колонку.
Третья проблема — в одной строке смешаны данные сразу о нескольких сущностях: о клиенте, его отделении, договоре и платежах.
Из-за этого возникают классические аномалии.
| Аномалия | Пример |
|---|---|
| Обновления | если страна клиента изменилась или исправляется РФ → Россия, её приходится менять во всех строках |
| Вставки | трудно добавить нового клиента, пока у него нет договора |
| Удаления | при удалении последнего договора можно случайно потерять сведения о клиенте |
| Структурная | нельзя сохранить более трёх платежей без ALTER TABLE |
Кроме того, последняя строка исходных данных содержит клиента «Григорий», отделение С.Петербург, статус Обсуждается и сумму 500, но не содержит номера договора и даты заключения. (SPBU_DB_2.pdf) Это отдельная важная проблема: такая строка фактически описывает ещё не заключённый договор, поэтому естественного ключа НомерДоговора у неё пока нет.
Исходя из смысла данных, можно выделить следующие зависимости.
Для клиента:
textКлиент → ТипКлиента, Страна
То есть у клиента имеются собственные характеристики, которые не должны зависеть от конкретного договора.
Для договора:
textНомерДоговора → Клиент, ОтделениеКлиента, ДатаЗаключения, СтатусДоговора, СуммаДоговора, Аванс
Здесь предполагается, что номер договора является уникальным естественным бизнес-ключом.
Для платежей после преобразования структуры:
text(НомерДоговора, НомерПлатежа) → СуммаПлатежа
Также отделение имеет смысл идентифицировать относительно конкретного клиента:
text(Клиент, ОтделениеКлиента)
потому что названия вроде Москва, Минск, СПб сами по себе не идентифицируют клиента.
Поля Оплата1, Оплата2, Оплата3 заменяем отдельной таблицей платежей.
Было:
textНомерДоговора | Оплата1 | Оплата2 | Оплата3 232433 | 200 | 300 | 500
Станет:
textНомерДоговора | НомерПлатежа | Сумма 232433 | 1 | 200 232433 | 2 | 300 232433 | 3 | 500
Теперь количество платежей заранее не ограничено.
Пустая ячейка из исходной таблицы означает отсутствие соответствующей записи в ContractPayment. При этом значение 0 следует хранить именно как 0, поскольку пустое значение и нулевая сумма — не одно и то же.
Аванс можно оставить непосредственно атрибутом договора, поскольку для одного договора в исходной предметной области предусмотрен один аванс.
После выделения платежей таблица платежей будет иметь составной ключ:
text(НомерДоговора, НомерПлатежа)
и сумма платежа зависит от всего ключа:
text(НомерДоговора, НомерПлатежа) → СуммаПлатежа
Следовательно, частичной зависимости нет.
Однако в исходной большой таблице сохранялась транзитивная зависимость:
textНомерДоговора → Клиент Клиент → ТипКлиента, Страна
следовательно:
textНомерДоговора → Клиент → ТипКлиента, Страна
Поэтому сведения о клиенте необходимо вынести в отдельное отношение Client.
Отделения клиента также логично вынести отдельно, поскольку один клиент может иметь несколько отделений.
Получаем четыре основные таблицы.
textClient( ClientName, ClientType, Country )
Естественный ключ:
textClientName
В рамках задания считаем имя/название клиента уникальным.
textClientBranch( ClientName, BranchName )
Естественный составной ключ:
text(ClientName, BranchName)
textContract( ContractNumber, ClientName, BranchName, ContractDate, ContractStatus, ContractAmount, AdvanceAmount )
Естественный ключ:
textContractNumber
textContractPayment( ContractNumber, PaymentNumber, PaymentAmount )
Естественный составной ключ:
text(ContractNumber, PaymentNumber)
Условие НФБК:
Для каждой нетривиальной функциональной зависимости
X → YдетерминантXдолжен быть суперключом.
Для Client:
textClientName → ClientType, Country
ClientName является ключом, поэтому НФБК выполняется.
Для ClientBranch значимые атрибуты образуют сам составной ключ:
text(ClientName, BranchName)
Неключевых функциональных зависимостей нет.
Для Contract:
textContractNumber → ClientName, BranchName, ContractDate, ContractStatus, ContractAmount, AdvanceAmount
ContractNumber является ключом.
Для ContractPayment:
text(ContractNumber, PaymentNumber) → PaymentAmount
детерминант является полным составным ключом.
Следовательно, все четыре отношения удовлетворяют НФБК.
Этот код можно целиком вставить в dbdiagram.io → New Diagram → DBML.
dbmlTable Client { ClientName varchar(128) [pk] ClientType varchar(32) [not null] Country varchar(64) [not null] Note: 'ClientName используется как естественный ключ в рамках задания' } Table ClientBranch { ClientName varchar(128) [pk] BranchName varchar(128) [pk] Note: 'Естественный составной ключ: ClientName + BranchName' } Table Contract { ContractNumber varchar(32) [pk] ClientName varchar(128) [not null] BranchName varchar(128) [not null] ContractDate date [not null] ContractStatus varchar(32) [not null] ContractAmount decimal(12,2) [not null] AdvanceAmount decimal(12,2) Note: 'ContractNumber — естественный номер договора' } Table ContractPayment { ContractNumber varchar(32) [pk] PaymentNumber int [pk] PaymentAmount decimal(12,2) [not null] Note: 'Количество платежей теперь не ограничено тремя' } Ref: Client.ClientName < ClientBranch.ClientName Ref: ClientBranch.(ClientName, BranchName) < Contract.(ClientName, BranchName) Ref: Contract.ContractNumber < ContractPayment.ContractNumber
У строки:
text"Григорий" | Беларусь | С.Петербург | NULL | NULL | Обсуждается | 500
ещё нет Номера договора. Следовательно, она не может быть помещена в Contract, если ContractNumber является естественным ключом.
Это не ошибка нормализации, а недостаток исходной предметной области. Необходимо отдельно определить, каким естественным признаком идентифицируется ещё не заключённый договор, например датой начала обсуждения вместе с клиентом и отделением. Без такого бизнес-правила несколько одновременно обсуждаемых договоров одного клиента различить невозможно.
В условии сказано, что Movie хранит название, дату выхода, режиссёра, жанр и отметку о просмотре; Cast содержит актёров, а Movie_Cast реализует связь many-to-many между фильмами и актёрами. (DB2026[2].docx)
Исходно дана примерно такая структура:
textMovie ----------------------- Title PK ReleaseDate Director Genre WasWatched Cast ----------------------- FirstName PK LastName PK Birthday Gender Movie_Cast ----------------------- Title PK, FK First_Name PK, FK Last_Name PK, FK
Связь фильма и актёров через Movie_Cast сама по себе выбрана правильно: у фильма много актёров, а один актёр может сниматься во многих фильмах.
Проблемы находятся в других частях модели.
Title как единственный ключ фильмаСейчас:
textMovie.Title PK
Это означает, что нельзя сохранить два разных фильма с одинаковым названием.
Например, теоретически могут существовать:
textMovie A: "It", 1990 Movie B: "It", 2017
или ремейки других фильмов с одинаковыми названиями.
Кроме того, название фильма иногда меняется при локализации или переиздании.
При использовании естественных ключей разумнее использовать составной идентификатор, например:
text(Title, ProductionYear)
где ProductionYear — именно год производства фильма, а не планируемая дата премьеры.
Director хранится просто строкойВ Movie имеется:
textDirector varchar(128)
Это создаёт сразу несколько проблем.
Во-первых, один фильм может иметь несколько режиссёров, а одна колонка естественно описывает только одного.
Во-вторых, один режиссёр может работать над множеством фильмов, но его имя будет копироваться из строки в строку.
В-третьих, режиссёр по сути является тем же человеком, что и актёр, продюсер или сценарист. Поэтому хранить актёров в Cast, а режиссёров просто текстом — логически непоследовательно.
Новые требования напрямую подтверждают эту проблему: некоторые фильмы должны поддерживать нескольких режиссёров. (DB2026[2].docx)
Genre varchar(1024)Поле:
textGenre varchar(1024)
предполагает, что туда, скорее всего, будут записываться значения вида:
textComedy, Drama, Action
Если так происходит, то одно поле содержит множество логических значений.
Например, запрос:
sqlWHERE Genre = 'Drama'
уже не сработает для строки:
text'Comedy, Drama'
и придётся использовать ненадёжный поиск по подстроке.
Правильная модель:
textGenre Movie_Genre
то есть связь many-to-many.
WasWatched является свойством не фильма, а пользователяВ текущей модели:
textMovie.WasWatched
означает:
этот фильм просмотрен.
Но на самом деле правильный вопрос:
кем он просмотрен?
Пока базой пользуется один человек, это незаметно. Но в новых требованиях сказано, что супруга и в дальнейшем дети также должны иметь собственные отметки. (DB2026[2].docx)
Поэтому:
textWasWatched
не должно находиться непосредственно в Movie.
Оно должно находиться в отношении между пользователем и фильмом:
textViewerMovie( Viewer, Movie, WasWatched )
Тогда:
textПапа → Interstellar → true Мама → Interstellar → false Ребёнок → Interstellar → false
не конфликтуют друг с другом.
Cast слишком специализированаСейчас таблица называется:
textCast
и предназначена только для актёров.
Однако по новому требованию необходимо хранить ещё:
Это явно указано в условии. (DB2026[2].docx)
Создание отдельных таблиц:
textActor Director Producer Screenwriter
было бы плохим решением, потому что один и тот же человек может одновременно быть, например, режиссёром и сценаристом.
Гораздо лучше выделить:
textPerson Role MoviePerson
Текущий ключ:
text(FirstName, LastName)
не гарантирует уникальность.
Может существовать два совершенно разных человека:
textJohn Smith John Smith
При этом в таблице уже имеется Birthday, поэтому в рамках требования об естественных ключах логичнее использовать:
text(FirstName, LastName, BirthDate)
Это гораздо надёжнее, хотя в реальной промышленной системе даже такой ключ не даёт абсолютной математической гарантии уникальности.
ReleaseDate недостаточноВ текущем Movie есть одна дата:
textReleaseDate
Но новое требование различает:
textпредполагаемую дату выхода фактическую дату выхода
Причём причиной требования являются изменения дат релизов во время пандемии. (DB2026[2].docx)
Поэтому хранить всего одну дату нельзя.
Минимальный вариант:
textExpectedReleaseDate ActualReleaseDate
Ещё лучше хранить историю изменения ожидаемых дат.
Например:
text01.01.2025 → ожидали 01.06.2025 20.03.2025 → перенесли на 01.09.2025 15.07.2025 → перенесли на 20.11.2025
Тогда можно увидеть не только последнюю плановую дату, но и всю историю переносов.
Есть одновременно:
textLastName Last_Name firstName First_Name
На нормальные формы это напрямую не влияет, но серьёзно ухудшает сопровождаемость схемы.
Лучше выбрать один стиль, например:
textFirstName LastName BirthDate RoleName
или один snake_case:
textfirst_name last_name birth_date role_name
и использовать его везде.
Третья часть задания требует исправить найденные недостатки и поддержать несколько режиссёров, продюсеров, сценаристов и будущие роли, нескольких членов семьи, а также предполагаемые и фактические даты релиза. (DB2026[2].docx)
Я предлагаю следующую структуру.
textMovie( Title, ProductionYear, ActualReleaseDate )
Ключ:
text(Title, ProductionYear)
ProductionYear добавляется именно как часть естественного ключа, чтобы можно было различать фильмы с одинаковыми названиями.
Важно: ProductionYear — не предполагаемый год проката. Поэтому перенос даты релиза не меняет идентификатор фильма.
Поскольку планируемая дата может изменяться, создаём:
textMovieReleasePlan( Title, ProductionYear, AnnouncedAt, ExpectedReleaseDate )
Например:
textDune | 2020 | 2020-05-01 | 2020-12-18 Dune | 2020 | 2020-10-05 | 2021-10-01 Dune | 2020 | 2021-06-25 | 2021-10-22
После фактического релиза значение заносится в:
textMovie.ActualReleaseDate
Таким образом, история ожидаемых дат не теряется.
Вместо Cast используется универсальная таблица:
textPerson( FirstName, LastName, BirthDate, Gender )
Ключ:
text(FirstName, LastName, BirthDate)
Один человек хранится только один раз независимо от того, является он актёром, режиссёром, сценаристом или сразу выполняет несколько функций.
textRole( RoleName )
Примеры данных:
textActor Director Producer Screenwriter
Если через год потребуется добавить:
textComposer Cinematographer Editor
структуру БД менять не понадобится.
Достаточно выполнить:
sqlINSERT INTO Role(RoleName) VALUES ('Composer');
Именно это даёт требуемую расширяемость.
Связь человека с фильмом:
textMoviePerson( Title, ProductionYear, FirstName, LastName, BirthDate, RoleName )
Например:
textFilm A | 2026 | John | Smith | 1980-01-01 | Director Film A | 2026 | John | Smith | 1980-01-01 | Screenwriter
Один человек может иметь сразу несколько ролей.
И одновременно фильм может иметь несколько человек с одной ролью:
textFilm A → Alice Brown → Director Film A → John Smith → Director
Таким образом выполняется требование о нескольких режиссёрах.
Вместо Genre varchar(1024):
textGenre( GenreName )
и:
textMovieGenre( Title, ProductionYear, GenreName )
Теперь один фильм может иметь любое число жанров.
Каждый член семьи получает собственный профиль:
textViewer( ProfileName )
В рамках требования естественных ключей ProfileName считается уникальным пользовательским именем профиля.
Например:
textAlex Anna Child1 Child2
Связь пользователя и фильма:
textViewerMovie( ProfileName, Title, ProductionYear, WasWatched, WatchedAt )
Например:
textAlex | Film A | 2026 | true | 2026-09-01 Anna | Film A | 2026 | false | NULL
Теперь статус просмотра является индивидуальным.
При этом исходная семантика WasWatched сохраняется:
textfalse = планирует посмотреть true = уже посмотрел
WatchedAt можно считать полезным расширением: поле необязательно, поэтому если дата просмотра неизвестна, оно остаётся NULL.
Это уже исправленная и расширенная версия исходной Movie DB.
dbmlTable Movie { Title varchar(256) [pk] ProductionYear int [pk] ActualReleaseDate date Note: 'Естественный ключ: Title + ProductionYear' } Table MovieReleasePlan { Title varchar(256) [pk] ProductionYear int [pk] AnnouncedAt datetime [pk] ExpectedReleaseDate date [not null] Note: 'Хранит историю изменения предполагаемых дат выхода' } Table Person { FirstName varchar(64) [pk] LastName varchar(64) [pk] BirthDate date [pk] Gender varchar(32) Note: 'Общая таблица для актеров, режиссеров, продюсеров и других участников' } Table Role { RoleName varchar(64) [pk] Note: 'Примеры: Actor, Director, Producer, Screenwriter' } Table MoviePerson { Title varchar(256) [pk] ProductionYear int [pk] FirstName varchar(64) [pk] LastName varchar(64) [pk] BirthDate date [pk] RoleName varchar(64) [pk] Note: 'Many-to-many между фильмами и людьми с указанием роли' } Table Genre { GenreName varchar(64) [pk] } Table MovieGenre { Title varchar(256) [pk] ProductionYear int [pk] GenreName varchar(64) [pk] Note: 'Фильм может принадлежать нескольким жанрам' } Table Viewer { ProfileName varchar(64) [pk] Note: 'Профиль члена семьи' } Table ViewerMovie { ProfileName varchar(64) [pk] Title varchar(256) [pk] ProductionYear int [pk] WasWatched boolean [not null] WatchedAt date Note: 'WasWatched хранится отдельно для каждого пользователя' } /* Фильм -> история планируемых дат */ Ref: Movie.(Title, ProductionYear) < MovieReleasePlan.(Title, ProductionYear) /* Фильм -> участники */ Ref: Movie.(Title, ProductionYear) < MoviePerson.(Title, ProductionYear) Ref: Person.(FirstName, LastName, BirthDate) < MoviePerson.(FirstName, LastName, BirthDate) Ref: Role.RoleName < MoviePerson.RoleName /* Фильм -> жанры */ Ref: Movie.(Title, ProductionYear) < MovieGenre.(Title, ProductionYear) Ref: Genre.GenreName < MovieGenre.GenreName /* Пользователи -> фильмы */ Ref: Viewer.ProfileName < ViewerMovie.ProfileName Ref: Movie.(Title, ProductionYear) < ViewerMovie.(Title, ProductionYear)
В dbdiagram.io в результате получится примерно такая логика:
textMovieReleasePlan | | Genre --- MovieGenre --- Movie --- ViewerMovie --- Viewer | | MoviePerson / \ Person Role
Итоговую схему удобно проверить непосредственно по требованиям задания.
| Требование | Как реализовано |
|---|---|
| Сохранить существующую информацию о фильмах | Movie |
| Несколько режиссёров | несколько строк MoviePerson с RoleName = 'Director' |
| Хранить актёров | Person + MoviePerson, роль Actor |
| Хранить продюсеров | роль Producer |
| Хранить сценаристов | роль Screenwriter |
| Добавлять новые профессии в будущем | новая строка в Role, изменение схемы не требуется |
| Несколько пользователей | Viewer |
| У каждого пользователя свои отметки | ViewerMovie |
| Хранить ожидаемую дату | MovieReleasePlan.ExpectedReleaseDate |
| Хранить изменение ожидаемых дат | несколько строк MovieReleasePlan |
| Хранить фактическую дату | Movie.ActualReleaseDate |
| Несколько жанров | Genre + MovieGenre |
| Использовать естественные ключи | составные естественные ключи вместо id/serial |
При переходе от старой структуры к новой потребуется следующая логика.
Старая таблица:
textMovie.Title
переходит в:
textMovie.Title
Для каждого фильма дополнительно понадобится определить ProductionYear, потому что он становится частью естественного ключа.
Старое:
textMovie.Director
превращается в человека:
textPerson
и связь:
textMoviePerson(..., RoleName = 'Director')
Старые актёры из Cast также переходят в Person, а строки Movie_Cast становятся строками:
textMoviePerson(..., RoleName = 'Actor')
Старое поле:
textGenre
разбивается на отдельные жанры и переносится в:
textGenre MovieGenre
Старое:
textWasWatched
переносится в ViewerMovie для профиля первоначального владельца базы.
Есть только один принципиально неоднозначный момент — старое поле:
textReleaseDate
По исходным данным нельзя автоматически определить, было ли конкретное значение предполагаемой или уже фактической датой выхода. Само исходное описание говорит лишь о «дате выхода фильма». (DB2026[2].docx) Поэтому при миграции эту часть придётся определить отдельно: для уже вышедших фильмов перенести дату в ActualReleaseDate, а для будущих — в MovieReleasePlan.ExpectedReleaseDate.
В первом задании исходная таблица была декомпозирована на сущности Client, ClientBranch, Contract и ContractPayment. Это устранило дублирование сведений о клиентах, зависимость количества платежей от количества столбцов и аномалии вставки, изменения и удаления. В полученных отношениях каждый детерминант нетривиальной функциональной зависимости является потенциальным ключом, поэтому отношения удовлетворяют НФБК.
Во втором и третьем пунктах было установлено, что исходная Movie DB плохо масштабируется из-за хранения режиссёра и жанра непосредственно в Movie, глобального признака WasWatched, специализированной таблицы Cast и единственной даты релиза. В новой модели участники съёмочного процесса представлены универсальными сущностями Person, Role и MoviePerson; жанры вынесены в many-to-many; пользовательские отметки перенесены в ViewerMovie; а ожидаемые даты выпуска хранятся отдельно от фактической даты. Благодаря таблице Role модель позволяет добавлять новые типы участников без изменения структуры базы данных.