Table Movie { Title varchar (256) pk ReleaseDa...

Создано: 20 сентября 2026 г.

Отвечено с использованием GPT-5.6 Thinking от Chat01

Вопрос

DB2026[2].docx
SPBU_DB_2.pdf

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

Мысль

Normalizing contract data

I’m separating clients, branches, contracts, and payments to eliminate repeating payment columns. I’m also weighing how to represent unsigned discussions and whether advance payments belong in the contract or payment relation.

Normalized contract data

Handling incomplete records

I’m noting that the undated discussion record lacks a reliable natural key. Rather than inventing identifying data, I’m treating it as incomplete and documenting that it needs a business identifier before insertion.

Handled incomplete records

Worked for 2m 22s

Ответ

Ниже — готовое подробное решение, которое можно использовать как основу для отчёта. Я сделаю две схемы в формате DBML для dbdiagram.io: первая — результат нормализации SPBU_DB_2, вторая — исправленная и расширенная БД фильмов. Это соответствует смыслу задания: сначала требуется привести исходную таблицу к НФБК, затем проанализировать текущую схему фильмов и на основании найденных проблем спроектировать новую. В условии отдельно указано, что предполагается использование естественных ключей, поэтому id serial, movie_id, person_id и подобные искусственные ключи я не использую. (DB2026[2].docx) (DB2026[2].docx)


1. Нормализация таблицы SPBU_DB_2 до НФБК

В исходной таблице находятся данные о клиенте, его отделении, договоре и платежах: клиент, тип клиента, страна, отделение, номер договора, дата заключения, статус, сумма, аванс и до трёх платежей. На странице с исходными данными видно, что один и тот же клиент встречается несколько раз для разных договоров, например ООО «Автомобили». (SPBU_DB_2.pdf)

Исходное отношение можно записать так:

text
R( Клиент, ТипКлиента, Страна, ОтделениеКлиента, НомерДоговора, ДатаЗаключения, СтатусДоговора, СуммаДоговора, Аванс, Оплата1, Оплата2, Оплата3 )

1.1. Какие проблемы есть в исходной таблице

Первая проблема — дублирование информации о клиенте. Например, ООО «Автомобили» встречается в таблице более одного раза. Вместе с каждым договором повторяются тип клиента, страна и отделение. Более того, для одного и того же клиента страна записана как Россия и как РФ, то есть уже появилась аномалия обновления данных. (SPBU_DB_2.pdf)

Вторая проблема — поля:

text
Оплата 1 Оплата 2 Оплата 3

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

Третья проблема — в одной строке смешаны данные сразу о нескольких сущностях: о клиенте, его отделении, договоре и платежах.

Из-за этого возникают классические аномалии.

АномалияПример
Обновленияесли страна клиента изменилась или исправляется РФРоссия, её приходится менять во всех строках
Вставкитрудно добавить нового клиента, пока у него нет договора
Удаленияпри удалении последнего договора можно случайно потерять сведения о клиенте
Структурнаянельзя сохранить более трёх платежей без ALTER TABLE

Кроме того, последняя строка исходных данных содержит клиента «Григорий», отделение С.Петербург, статус Обсуждается и сумму 500, но не содержит номера договора и даты заключения. (SPBU_DB_2.pdf) Это отдельная важная проблема: такая строка фактически описывает ещё не заключённый договор, поэтому естественного ключа НомерДоговора у неё пока нет.


1.2. Функциональные зависимости

Исходя из смысла данных, можно выделить следующие зависимости.

Для клиента:

text
Клиент → ТипКлиента, Страна

То есть у клиента имеются собственные характеристики, которые не должны зависеть от конкретного договора.

Для договора:

text
НомерДоговора → Клиент, ОтделениеКлиента, ДатаЗаключения, СтатусДоговора, СуммаДоговора, Аванс

Здесь предполагается, что номер договора является уникальным естественным бизнес-ключом.

Для платежей после преобразования структуры:

text
(НомерДоговора, НомерПлатежа) → СуммаПлатежа

Также отделение имеет смысл идентифицировать относительно конкретного клиента:

text
(Клиент, ОтделениеКлиента)

потому что названия вроде Москва, Минск, СПб сами по себе не идентифицируют клиента.


1.3. Приведение к первой нормальной форме

Поля Оплата1, Оплата2, Оплата3 заменяем отдельной таблицей платежей.

Было:

text
НомерДоговора | Оплата1 | Оплата2 | Оплата3 232433 | 200 | 300 | 500

Станет:

text
НомерДоговора | НомерПлатежа | Сумма 232433 | 1 | 200 232433 | 2 | 300 232433 | 3 | 500

Теперь количество платежей заранее не ограничено.

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

Аванс можно оставить непосредственно атрибутом договора, поскольку для одного договора в исходной предметной области предусмотрен один аванс.


1.4. Приведение ко второй и третьей нормальным формам

После выделения платежей таблица платежей будет иметь составной ключ:

text
(НомерДоговора, НомерПлатежа)

и сумма платежа зависит от всего ключа:

text
(НомерДоговора, НомерПлатежа) → СуммаПлатежа

Следовательно, частичной зависимости нет.

Однако в исходной большой таблице сохранялась транзитивная зависимость:

text
НомерДоговора → Клиент Клиент → ТипКлиента, Страна

следовательно:

text
НомерДоговора → Клиент → ТипКлиента, Страна

Поэтому сведения о клиенте необходимо вынести в отдельное отношение Client.

Отделения клиента также логично вынести отдельно, поскольку один клиент может иметь несколько отделений.


1.5. Итоговая декомпозиция

Получаем четыре основные таблицы.

Client

text
Client( ClientName, ClientType, Country )

Естественный ключ:

text
ClientName

В рамках задания считаем имя/название клиента уникальным.

ClientBranch

text
ClientBranch( ClientName, BranchName )

Естественный составной ключ:

text
(ClientName, BranchName)

Contract

text
Contract( ContractNumber, ClientName, BranchName, ContractDate, ContractStatus, ContractAmount, AdvanceAmount )

Естественный ключ:

text
ContractNumber

ContractPayment

text
ContractPayment( ContractNumber, PaymentNumber, PaymentAmount )

Естественный составной ключ:

text
(ContractNumber, PaymentNumber)

1.6. Почему полученные таблицы находятся в НФБК

Условие НФБК:

Для каждой нетривиальной функциональной зависимости X → Y детерминант X должен быть суперключом.

Для Client:

text
ClientName → ClientType, Country

ClientName является ключом, поэтому НФБК выполняется.

Для ClientBranch значимые атрибуты образуют сам составной ключ:

text
(ClientName, BranchName)

Неключевых функциональных зависимостей нет.

Для Contract:

text
ContractNumber → ClientName, BranchName, ContractDate, ContractStatus, ContractAmount, AdvanceAmount

ContractNumber является ключом.

Для ContractPayment:

text
(ContractNumber, PaymentNumber) → PaymentAmount

детерминант является полным составным ключом.

Следовательно, все четыре отношения удовлетворяют НФБК.


1.7. Диаграмма №1 для dbdiagram.io

Этот код можно целиком вставить в dbdiagram.io → New Diagram → DBML.

dbml
Table 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 является естественным ключом.

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


2. Анализ исходной базы фильмов

В условии сказано, что Movie хранит название, дату выхода, режиссёра, жанр и отметку о просмотре; Cast содержит актёров, а Movie_Cast реализует связь many-to-many между фильмами и актёрами. (DB2026[2].docx)

Исходно дана примерно такая структура:

text
Movie ----------------------- 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 сама по себе выбрана правильно: у фильма много актёров, а один актёр может сниматься во многих фильмах.

Проблемы находятся в других частях модели.


2.1. Title как единственный ключ фильма

Сейчас:

text
Movie.Title PK

Это означает, что нельзя сохранить два разных фильма с одинаковым названием.

Например, теоретически могут существовать:

text
Movie A: "It", 1990 Movie B: "It", 2017

или ремейки других фильмов с одинаковыми названиями.

Кроме того, название фильма иногда меняется при локализации или переиздании.

При использовании естественных ключей разумнее использовать составной идентификатор, например:

text
(Title, ProductionYear)

где ProductionYear — именно год производства фильма, а не планируемая дата премьеры.


2.2. Director хранится просто строкой

В Movie имеется:

text
Director varchar(128)

Это создаёт сразу несколько проблем.

Во-первых, один фильм может иметь несколько режиссёров, а одна колонка естественно описывает только одного.

Во-вторых, один режиссёр может работать над множеством фильмов, но его имя будет копироваться из строки в строку.

В-третьих, режиссёр по сути является тем же человеком, что и актёр, продюсер или сценарист. Поэтому хранить актёров в Cast, а режиссёров просто текстом — логически непоследовательно.

Новые требования напрямую подтверждают эту проблему: некоторые фильмы должны поддерживать нескольких режиссёров. (DB2026[2].docx)


2.3. Genre varchar(1024)

Поле:

text
Genre varchar(1024)

предполагает, что туда, скорее всего, будут записываться значения вида:

text
Comedy, Drama, Action

Если так происходит, то одно поле содержит множество логических значений.

Например, запрос:

sql
WHERE Genre = 'Drama'

уже не сработает для строки:

text
'Comedy, Drama'

и придётся использовать ненадёжный поиск по подстроке.

Правильная модель:

text
Genre Movie_Genre

то есть связь many-to-many.


2.4. WasWatched является свойством не фильма, а пользователя

В текущей модели:

text
Movie.WasWatched

означает:

этот фильм просмотрен.

Но на самом деле правильный вопрос:

кем он просмотрен?

Пока базой пользуется один человек, это незаметно. Но в новых требованиях сказано, что супруга и в дальнейшем дети также должны иметь собственные отметки. (DB2026[2].docx)

Поэтому:

text
WasWatched

не должно находиться непосредственно в Movie.

Оно должно находиться в отношении между пользователем и фильмом:

text
ViewerMovie( Viewer, Movie, WasWatched )

Тогда:

text
Папа → Interstellar → true Мама → Interstellar → false Ребёнок → Interstellar → false

не конфликтуют друг с другом.


2.5. Таблица Cast слишком специализирована

Сейчас таблица называется:

text
Cast

и предназначена только для актёров.

Однако по новому требованию необходимо хранить ещё:

  • режиссёров;
  • продюсеров;
  • сценаристов;
  • а впоследствии, возможно, другие роли участников съёмочного процесса.

Это явно указано в условии. (DB2026[2].docx)

Создание отдельных таблиц:

text
Actor Director Producer Screenwriter

было бы плохим решением, потому что один и тот же человек может одновременно быть, например, режиссёром и сценаристом.

Гораздо лучше выделить:

text
Person Role MoviePerson

2.6. Первого и последнего имени недостаточно для идентификации человека

Текущий ключ:

text
(FirstName, LastName)

не гарантирует уникальность.

Может существовать два совершенно разных человека:

text
John Smith John Smith

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

text
(FirstName, LastName, BirthDate)

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


2.7. ReleaseDate недостаточно

В текущем Movie есть одна дата:

text
ReleaseDate

Но новое требование различает:

text
предполагаемую дату выхода фактическую дату выхода

Причём причиной требования являются изменения дат релизов во время пандемии. (DB2026[2].docx)

Поэтому хранить всего одну дату нельзя.

Минимальный вариант:

text
ExpectedReleaseDate ActualReleaseDate

Ещё лучше хранить историю изменения ожидаемых дат.

Например:

text
01.01.2025 → ожидали 01.06.2025 20.03.2025 → перенесли на 01.09.2025 15.07.2025 → перенесли на 20.11.2025

Тогда можно увидеть не только последнюю плановую дату, но и всю историю переносов.


2.8. Разные правила именования столбцов

Есть одновременно:

text
LastName Last_Name firstName First_Name

На нормальные формы это напрямую не влияет, но серьёзно ухудшает сопровождаемость схемы.

Лучше выбрать один стиль, например:

text
FirstName LastName BirthDate RoleName

или один snake_case:

text
first_name last_name birth_date role_name

и использовать его везде.


3. Исправление схемы и расширение согласно новым требованиям

Третья часть задания требует исправить найденные недостатки и поддержать несколько режиссёров, продюсеров, сценаристов и будущие роли, нескольких членов семьи, а также предполагаемые и фактические даты релиза. (DB2026[2].docx)

Я предлагаю следующую структуру.


3.1. Movie

text
Movie( Title, ProductionYear, ActualReleaseDate )

Ключ:

text
(Title, ProductionYear)

ProductionYear добавляется именно как часть естественного ключа, чтобы можно было различать фильмы с одинаковыми названиями.

Важно: ProductionYear — не предполагаемый год проката. Поэтому перенос даты релиза не меняет идентификатор фильма.


3.2. MovieReleasePlan

Поскольку планируемая дата может изменяться, создаём:

text
MovieReleasePlan( Title, ProductionYear, AnnouncedAt, ExpectedReleaseDate )

Например:

text
Dune | 2020 | 2020-05-01 | 2020-12-18 Dune | 2020 | 2020-10-05 | 2021-10-01 Dune | 2020 | 2021-06-25 | 2021-10-22

После фактического релиза значение заносится в:

text
Movie.ActualReleaseDate

Таким образом, история ожидаемых дат не теряется.


3.3. Person

Вместо Cast используется универсальная таблица:

text
Person( FirstName, LastName, BirthDate, Gender )

Ключ:

text
(FirstName, LastName, BirthDate)

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


3.4. Role

text
Role( RoleName )

Примеры данных:

text
Actor Director Producer Screenwriter

Если через год потребуется добавить:

text
Composer Cinematographer Editor

структуру БД менять не понадобится.

Достаточно выполнить:

sql
INSERT INTO Role(RoleName) VALUES ('Composer');

Именно это даёт требуемую расширяемость.


3.5. MoviePerson

Связь человека с фильмом:

text
MoviePerson( Title, ProductionYear, FirstName, LastName, BirthDate, RoleName )

Например:

text
Film A | 2026 | John | Smith | 1980-01-01 | Director Film A | 2026 | John | Smith | 1980-01-01 | Screenwriter

Один человек может иметь сразу несколько ролей.

И одновременно фильм может иметь несколько человек с одной ролью:

text
Film A → Alice Brown → Director Film A → John Smith → Director

Таким образом выполняется требование о нескольких режиссёрах.


3.6. Genre и MovieGenre

Вместо Genre varchar(1024):

text
Genre( GenreName )

и:

text
MovieGenre( Title, ProductionYear, GenreName )

Теперь один фильм может иметь любое число жанров.


3.7. Viewer

Каждый член семьи получает собственный профиль:

text
Viewer( ProfileName )

В рамках требования естественных ключей ProfileName считается уникальным пользовательским именем профиля.

Например:

text
Alex Anna Child1 Child2

3.8. ViewerMovie

Связь пользователя и фильма:

text
ViewerMovie( ProfileName, Title, ProductionYear, WasWatched, WatchedAt )

Например:

text
Alex | Film A | 2026 | true | 2026-09-01 Anna | Film A | 2026 | false | NULL

Теперь статус просмотра является индивидуальным.

При этом исходная семантика WasWatched сохраняется:

text
false = планирует посмотреть true = уже посмотрел

WatchedAt можно считать полезным расширением: поле необязательно, поэтому если дата просмотра неизвестна, оно остаётся NULL.


4. Итоговая диаграмма №2 для dbdiagram.io

Это уже исправленная и расширенная версия исходной Movie DB.

dbml
Table 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 в результате получится примерно такая логика:

text
MovieReleasePlan | | Genre --- MovieGenre --- Movie --- ViewerMovie --- Viewer | | MoviePerson / \ Person Role

5. Проверка новых бизнес-требований

Итоговую схему удобно проверить непосредственно по требованиям задания.

ТребованиеКак реализовано
Сохранить существующую информацию о фильмахMovie
Несколько режиссёровнесколько строк MoviePerson с RoleName = 'Director'
Хранить актёровPerson + MoviePerson, роль Actor
Хранить продюсеровроль Producer
Хранить сценаристовроль Screenwriter
Добавлять новые профессии в будущемновая строка в Role, изменение схемы не требуется
Несколько пользователейViewer
У каждого пользователя свои отметкиViewerMovie
Хранить ожидаемую датуMovieReleasePlan.ExpectedReleaseDate
Хранить изменение ожидаемых датнесколько строк MovieReleasePlan
Хранить фактическую датуMovie.ActualReleaseDate
Несколько жанровGenre + MovieGenre
Использовать естественные ключисоставные естественные ключи вместо id/serial

6. Как перенести данные из старой Movie DB

При переходе от старой структуры к новой потребуется следующая логика.

Старая таблица:

text
Movie.Title

переходит в:

text
Movie.Title

Для каждого фильма дополнительно понадобится определить ProductionYear, потому что он становится частью естественного ключа.

Старое:

text
Movie.Director

превращается в человека:

text
Person

и связь:

text
MoviePerson(..., RoleName = 'Director')

Старые актёры из Cast также переходят в Person, а строки Movie_Cast становятся строками:

text
MoviePerson(..., RoleName = 'Actor')

Старое поле:

text
Genre

разбивается на отдельные жанры и переносится в:

text
Genre MovieGenre

Старое:

text
WasWatched

переносится в ViewerMovie для профиля первоначального владельца базы.

Есть только один принципиально неоднозначный момент — старое поле:

text
ReleaseDate

По исходным данным нельзя автоматически определить, было ли конкретное значение предполагаемой или уже фактической датой выхода. Само исходное описание говорит лишь о «дате выхода фильма». (DB2026[2].docx) Поэтому при миграции эту часть придётся определить отдельно: для уже вышедших фильмов перенести дату в ActualReleaseDate, а для будущих — в MovieReleasePlan.ExpectedReleaseDate.


7. Короткий вывод для отчёта

В первом задании исходная таблица была декомпозирована на сущности Client, ClientBranch, Contract и ContractPayment. Это устранило дублирование сведений о клиентах, зависимость количества платежей от количества столбцов и аномалии вставки, изменения и удаления. В полученных отношениях каждый детерминант нетривиальной функциональной зависимости является потенциальным ключом, поэтому отношения удовлетворяют НФБК.

Во втором и третьем пунктах было установлено, что исходная Movie DB плохо масштабируется из-за хранения режиссёра и жанра непосредственно в Movie, глобального признака WasWatched, специализированной таблицы Cast и единственной даты релиза. В новой модели участники съёмочного процесса представлены универсальными сущностями Person, Role и MoviePerson; жанры вынесены в many-to-many; пользовательские отметки перенесены в ViewerMovie; а ожидаемые даты выпуска хранятся отдельно от фактической даты. Благодаря таблице Role модель позволяет добавлять новые типы участников без изменения структуры базы данных.

Поделиться этим Q&A