Weak vs strong entities, subtypes и supertypes
Базовых трёх элементов — сущность, атрибут, связь — достаточно для простых моделей. Но реальные предметные области сложнее, и ER-моделирование даёт две продвинутые конструкции для их точного описания. Первая — различие слабых и сильных сущностей: что делать с сущностью, у которой нет собственного идентификатора. Вторая — обобщение и специализация (supertype/subtype): что делать, когда сущности похожи, но не одинаковы.
Эти конструкции встречаются в реальных моделях постоянно, и важно научиться их распознавать и правильно применять.
Strong entity — сильная сущность
Сильная (регулярная) сущность (strong entity) — это сущность, которая имеет собственный ключевой атрибут и существует независимо от других сущностей. Большинство сущностей, с которыми мы работали, — сильные: Клиент, Товар, Заказ. У Клиента есть свой customer_id, и клиент существует сам по себе, не зависит от существования других сущностей.
Сильная сущность — это норма. Если ничего особенного про сущность не сказано, она сильная.
Weak entity — слабая сущность
Слабая сущность (weak entity) — это сущность, которая не имеет собственного достаточного ключевого атрибута и не может быть однозначно идентифицирована без другой сущности. Слабая сущность зависит от сущности-владельца (owner / identifying entity).
Разберём на примере. Возьмём сущность «Позиция заказа» (order line) — конкретная строка внутри заказа: товар, количество, цена. Можно ли идентифицировать позицию заказа сама по себе? Номер позиции «1» бессмыслен в отрыве от заказа — позиция «1» есть в каждом заказе. Позиция уникальна только в паре «заказ + номер позиции». Позиция заказа — слабая сущность; её владелец — Заказ.
Другой пример: «Комната» в гостинице. Комната «305» сама по себе не уникальна — комната «305» есть во многих гостиницах. Комната уникальна только как «гостиница + номер комнаты». Комната — слабая сущность, владелец — Гостиница.
У слабой сущности два ключевых понятия:
- Идентифицирующая связь (identifying relationship) — связь со владельцем, через которую слабая сущность получает идентификацию. Слабая сущность не существует без этой связи.
- Частичный ключ / дискриминатор (partial key, discriminator) — атрибут, который различает экземпляры слабой сущности в пределах одного владельца. Для позиции заказа это номер позиции; для комнаты — номер комнаты. Сам по себе частичный ключ не уникален.
Полный идентификатор слабой сущности = ключ владельца + частичный ключ. Для позиции заказа: order_id (ключ владельца) + line_number (частичный ключ).
В таблице слабая сущность реализуется через составной первичный ключ, включающий внешний ключ на владельца:
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
order_date DATE NOT NULL
);
-- Слабая сущность: PK составной — ключ владельца + частичный ключ
CREATE TABLE order_lines (
order_id BIGINT NOT NULL REFERENCES orders(order_id),
line_number INTEGER NOT NULL,
product_id BIGINT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
PRIMARY KEY (order_id, line_number)
);
-- line_number сам по себе не уникален; уникальна пара (order_id, line_number)
Различие слабой и сильной сущности тесно связано с identifying и non-identifying связями, которые подробно разбираются в модуле про связи и кардинальность. Слабая сущность всегда участвует в identifying-связи со своим владельцем — внешний ключ на владельца входит в её первичный ключ. Здесь достаточно понимать саму идею: некоторые сущности не могут быть идентифицированы без «родителя».
На практике многозначные атрибуты и слабые сущности часто пересекаются. Помните customer_phones из прошлого урока? Телефон клиента — это, по сути, слабая сущность: телефон идентифицируется только в паре «клиент + номер». Многозначный атрибут, вынесенный в отдельную таблицу, нередко и есть слабая сущность.
Generalization и specialization
Вторая продвинутая конструкция решает другую задачу: что делать, когда сущности похожи, но не идентичны.
Представим систему компании с сотрудниками. Есть штатные сотрудники и подрядчики. У них много общего: имя, контакты, табельный номер. Но есть и различия: у штатного есть оклад и отпускные дни, у подрядчика — ставка за час и срок контракта. Как это смоделировать?
Здесь работает обобщение/специализация (generalization/specialization) — иерархия supertype/subtype.
- Supertype (надтип, родительская сущность) — общая сущность, содержащая атрибуты, общие для всех. В примере — «Сотрудник» с атрибутами имя, контакты, табельный номер.
- Subtype (подтип, дочерняя сущность) — специализированная сущность, содержащая атрибуты, специфичные для своего вида. В примере — «Штатный сотрудник» (оклад, отпуск) и «Подрядчик» (ставка, срок контракта).
Подтип наследует все атрибуты надтипа и добавляет свои. Штатный сотрудник — это и есть Сотрудник, плюс оклад и отпуск.
Направление имеет названия: движение от подтипов к надтипу (выделение общего) — это обобщение (generalization); движение от надтипа к подтипам (выделение различий) — это специализация (specialization). Это две стороны одного отношения.
Два свойства иерархии supertype/subtype
Иерархию уточняют два свойства — они отвечают на важные вопросы о подтипах.
Disjoint vs overlapping (непересекающиеся или пересекающиеся). Может ли один экземпляр принадлежать сразу нескольким подтипам?
- Disjoint (непересекающиеся) — экземпляр принадлежит ровно одному подтипу. Сотрудник либо штатный, либо подрядчик, но не оба сразу.
- Overlapping (пересекающиеся) — экземпляр может принадлежать нескольким подтипам. Например, в университете человек может быть одновременно и Студентом, и Преподавателем.
Total vs partial (полное или частичное участие). Обязан ли каждый экземпляр надтипа принадлежать какому-то подтипу?
- Total (полное участие) — каждый экземпляр надтипа обязательно относится к одному из подтипов. Каждый Сотрудник — либо штатный, либо подрядчик, третьего не дано.
- Partial (частичное участие) — экземпляр надтипа может не относиться ни к одному подтипу. Например, если кроме штатных и подрядчиков бывают сотрудники, чей тип ещё не определён.
Эти два свойства независимы и дают четыре комбинации. Их важно зафиксировать, потому что они влияют на реализацию.
| Комбинация | Что означает | Пример |
|---|---|---|
| Disjoint + total | Ровно один подтип, обязательно | Сотрудник: штатный xor подрядчик |
| Disjoint + partial | Не более одного подтипа | Сотрудник: штатный, подрядчик или «пока никто» |
| Overlapping + total | Хотя бы один подтип, можно несколько | Участник вуза: студент и/или преподаватель |
| Overlapping + partial | Любое число подтипов, включая ноль | Гибкая классификация без обязательности |
Как иерархию превращают в таблицы
Иерархию supertype/subtype можно реализовать в реляционной схеме тремя способами — это важное проектное решение.
Способ 1: одна таблица на всё (single table). Все атрибуты надтипа и всех подтипов — в одной таблице, плюс колонка-дискриминатор «тип». Атрибуты неподходящего подтипа в строке остаются NULL (у подрядчика колонка «оклад» пустая). Просто, без соединений, но много NULL.
Способ 2: таблица на каждый подтип (table per subtype). Отдельная таблица на каждый подтип, и каждая содержит и общие атрибуты, и свои. Надтип отдельной таблицей не существует. Нет лишних NULL, но общие данные продублированы по структуре, а запрос «все сотрудники» требует объединения таблиц.
Способ 3: таблица надтипа плюс таблицы подтипов (table per type). Таблица для надтипа с общими атрибутами и отдельные таблицы для каждого подтипа со специфичными атрибутами, связанные с надтипом. Чисто с точки зрения нормализации, но запросы требуют соединений.
Нет единственно правильного способа — выбор зависит от данных и запросов. Если подтипы почти не отличаются атрибутами и часто запрашиваются вместе — берите single table. Если подтипы сильно различаются и запрашиваются раздельно — table per subtype. Если важна нормализация и общие атрибуты объёмны — table per type. Это решение physical-уровня, и принимать его надо осознанно, взвесив количество NULL, дублирование и стоимость соединений.
Попробуй сам
Часть 1, слабые сущности. Возьмите систему многоквартирного дома: есть Дом и есть Квартира. Объясните, почему Квартира — слабая сущность: что является её владельцем, что частичным ключом, из чего состоит её полный идентификатор. Напишите для Дома и Квартиры пару CREATE TABLE с правильным составным первичным ключом у Квартиры.
Часть 2, иерархия. Возьмите сущность «Платёж» в интернет-магазине: бывают платежи картой (есть номер карты, банк) и платежи криптовалютой (есть адрес кошелька, сеть). Постройте иерархию supertype/subtype: что в надтипе, что в подтипах. Определите два свойства — disjoint или overlapping, total или partial — и обоснуйте. Выберите один из трёх способов реализации в таблицах и объясните, почему именно он подходит для платежей.