Ограничения: целостность, которую обеспечивает движок
NOT NULL, CHECK, UNIQUE, внешние ключи с действиями ON DELETE, DEFERRABLE-проверки и EXCLUDE-ограничения проталкивают целостность данных в движок — где приложение не сможет о ней забыть.
Система бронирования следила за «никаких двойных броней зала» в приложении: читаем календарь, проверяем пересечение, потом вставляем. Работало во всех тестах. В проде два запроса попали в один зал в одно окно 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 NOT NULL — одно значение должно присутствовать
- 2 CHECK — построчный предикат должен выполняться
- 3 UNIQUE — никакие две строки не равны по ключу
- 4 FOREIGN KEY — значение должно ссылаться на существующую строку другой таблицы
- 5 EXCLUDE — никакие две строки не конфликтуют по оператору (например пересечение диапазонов)
Почему UNIQUE-ограничение — верный фикс двойной брони вместо «прочитай календарь, потом вставь» в коде приложения?
Ты удаляешь товар, на который ещё ссылаются order_items. FK задан как ON DELETE RESTRICT. Что произойдёт?
- 01Почему ограничение базы сильнее того же правила, обеспеченного кодом приложения?
- 02Назови три действия ON DELETE и когда каждое верно.
- 03Что EXCLUDE добавляет к UNIQUE и как он предотвращает двойную бронь?
Ограничение — это правило данных, которое движок обеспечивает на каждой записи, внутри транзакции, для каждого писателя — поэтому целостность принадлежит базе, а не разбросана по забывчивому, проигрывающему гонки коду приложения. Слои растут от узкого к широкому: NOT NULL (значение присутствует), CHECK (построчный предикат, рабочая лошадь бизнес-правил, но слепой к другим строкам), UNIQUE (нет дубля ключа, на основе индекса), PRIMARY KEY (NOT NULL + UNIQUE, идентичность строки), FOREIGN KEY (ссылочная целостность, где действие ON DELETE — RESTRICT, CASCADE или SET NULL — реальное решение моделирования для каждой связи) и EXCLUDE (UNIQUE, обобщённый до любого оператора). DEFERRABLE INITIALLY DEFERRED откладывает проверку до COMMIT, чтобы зависящие от порядка записи могли быть временно несогласованными. Звезда — EXCLUDE USING gist (... WITH &&) на range-типе: он делает двойную бронь атомарно невозможной, правильный фикс гонки read-then-write, которую не выиграет код приложения. Теперь, когда увидишь бизнес-правило, живущее только в коде приложения, спроси себя: не стоит ли сделать его CHECK, внешним ключом или EXCLUDE — и если да, это работа движка, а не приложения.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.