Типы атрибутов: simple, composite, multi-valued, derived
В прошлом уроке мы определили атрибут как свойство сущности. Но атрибуты не однородны — они бывают разных видов, и вид атрибута напрямую влияет на то, как сущность превратится в таблицу. Составной атрибут, многозначный атрибут, вычисляемый атрибут — каждый требует своего решения при переходе к физической схеме.
Этот урок разбирает четыре оси классификации атрибутов. Понимать их важно, потому что именно здесь начинается связь ER-модели с реляционными правилами: например, многозначный атрибут — это прямой повод задуматься о нормальной форме, которую мы разберём позже.
Ось 1: simple vs composite
Простой атрибут (simple, atomic) — атрибут, который нельзя осмысленно разбить на более мелкие части. «Возраст», «цена», «email» как единое значение — простые атрибуты.
Составной атрибут (composite) — атрибут, состоящий из нескольких компонентов, каждый из которых имеет собственный смысл. Классический пример — «полное имя»: оно делится на фамилию, имя, отчество. Или «адрес»: страна, город, улица, дом, индекс.
Ключевой вопрос — нужны ли компоненты по отдельности. Если по «полному имени» когда-нибудь понадобится отсортировать по фамилии или обратиться к имени отдельно — это составной атрибут, и его компоненты лучше хранить раздельными колонками. Если адрес используется только целиком (показать на экране, напечатать на конверте) — можно хранить одной строкой.
При переходе к таблице составной атрибут обычно разбивают на колонки по компонентам: вместо одной колонки full_name — три колонки last_name, first_name, middle_name. Это даёт гибкость: можно фильтровать и сортировать по любому компоненту.
-- Составной атрибут "ФИО", разбитый на компоненты:
CREATE TABLE customers (
customer_id BIGINT PRIMARY KEY,
last_name TEXT NOT NULL,
first_name TEXT NOT NULL,
middle_name TEXT
);
-- Теперь легко: SELECT * FROM customers ORDER BY last_name;
Ось 2: single-valued vs multi-valued
Однозначный атрибут (single-valued) — у одного экземпляра сущности может быть только одно значение этого атрибута. Дата рождения у человека одна. Год выпуска у автомобиля один.
Многозначный атрибут (multi-valued) — у одного экземпляра может быть несколько значений одновременно. У клиента может быть несколько телефонов. У человека — несколько гражданств. У товара — несколько фотографий.
Многозначный атрибут — самый коварный вид, потому что реляционная таблица не умеет хранить несколько значений в одной ячейке. Соблазн велик — записать телефоны через запятую в одну колонку:
-- ПЛОХО: многозначный атрибут списком в одной ячейке
CREATE TABLE customers_bad (
customer_id BIGINT PRIMARY KEY,
phones TEXT -- "+7900..., +7901..., +7902..."
);
Это нарушение атомарности и первой нормальной формы (1NF). Список в ячейке делает невозможным нормально искать по телефону, считать количество телефонов, проверять формат каждого. Правильное решение — вынести многозначный атрибут в отдельную таблицу со связью один-ко-многим:
-- ХОРОШО: многозначный атрибут вынесен в отдельную таблицу
CREATE TABLE customers (
customer_id BIGINT PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE customer_phones (
customer_id BIGINT NOT NULL REFERENCES customers(customer_id),
phone TEXT NOT NULL,
phone_type TEXT, -- 'мобильный', 'рабочий'
PRIMARY KEY (customer_id, phone)
);
-- Один клиент -> много строк в customer_phones. Каждый телефон атомарен.
Список значений через запятую в одной колонке — один из самых частых анти-паттернов начинающих. Он кажется удобным («всё в одном месте»), но ломает почти любую работу с этими значениями. Увидев многозначный атрибут, сразу планируйте отдельную таблицу. Подробно про 1NF и атомарность — в модуле про нормализацию.
Ось 3: stored vs derived
Хранимый атрибут (stored) — значение хранится в базе как есть. Дата рождения хранится.
Вычисляемый атрибут (derived) — значение не хранится, а вычисляется из других атрибутов в момент запроса. Возраст человека — вычисляемый: он выводится из даты рождения и текущей даты. Возраст не нужно хранить — он устаревал бы каждый день.
Правило-ориентир: если значение можно надёжно вычислить из других имеющихся данных — это кандидат в вычисляемые атрибуты, и хранить его не нужно. Хранение того, что выводимо, порождает риск рассинхрона: сохранённый «возраст» и реальный возраст по дате рождения разойдутся.
-- Дата рождения хранится; возраст вычисляется в запросе, не хранится:
SELECT
name,
birth_date,
date_part('year', age(birth_date)) AS current_age
FROM customers;
-- Колонки current_age в таблице нет — она вычисляется каждый раз.
Другие примеры вычисляемых атрибутов: сумма заказа (из цен и количеств позиций), количество заказов клиента (подсчётом строк), полное имя (конкатенацией компонентов).
У этого правила есть важное исключение — денормализация. В аналитических хранилищах вычисляемые значения (предпосчитанные суммы, агрегаты) часто сознательно хранят ради скорости чтения. Но это осознанное решение с пониманием цены: за хранимый агрегат отвечает процесс его обновления. В обычном OLTP-моделировании вычисляемое не хранят. Денормализацию разбираем в модуле про нормализацию.
Ось 4: key attribute (идентификатор)
Отдельно стоит ключевой атрибут (key attribute, identifier) — атрибут (или набор атрибутов), который уникально идентифицирует экземпляр сущности. У сущности Клиент таким может быть customer_id или email. Никакие два разных клиента не имеют одинакового значения ключевого атрибута.
Ключевой атрибут — не «ещё один вид» в одном ряду с предыдущими осями, а особая роль: ключом может быть простой атрибут (customer_id), а может быть составной (например, для сущности «позиция заказа» ключ — пара «заказ + товар»). На ER-диаграммах ключевой атрибут принято выделять — подчёркиванием или пометкой.
Ключи — большая тема, им посвящён целый модуль курса (superkey, candidate key, primary key, natural и surrogate keys). Здесь важно зафиксировать: у любой сущности обязательно есть чем уникально отличить её экземпляры, и этот атрибут выделяется среди остальных.
Сводка: четыре оси классификации
Один и тот же атрибут классифицируется сразу по всем осям. «Дата рождения» — простой, однозначный, хранимый, не ключевой. «Список телефонов» — простой по структуре, но многозначный. Соберём оси в таблицу.
| Ось | Варианты | Чем определяется выбор |
|---|---|---|
| Структура | simple / composite | Нужны ли компоненты по отдельности |
| Количество значений | single-valued / multi-valued | Может ли быть несколько значений у одного экземпляра |
| Источник значения | stored / derived | Выводимо ли значение из других данных |
| Роль | обычный / key (идентификатор) | Идентифицирует ли уникально экземпляр |
Главная мысль: вид атрибута — это не теоретическая метка, а решение о структуре. Составной атрибут влияет на число колонок. Многозначный требует отдельной таблицы. Вычисляемый не хранится вообще. Ключевой определяет идентификацию строки.
Массивы в PostgreSQL — альтернатива отдельной таблице для multi-valued Типы данных и домен атрибута — физический уровеньПочему это влияет на физическую схему
Свяжем урок с предыдущими модулями. На logical-уровне модели вы классифицируете каждый атрибут по четырём осям. На physical-уровне эта классификация превращается в конкретные решения:
- составной атрибут -> несколько колонок;
- многозначный атрибут -> отдельная таблица со связью один-ко-многим;
- вычисляемый атрибут -> не колонка, а выражение в запросе или вычисляемая колонка;
- ключевой атрибут -> объявляется как
PRIMARY KEY.
Особенно важна связь многозначного атрибута с нормализацией. Многозначный атрибут, оставленный списком в ячейке, — это нарушение 1NF, первой и самой базовой нормальной формы. Поэтому правильная классификация атрибутов на этапе ER-моделирования — это профилактика проблем нормализации ещё до того, как мы дойдём до неё формально.
Попробуй сам
Возьмите сущность Сотрудник и набор её атрибутов: ФИО, дата рождения, стаж работы (в годах), email, навыки (человек владеет несколькими), оклад, табельный номер. Для каждого атрибута определите его вид по всем четырём осям: простой или составной, однозначный или многозначный, хранимый или вычисляемый, является ли ключевым. Особо разберите два: «стаж работы» (подумайте, хранить или вычислять, и из чего) и «навыки» (это многозначный атрибут — как его правильно представить). Затем напишите CREATE TABLE для сущности Сотрудник плюс, если нужно, дополнительную таблицу — так, чтобы ни один многозначный атрибут не лежал списком в ячейке и ни один вычисляемый не хранился зря.