open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 06 · 02

Ограничения: целостность, которую обеспечивает движок

NOT NULL, CHECK, UNIQUE, внешние ключи с действиями ON DELETE, DEFERRABLE-проверки и EXCLUDE-ограничения проталкивают целостность данных в движок — где приложение не сможет о ней забыть.

SQL Middle ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Система бронирования следила за «никаких двойных броней зала» в приложении: читаем календарь, проверяем пересечение, потом вставляем. Работало во всех тестах. В проде два запроса попали в один зал в одно окно 40 мс, оба прочитали пустой слот, оба вставили — и свадьба с конференцией вошли в один зал. Никакой аккуратный код приложения не чинит гонку, которую база могла отвергнуть атомарно. Ограничения — не синтаксический сахар валидации; это последняя и единственная линия, мимо которой две конкурентные транзакции не проскользнут обе.

Суть: целостность принадлежит движку

Любое правило о валидности данных — «цена никогда не отрицательна», «заказ ссылается на реального пользователя», «никаких двойных броней зала» — может жить в двух местах: разбросанным по коду приложения или объявленным один раз на таблице. Проверки приложения совещательны: они работают лишь на путях, которые не забыли их вызвать, и проигрывают любую гонку конкурентности. Ограничение обеспечивается принудительно: оно работает в той же транзакции, что и запись, держит нужные блокировки и отвергает плохие данные атомарно, какой бы сервис, скрипт или psql-сессия их ни вставляли. (Глава relational-model трека databases разбирает ограничения и ключи со стороны теории; здесь мы подключаем их в Postgres.)

NOT NULL, CHECK, UNIQUE, PRIMARY KEY

Повседневная тройка. NOT NULL запрещает пропущенное значение. CHECK обеспечивает булев предикат на строку — рабочая лошадь для бизнес-правил. UNIQUE запрещает дублирующиеся значения и опирается на уникальный индекс, так что стоит по индексу на ограничение. PRIMARY KEY — это ровно NOT NULL + UNIQUE, плюс маркер, что это та самая идентичность строки (одна на таблицу).

CREATE TABLE products (
  id          bigint PRIMARY KEY,
  sku         text NOT NULL UNIQUE,                 -- нет двух товаров с одним SKU
  price_cents bigint NOT NULL CHECK (price_cents >= 0),
  status      text NOT NULL CHECK (status IN ('draft','active','archived'))
);

CHECK может ссылаться на несколько колонок одной строки (CHECK (sale_price <= price_cents)), но не на другие строки или другие таблицы — он строго построчный. Для «валидно по строкам» нужны UNIQUE, EXCLUDE или триггер.

Внешние ключи и действия ON DELETE / ON UPDATE

FOREIGN KEY говорит «эта колонка должна совпадать с существующей строкой в другой таблице» — ссылочная целостность. Интересное — что происходит, когда ссылаемую строку удаляют или её ключ меняется. Ты объявляешь действие:

  • ON DELETE RESTRICT (и похожий NO ACTION) — отказать в удалении родителя, пока есть дети. Безопасный дефолт для большинства связей; он всплывает случайные каскадные удаления как ошибки.
  • ON DELETE CASCADE — удалить и детей тоже. Подходит для настоящего владения (удалил заказ — его позиции уходят с ним); опасно, когда «ребёнок» общий или ценный.
  • ON DELETE SET NULL — оставить ребёнка, но обнулить ссылку. Для опциональных связей (manager_id сотрудника, когда менеджер уходит).
CREATE TABLE orders (
  id      bigint PRIMARY KEY,
  user_id bigint NOT NULL REFERENCES users(id) ON DELETE RESTRICT
);

CREATE TABLE order_items (
  id       bigint PRIMARY KEY,
  order_id bigint NOT NULL REFERENCES orders(id) ON DELETE CASCADE,  -- принадлежит заказу
  product_id bigint NOT NULL REFERENCES products(id) ON DELETE RESTRICT
);

Заметь намеренную смесь: удаление заказа каскадит на его order_items (без него они бессмысленны), но нельзя удалить product, на который ссылается любая позиция заказа — это переписало бы историю. Выбор действия для каждой связи и есть моделирование.

DEFERRABLE: проверка на COMMIT, а не на операторе

По умолчанию ограничение проверяется немедленно, в момент выполнения оператора. Это ломает циклические или зависящие от порядка записи — вставку двух строк, ссылающихся друг на друга, или перестановку списка, где позиции должны оставаться уникальными в середине апдейта. Ограничение DEFERRABLE INITIALLY DEFERRED проверяется один раз, на COMMIT, так что данные могут быть временно несогласованными внутри транзакции, если они согласованы к моменту коммита.

ALTER TABLE order_items
  ADD CONSTRAINT uniq_position UNIQUE (order_id, position)
  DEFERRABLE INITIALLY DEFERRED;
-- теперь можно поменять местами позиции 1 и 2 в двух UPDATE;
-- проверка уникальности сработает лишь на COMMIT, когда они снова согласованы.

EXCLUDE: UNIQUE, обобщённый до любого оператора

UNIQUE говорит «никакие две строки не равны по этим колонкам». EXCLUDE обобщает это до любого оператора: «никакие две строки не конфликтуют по оператору X». Убойное применение — бронь без пересечений: объедини range-тип с оператором пересечения && и GiST-индексом (Generalized Search Tree, обобщённое дерево поиска), и база сделает двойную бронь физически невозможной:

CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE bookings (
  id      bigint PRIMARY KEY,
  room_id bigint NOT NULL,
  during  tstzrange NOT NULL,
  EXCLUDE USING gist (room_id WITH =, during WITH &&)  -- тот же зал И пересекающееся время → отказ
);

Это атомарный фикс гонки из Hook. Две конкурентные вставки на один зал и пересекающееся окно не могут обе пройти: GiST-индекс сериализует проверку конфликта, и вторая вставка отвергается. Ни окна read-then-write, ни логики приложения, ни гонки.

Почему это работает

Почему не обеспечить всё это в приложении и держать схему простой? Потому что приложение множественно и забывчиво. Есть API, админка, ночной batch-джоб, скрипт правки данных, который кто-то гоняет в psql в 2 ночи, и переписка следующего года на другом языке. Каждый из них должен помнить каждое правило, и никто не выиграет гонку конкурентности так, как это делает ограничение на основе блокировки и индекса. Ограничение обеспечивается один раз, для всех писателей, навсегда — поэтому «протолкни целостность в базу» это сеньорский рефлекс, а не золочение.

Расставь шаги по порядку

Упорядочь ограничения от самого узкого правила до самого широкого класса плохих данных, который оно отвергает:

  1. 1 NOT NULL — одно значение должно присутствовать
  2. 2 CHECK — построчный предикат должен выполняться
  3. 3 UNIQUE — никакие две строки не равны по ключу
  4. 4 FOREIGN KEY — значение должно ссылаться на существующую строку другой таблицы
  5. 5 EXCLUDE — никакие две строки не конфликтуют по оператору (например пересечение диапазонов)
Викторина

Почему UNIQUE-ограничение — верный фикс двойной брони вместо «прочитай календарь, потом вставь» в коде приложения?

Викторина

Ты удаляешь товар, на который ещё ссылаются order_items. FK задан как ON DELETE RESTRICT. Что произойдёт?

Вспомните перед уходом
  1. 01
    Почему ограничение базы сильнее того же правила, обеспеченного кодом приложения?
  2. 02
    Назови три действия ON DELETE и когда каждое верно.
  3. 03
    Что EXCLUDE добавляет к UNIQUE и как он предотвращает двойную бронь?
Итог

Ограничение — это правило данных, которое движок обеспечивает на каждой записи, внутри транзакции, для каждого писателя — поэтому целостность принадлежит базе, а не разбросана по забывчивому, проигрывающему гонки коду приложения. Слои растут от узкого к широкому: NOT NULL (значение присутствует), CHECK (построчный предикат, рабочая лошадь бизнес-правил, но слепой к другим строкам), UNIQUE (нет дубля ключа, на основе индекса), PRIMARY KEY (NOT NULL + UNIQUE, идентичность строки), FOREIGN KEY (ссылочная целостность, где действие ON DELETERESTRICT, CASCADE или SET NULL — реальное решение моделирования для каждой связи) и EXCLUDE (UNIQUE, обобщённый до любого оператора). DEFERRABLE INITIALLY DEFERRED откладывает проверку до COMMIT, чтобы зависящие от порядка записи могли быть временно несогласованными. Звезда — EXCLUDE USING gist (... WITH &&) на range-типе: он делает двойную бронь атомарно невозможной, правильный фикс гонки read-then-write, которую не выиграет код приложения. Теперь, когда увидишь бизнес-правило, живущее только в коде приложения, спроси себя: не стоит ли сделать его CHECK, внешним ключом или EXCLUDE — и если да, это работа движка, а не приложения.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.