Ключи и ограничения как часть модели
Хорошая модель кодирует правила данных в схеме, а не только в коде. Primary key идентифицирует строку, foreign key защищает ссылочную целостность, unique выражает бизнес-уникальность, NOT NULL и CHECK ограничивают допустимые значения.
Эти ограничения — не бюрократия, а машинно-проверяемая документация инвариантов. Схему читают новые разработчики, аналитики и скрипты миграции; правило, записанное в CHECK, невозможно «не заметить», в отличие от проверки, спрятанной в одном из сервисов.
Отдельный сюжет — суррогатный ключ против естественного. Суррогатный (auto id, UUID) стабилен и ничего не значит для бизнеса, поэтому его безопасно менять местами и ссылаться на него. Естественный (email, ИНН, номер паспорта) экономит соединение, но имеет свойство меняться — а меняющийся PK тянет за собой обновление всех ссылающихся строк. Практика по умолчанию: суррогатный PK плюс unique-ограничение на естественный ключ — так получаем и стабильность, и защиту бизнес-уникальности.
create table orders (
id bigserial primary key, -- суррогатный ключ
tenant_id bigint not null references tenants(id),
order_no text not null,
status text not null,
total numeric(12,2) not null,
created_at timestamptz not null default now(),
constraint uq_order_no unique (tenant_id, order_no), -- бизнес-уникальность
constraint ck_total_non_negative check (total >= 0), -- инвариант в схеме
constraint ck_status check (status in ('NEW','PAID','SHIPPED','CANCELLED'))
);
-- колонку-источник FK индексируем сами: автоматически это не делается
create index idx_orders_tenant on orders (tenant_id);
Нормальные формы простыми словами
1NF — атомарные значения, без списков в одной ячейке. 2NF — нет зависимости части атрибутов только от части составного ключа. 3NF — неключевые атрибуты зависят только от ключа, а не друг от друга (нет транзитивных зависимостей).
Практический смысл нормализации — хранить каждый факт в одном месте, чтобы не было аномалий вставки/обновления/удаления и рассинхронизации копий.
Осознанная денормализация
Денормализация — сознательное дублирование данных ради скорости чтения (меньше join, готовые агрегаты). Это компромисс: чтение дешевеет, но запись усложняется, и появляется риск рассинхронизации.
Денормализуйте после измерений и с явным механизмом поддержания согласованности (триггеры, пересчёт, события), а не «на всякий случай».
Каскадные действия
ON DELETE CASCADE удобен для строк, не имеющих смысла без родителя (позиции заказа), но опасен на больших графах: одно удаление может снести многое. RESTRICT/NO ACTION заставляет приложение явно решить судьбу зависимостей. SET NULL подходит для опциональной связи.
Выбор действия — часть модели, а не деталь реализации.
JSON-колонки и гибкая схема
Современные реляционные СУБД умеют хранить документ прямо в колонке (jsonb в PostgreSQL, JSON в MySQL) и индексировать его содержимое. Это снимает вечный соблазн изобрести EAV — таблицу вида (entity_id, attribute, value), которая формально универсальна, но убивает и запросы, и типизацию, и ограничения.
Разумная граница проходит по предсказуемости: то, по чему фильтруют, соединяют и считают агрегаты, должно быть обычными колонками с типами и ограничениями. То, что действительно произвольно и меняется от записи к записи (атрибуты товара в маркетплейсе, сырой ответ внешнего API, настройки), уместно в jsonb.
Цена гибкости честная: в JSON нет NOT NULL, FK и CHECK для внутренних полей, опечатка в имени ключа не будет замечена базой, а индексы по содержимому дороже обычных. Поэтому «сложим всё в jsonb и разберёмся потом» — это не гибкость, а отложенный технический долг.
create table products (
id bigserial primary key,
sku text not null unique, -- предсказуемое → колонка
price numeric(12,2) not null, -- по нему фильтруют → колонка
attributes jsonb not null default '{}' -- произвольное → документ
);
-- индекс по содержимому документа
create index idx_products_attrs on products using gin (attributes);
select * from products where attributes @> '{"color": "red"}';
Практический критерий
Если нарушение правила критично для бизнеса, не держите его только в коде — добавьте constraint в БД. Код можно обойти миграцией, багом или вторым сервисом; constraint остаётся последней линией защиты.
Обратная сторона критерия: ограничение в схеме тем полезнее, чем стабильнее правило. Инварианты вроде «сумма не отрицательна» или «номер заказа уникален в рамках тенанта» живут годами. Изменчивую бизнес-логику («скидка не больше 30% по вторникам») в CHECK загонять не стоит — каждое изменение правила превратится в миграцию.
Как отвечать на собеседовании
Нормальные формы объясняйте смыслом, а не заученными формулировками: цель нормализации — хранить каждый факт в ОДНОМ месте, чтобы не было аномалий вставки, обновления и удаления. Дальше уже можно назвать 1NF (атомарные значения), 2NF (нет зависимости от части составного ключа) и 3NF (неключевые атрибуты не зависят друг от друга).
Ключевая фраза, которую ждут: нормализуйте по умолчанию, денормализуйте осознанно — после измерений и с явным механизмом поддержания согласованности (триггер, пересчёт, событие). Денормализация — это обмен: чтение дешевеет, запись усложняется, появляется риск рассинхронизации.
Про ключи дайте готовую позицию: суррогатный PK для стабильности плюс unique на естественный ключ для бизнес-правила. Это лучше, чем абстрактно рассуждать, что «у обоих есть плюсы».
Каскады подавайте как проектное решение: CASCADE для строк, не имеющих смысла без родителя (позиции заказа), RESTRICT когда удаление должно быть осознанным, SET NULL для опциональной связи. И упомяните риск CASCADE на большом графе — одно удаление способно снести многое.
Завершите критерием про constraints: критичные инварианты закрепляем в схеме, потому что это последняя линия защиты, которую не обойдёт ни баг, ни второй сервис, ни ручной UPDATE в проде.