SQL

БЛОК 1. Основы реляционных БД — 4. Ключи и ограничения

📚 24 вопросовПройти тест →
Лекция

БЛОК 1. Основы реляционных БД — 4. Ключи и ограничения

SQL

4. Ключи и ограничения

🧭 Введение: почему ключи и ограничения важнее «доверия к коду»

Многие начинающие команды сначала пишут логику в приложении и считают, что этого достаточно для контроля данных.
Проблема начинается, когда один сервис пишет в БД напрямую, второй работает через другой API, а третий делает массовую загрузку.
Ключи и ограничения в БД нужны как последняя линия защиты: они не дают сохранить дубли, «битые» ссылки и невалидные значения.
Именно поэтому надёжная схема строится не только на валидации в коде, но и на правилах внутри самой СУБД.
💡 Совет: Проектируйте ключи и ограничения как контракт данных между всеми сервисами, а не как локальную настройку одного backend-модуля.
Вывод: Ключи и ограничения — это фундамент целостности данных, без которого система быстро получает скрытые ошибки.

⚠️ Проблема -> решение

Если ограничения не заданы, в БД появляются дубликаты пользователей, заказы на несуществующие товары и статусы вне бизнес-логики.
На старте это может быть незаметно, но при росте продукта такие данные ломают отчёты, расчёты и автоматизацию.
Решение — задать ключи (PRIMARY KEY, FOREIGN KEY) и базовые ограничения (UNIQUE, NOT NULL, CHECK, DEFAULT) на уровне схемы.
Тогда СУБД сама блокирует некорректные изменения, а команда тратит меньше времени на «пожарные» исправления.
🟢 Если совсем просто: Не доверяйте данным «на слово» — закрепляйте правила в БД.
🎯 Как понять, что этап прошёл успешно: База не даёт вставить дубли и невалидные связи даже при ошибке в коде приложения.

🛠️ Чем помогает и как работает

Ключи и ограничения делают структуру данных предсказуемой: любой сервис пишет в одну и ту же модель с одинаковыми правилами.
Это снижает стоимость поддержки, ускоряет расследование багов и упрощает миграции.
🟢 Если совсем просто: Схема с ограничениями сама защищает данные от большинства типовых ошибок.
🎯 Как понять, что этап прошёл успешно: Некорректные операции ломаются сразу на INSERT/UPDATE, а не всплывают в проде через неделю.
Чем помогает:
  • предотвращает дубли ключевых сущностей;
  • гарантирует целостность ссылок между таблицами;
  • защищает от пустых и нелогичных значений;
  • делает данные надёжной основой для аналитики и автоматизации.
Как это работает:
  • Шаг 1: определяем, какая колонка (или комбинация колонок) однозначно идентифицирует строку.
  • Шаг 2: задаём PRIMARY KEY для каждой таблицы.
  • Шаг 3: описываем связи таблиц через FOREIGN KEY.
  • Шаг 4: добавляем UNIQUE там, где дубли бизнес-значений недопустимы.
  • Шаг 5: фиксируем обязательные поля через NOT NULL.
  • Шаг 6: ограничиваем допустимые значения через CHECK.
  • Шаг 7: задаём безопасные значения по умолчанию через DEFAULT.
  • Шаг 8: проверяем сценарии insert/update/delete и действия при удалении (RESTRICT, CASCADE, SET NULL).
Вывод: Хорошая схема не просто хранит данные, а активно контролирует их качество.

📚 Ключевые термины (простыми словами)

Перед практикой важно договориться о базовых терминах, чтобы одинаково понимать требования к схеме.
🟢 Если совсем просто: Это словарь, который нужен для любого ревью SQL-схемы.
🎯 Как понять, что этап прошёл успешно: Вы можете объяснить каждый термин на примере таблиц своего проекта.
  • Primary Key (первичный ключ) — уникальный идентификатор строки, не допускает NULL.
  • Foreign Key (внешний ключ) — ссылка на строку в другой таблице.
  • Unique Constraint (ограничение уникальности) — запрещает дубли значения/комбинации.
  • Not Null Constraint (ограничение обязательности) — запрещает пустое значение.
  • Check Constraint (проверка условия) — пропускает только значения, подходящие под правило.
  • Default (значение по умолчанию) — автоматически подставляемое значение, если поле не передали.
  • Composite Key (составной ключ) — ключ из нескольких колонок.
  • Referential Integrity (ссылочная целостность) — гарантия, что ссылка указывает на существующую запись.
Вывод: Чем точнее команда понимает термины, тем меньше ошибок в проектировании ограничений.

🧱 1. PRIMARY KEY: как однозначно идентифицировать запись

Без первичного ключа таблица теряет управляемость: сложно обновлять конкретную строку и невозможно надёжно строить связи.
PRIMARY KEY — минимальное требование к любой «боевой» сущности.
🟢 Если совсем просто: PK отвечает на вопрос: «Как точно найти эту запись среди всех?»
🎯 Как понять, что этап прошёл успешно: В каждой таблице есть один явный способ уникально идентифицировать строку.
Назначение: Обеспечить уникальную идентификацию каждой записи.
Простыми словами: Это паспорт строки, который не повторяется и не бывает пустым.
Для новичка: Если у таблицы нет PK, считайте её временной заготовкой, а не полноценной частью модели.
Аналогия: Как номер паспорта у человека: имена могут совпадать, номер — нет.
Пример:
CREATE TABLE users (  id BIGSERIAL PRIMARY KEY,  email TEXT NOT NULL UNIQUE,  full_name TEXT NOT NULL,  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
🔎 Как это происходит на практике:
  • Контекст: команда проектирует таблицу пользователей.
  • Действия: выбирает суррогатный ключ id как PK и отдельно фиксирует бизнес-уникальность email.
  • Результат: запись стабильно идентифицируется по id, а бизнес-правило уникальности тоже соблюдается.
Характеристики:
  • PK уникален;
  • PK не содержит NULL;
  • как правило, в большинстве популярных СУБД для PK автоматически создаётся индекс.
Когда использовать: Всегда для сущностей, которые участвуют в связях и транзакциях.
Вывод: PK — базовый якорь целостной схемы.

🔗 2. FOREIGN KEY: защита от «битых» ссылок

Связи между таблицами без FK часто превращаются в «договорённость в коде», которая ломается при обходных вставках и миграциях.
FOREIGN KEY переносит контроль связей в саму СУБД.
🟢 Если совсем просто: FK не даёт ссылаться на запись, которой не существует.
🎯 Как понять, что этап прошёл успешно: Невозможно создать заказ на несуществующего пользователя или товар.
Назначение: Гарантировать ссылочную целостность между сущностями.
Простыми словами: Каждая ссылка должна вести на реально существующую запись.
Для новичка: Если колонка хранит *_id другой таблицы, это почти всегда кандидат на FK.
Аналогия: Это как билет в поезд: номер вагона должен существовать в составе.
Пример:
CREATE TABLE orders (  id BIGSERIAL PRIMARY KEY,  user_id BIGINT NOT NULL REFERENCES users(id),  status TEXT NOT NULL,  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
🔎 Как это происходит на практике:
  • Контекст: нужно связать заказ с пользователем.
  • Действия: orders.user_id закрепляют через REFERENCES users(id).
  • Результат: БД не пропускает строки с невалидной ссылкой.
Характеристики:
  • защищает от orphan-записей;
  • упрощает ревью модели;
  • индекс чаще всего нужен на FK-колонке в дочерней таблице (child);
  • PK родительской таблицы (parent) обычно уже индексирован.
Когда использовать: В большинстве OLTP-сценариев и везде, где связь важна для бизнес-логики.
Вывод: FK превращает связь из «предположения» в технически гарантированное правило.

✅ 3. UNIQUE, NOT NULL, CHECK, DEFAULT: поведенческие правила полей

Ключи отвечают за идентификацию и связи, но этого мало.
Нужны ограничения, которые задают корректные границы значений в бизнес-полях.
🟢 Если совсем просто: Эти ограничения говорят БД, какие данные допустимы, а какие нет.
🎯 Как понять, что этап прошёл успешно: БД сама отклоняет пустые, дублирующиеся и нелогичные значения.
Назначение: Закрепить бизнес-правила на уровне отдельных колонок и их комбинаций.
Простыми словами: СУБД должна не только хранить, но и «фильтровать мусор».
Для новичка: Если правило важно для бизнеса, его лучше продублировать в БД, а не держать только в приложении.
Аналогия: Это как правила на производственной линии: деталь с браком не доходит до финального этапа.
Пример:
CREATE TABLE products (  id BIGSERIAL PRIMARY KEY,  sku TEXT NOT NULL UNIQUE,  title TEXT NOT NULL,  price NUMERIC(10,2) NOT NULL CHECK (price >= 0),  status TEXT NOT NULL CHECK (status IN ('draft', 'active', 'archived')),  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
🔎 Как это происходит на практике:
  • Контекст: товар заводят вручную через админку и автоматически через интеграции.
  • Действия: команда задаёт обязательность, уникальность и допустимые значения статуса/цены.
  • Результат: некорректные данные блокируются до попадания в рабочие процессы.
Характеристики:
  • UNIQUE защищает от дублей;
  • NOT NULL фиксирует обязательность;
  • CHECK ограничивает диапазон и справочники статусов;
  • для статусов часто выбирают CHECK / ENUM / справочник + FOREIGN KEY; выбор зависит от частоты изменений и домена;
  • DEFAULT уменьшает количество пропусков при записи.
Когда использовать: Для всех критичных бизнес-полей с явными правилами.
Вывод: Эти ограничения снижают число «невидимых» ошибок в данных сильнее, чем любые post-factum проверки.

🧩 4. Составные ключи и выбор стратегии ключа

В практических схемах встречаются как суррогатные ключи (id), так и составные ключи.
Главное — выбирать стратегию осознанно, исходя из роли таблицы.
🟢 Если совсем просто: Для сущностей чаще удобен id, для таблиц-связок часто уместен составной ключ.
🎯 Как понять, что этап прошёл успешно: Ключ отражает смысл таблицы и не создаёт лишних дублей.
Назначение: Выбрать формат ключа, который делает модель устойчивой и удобной в сопровождении.
Простыми словами: Нужен не «модный» ключ, а ключ, который лучше защищает конкретную таблицу.
Для новичка: Если таблица описывает факт связи N-N, составной PK (a_id, b_id) часто естественнее отдельного id.
Аналогия: В журнале посещений уникальность может задаваться парой «кто + куда», а не отдельным номером строки.
Пример:
CREATE TABLE order_items (  order_id BIGINT NOT NULL REFERENCES orders(id),  product_id BIGINT NOT NULL REFERENCES products(id),  qty INT NOT NULL CHECK (qty > 0),  unit_price NUMERIC(10,2) NOT NULL CHECK (unit_price >= 0),  PRIMARY KEY (order_id, product_id));
🔎 Как это происходит на практике:
  • Контекст: нужно запретить повтор одной и той же позиции товара в заказе.
  • Действия: таблица-связка получает составной PK из двух FK.
  • Результат: дубли связки невозможны без дополнительной логики в приложении.
Характеристики:
  • хорошо подходит для junction-таблиц;
  • упрощает контроль уникальности пары;
  • требует аккуратного дизайна зависимых FK в дочерних таблицах.
Когда использовать: Для таблиц-связок и ситуаций, где бизнес-уникальность естественно задаётся комбинацией колонок.
Вывод: Стратегия ключа должна следовать доменной логике, а не шаблону «всегда только id».

🛡️ 5. Действия при удалении и обновлении: RESTRICT, CASCADE, SET NULL

Даже правильно заданные FK могут вести к неожиданным эффектам, если не определено поведение при удалении родительской записи.
Поэтому важно выбирать политику каскада сознательно.
🟢 Если совсем просто: Нужно заранее решить, что делать с дочерними строками при удалении родителя.
🎯 Как понять, что этап прошёл успешно: Любое удаление в продакшене ведёт к предсказуемому результату и не нарушает бизнес-логику.
Назначение: Определить безопасное поведение зависимых данных при изменении/удалении ключевых записей.
Простыми словами: Система должна «знать», удалять хвосты, блокировать удаление или обнулять ссылку.
Для новичка: CASCADE удобен, но опасен без чёткого понимания последствий.
Аналогия: Это как удаление папки: можно запретить, можно удалить вместе с файлами, можно оставить файлы отдельно.
Пример:
CREATE TABLE payments (  id BIGSERIAL PRIMARY KEY,  order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,  amount NUMERIC(10,2) NOT NULL CHECK (amount >= 0),  status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'failed')));
🔎 Как это происходит на практике:
  • Контекст: заказ удаляют в тестовом и боевом процессах.
  • Действия: команда выбирает CASCADE для зависимых технических сущностей и RESTRICT для критичных исторических данных.
  • Результат: удаление контролируемо и согласовано с бизнес-требованиями.
Характеристики:
  • RESTRICT — запрет удаления при наличии зависимостей;
  • CASCADE — удаление зависимых строк;
  • SET NULL — отвязка дочерней записи без удаления.
  • для исторических и аудиторских таблиц часто используют soft delete (deleted_at) вместо физического удаления;
  • ON UPDATE CASCADE применяется реже, потому что PK обычно не меняют.
Когда использовать: При проектировании всех FK, особенно для финансовых и аудиторских сущностей.
Вывод: Политика удаления — это часть бизнес-логики, а не второстепенная SQL-настройка.

🚀 6. Ограничения в продакшене: как вводить безопасно

Добавление ограничений в живую базу может падать, если старые данные уже нарушают новые правила.
Поэтому важно внедрять ограничения поэтапно, а не «одним ALTER в пятницу вечером».
🟢 Если совсем просто: Сначала проверяем и чистим данные, потом включаем жёсткие ограничения.
🎯 Как понять, что этап прошёл успешно: Миграция проходит без блокирующих ошибок, а приложение не ломается после релиза.
Назначение: Внедрять ограничения в рабочей БД без потери данных и аварий.
Простыми словами: Нельзя включать правило, пока данные к нему не готовы.
Для новичка: Перед добавлением NOT NULL или UNIQUE всегда делайте проверочный запрос по текущим данным.
Аналогия: Это как ввод нового регламента на заводе: сначала аудит текущего процесса, потом запуск обязательного контроля.
Пример:
-- 1) Находим дубли до UNIQUESELECT email, COUNT(*)FROM usersGROUP BY emailHAVING COUNT(*) > 1; -- 2) После очистки включаем ограничениеALTER TABLE users  ADD CONSTRAINT users_email_unique UNIQUE (email);
Для продакшена в PostgreSQL (поэтапная валидация):
ALTER TABLE orders  ADD CONSTRAINT orders_user_id_fk  FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID; ALTER TABLE orders  VALIDATE CONSTRAINT orders_user_id_fk;
Для UNIQUE в PostgreSQL обычно используют другую стратегию (например, через создание уникального индекса), а NOT VALID чаще применяют для CHECK и FOREIGN KEY.
🔎 Как это происходит на практике:
  • Контекст: legacy-данные уже содержат нарушения.
  • Действия: команда выявляет конфликты, исправляет их и только потом добавляет ограничения.
  • Результат: миграция становится повторяемой и безопасной.
Характеристики:
  • изменения делаются через миграции;
  • сначала аудит данных, потом ограничение;
  • в PostgreSQL поэтапная валидация (NOT VALID -> VALIDATE CONSTRAINT) особенно полезна для CHECK и FOREIGN KEY;
  • для критичных изменений готовится rollback-план.
Когда использовать: Всегда при ужесточении схемы в продакшене.
Вывод: Технически верное ограничение может быть операционно опасным без правильной последовательности внедрения.

⚖️ Сравнение ключевых ограничений

Каждый тип ограничения решает свою задачу, и важно использовать их в связке.
Ниже компактная таблица для быстрого ориентирования.
🟢 Если совсем просто: PK определяет запись, FK связывает записи, остальные ограничения задают правила значений.
🎯 Как понять, что этап прошёл успешно: Для каждого бизнес-правила в схеме есть явный тип ограничения.
ОграничениеЧто контролируетТиповая ошибка без него
PRIMARY KEYуникальную идентификацию строкидубли и неоднозначные обновления
FOREIGN KEYссылочную целостностьссылки на несуществующие записи
UNIQUEнедопустимость дубля значениядва одинаковых email/sku
NOT NULLобязательность поляпустые критичные значения
CHECKдиапазон/список допустимых значенийотрицательная цена, невалидный статус
DEFAULTбезопасное значение при пропускенепредсказуемые пустые поля
Вывод: Стабильная схема строится не на одном «главном» ограничении, а на их согласованной комбинации.

📌 Must-know факты

Ниже минимум, который нужно помнить для практики и собеседований.
🟢 Если совсем просто: Это базовый чек-лист качества схемы.
🎯 Как понять, что этап прошёл успешно: Вы можете пройтись по схеме и подтвердить каждый пункт SQL-примером.
  • Каждая основная таблица должна иметь PRIMARY KEY.
  • Каждая критичная связь между сущностями должна быть закреплена через FOREIGN KEY.
  • Бизнес-уникальность (email, sku, внешний номер) должна иметь UNIQUE.
  • Обязательные поля фиксируются через NOT NULL.
  • Диапазоны и статусы фиксируются через CHECK.
  • Безопасные стартовые значения задаются через DEFAULT.
  • Индекс чаще всего нужен на FK-поле дочерней таблицы (child), а PK родителя (parent) обычно уже индексирован.
  • Ограничения в проде вводят поэтапно: аудит данных -> очистка -> миграция.
Вывод: Если хотя бы один must-know блок пропущен, целостность данных быстро деградирует.

🧨 Частые мифы

Ниже мифы, которые чаще всего приводят к «грязной» базе.
🟢 Если совсем просто: Мифы обычно звучат как «сейчас так быстрее», но потом дают долгие последствия.
🎯 Как понять, что этап прошёл успешно: Вы можете на реальном кейсе показать, почему миф ведёт к ошибкам.
Миф: Ограничения тормозят разработку, поэтому лучше добавить их потом. ✅ Как правильно: Минимальный набор ограничений нужен сразу, иначе к моменту «потом» данные уже испорчены. 📎 Почему это важно: Исправлять накопленные нарушения сложнее, чем предотвратить их в момент записи.
Миф: Если есть ORM-валидация, ограничения БД не нужны. ✅ Как правильно: ORM-валидация полезна, но БД должна дублировать критичные правила. 📎 Почему это важно: В БД могут писать разные сервисы и скрипты, не проходящие ваш ORM-слой.
Миф: CASCADE всегда лучший выбор, чтобы «не думать». ✅ Как правильно: Политику удаления выбирают по доменной логике и рискам потери данных. 📎 Почему это важно: Непродуманный CASCADE может удалить критичную историю.
Миф: UNIQUE заменяет PRIMARY KEY. ✅ Как правильно: UNIQUE решает бизнес-уникальность, а PK задаёт основной идентификатор строки. 📎 Почему это важно: Без явного PK усложняются связи, обновления и архитектурная согласованность.

❓ Часто спрашивают на собеседованиях

Это типовые вопросы для SQL-интервью по теме целостности данных.
🟢 Если совсем просто: Проверяют, умеете ли вы защищать данные на уровне схемы, а не только в приложении.
🎯 Как понять, что этап прошёл успешно: Вы можете ответить с примерами SQL, а не общими словами.
Вопрос: Чем отличается PRIMARY KEY от UNIQUE? ✅ Ответ: PK — основной идентификатор строки (у таблицы один PK), а UNIQUE — дополнительное правило недопустимости дубля для поля/комбинации.
Вопрос: Зачем нужен FOREIGN KEY, если есть проверки в коде? ✅ Ответ: FK защищает целостность независимо от конкретного сервиса и не позволяет сохранить невалидную ссылку при любом пути записи.
Вопрос: Когда использовать составной ключ? ✅ Ответ: Обычно в таблицах-связках, где уникальность определяется парой значений, например (order_id, product_id).
Вопрос: Что делает CHECK, чего не делает NOT NULL? ✅ Ответ: NOT NULL запрещает пустое значение, а CHECK контролирует допустимый диапазон/список значений.
Вопрос: Как безопасно добавить UNIQUE в продакшене? ✅ Ответ: Сначала найти и устранить дубли, затем добавить ограничение миграцией и проверить ключевые сценарии записи.
Вопрос: Когда выбирать RESTRICT, а когда CASCADE? ✅ Ответ: RESTRICT — когда нельзя терять зависимые данные, CASCADE — когда зависимые записи технически производны и должны удаляться вместе с родителем.
Вопрос: Почему индекс на FK-поле часто обязателен? ✅ Ответ: Индекс чаще всего нужен на FK-поле в дочерней таблице (child), потому что без него JOIN и фильтры быстро деградируют; PK родителя обычно уже индексирован.
Вопрос: Можно ли хранить бизнес-ключ как PK? ✅ Ответ: Можно, но чаще выбирают суррогатный PK и отдельно фиксируют бизнес-уникальность через UNIQUE для гибкости изменений.

🚫 Типичные ошибки

Ниже ошибки, которые чаще всего находят на code review SQL-схем.
🟢 Если совсем просто: Каждая ошибка ниже прямо влияет на качество данных и скорость исправления багов.
🎯 Как понять, что этап прошёл успешно: Вы проверили схему по каждому пункту и исключили эти риски.

Ошибка 1: таблица без первичного ключа

Неправильно: Создать таблицу «на время» без PK и оставить её в продакшене.
Правильно: Сразу задавать PK даже для простых справочников.
Почему: Без PK растёт риск дублей и усложняются связи.

Ошибка 2: хранить внешние ссылки без FK

Неправильно: Иметь поле user_id без REFERENCES users(id).
Правильно: Закреплять ссылку через FK и осознанную политику удаления.
Почему: Иначе появляются orphan-записи и ломается аналитика.

Ошибка 3: надеяться только на backend-валидацию

Неправильно: Проверять статусы и диапазоны только в приложении.
Правильно: Дублировать критичные правила через CHECK, NOT NULL, UNIQUE.
Почему: В БД могут писать разные потребители вне одного кода.

Ошибка 4: не индексировать FK-поля

Неправильно: Создать связи, но не добавить индексы под регулярные JOIN.
Правильно: Индексировать FK и горячие фильтры после моделирования связей.
Почему: Без индексов нормальная по логике схема может стать медленной в проде.

Ошибка 5: включать ограничения без проверки текущих данных

Неправильно: Сразу добавлять UNIQUE/NOT NULL в legacy-таблицу.
Правильно: Сначала аудит и очистка данных, потом ограничение миграцией.
Почему: Иначе миграция падает и релиз блокируется.

✅ Best Practices

Это практики, которые помогают держать схему устойчивой в долгую.
🟢 Если совсем просто: Best Practices снижают риск ошибок не только сейчас, но и при росте проекта.
🎯 Как понять, что этап прошёл успешно: Новая функциональность добавляется без «ручного лечения» данных.
  • Давайте каждой таблице явный PK.
  • Определяйте бизнес-уникальность отдельными UNIQUE-ограничениями.
  • Обязательные поля всегда фиксируйте через NOT NULL.
  • Статусы и диапазоны закрепляйте CHECK-правилами.
  • Для FK сразу выбирайте поведение при удалении (RESTRICT/CASCADE/SET NULL).
  • Индексируйте FK-поля и частые фильтры.
  • Внедряйте новые ограничения в продакшене поэтапно.
  • Документируйте, почему выбрана конкретная стратегия ключа.

🧾 Заключение

Ключи и ограничения — это не «дополнение» к схеме, а её ядро.
Именно они превращают БД в надёжный источник данных для всех сервисов и процессов.

Ключевые мысли

  • PK отвечает за идентичность строки.
  • FK отвечает за корректность связей.
  • UNIQUE/NOT NULL/CHECK/DEFAULT отвечают за качество значений.
  • Индексы на ключевых полях обеспечивают рабочую производительность.
Если ключи и ограничения продуманы правильно, большая часть ошибок отсекается ещё на уровне записи данных.
🎯

Проверьте знания

Закрепите материал — пройдите тест по теме «БЛОК 1. Основы реляционных БД — 4. Ключи и ограничения»

Пройти тест →