Типичный код валидации перед вставкой:
if (checkFreeSlot(room, start, end)) {
db.insertBooking(room, start, end);
}
При параллельных запросах возникает Race Condition: обе сессии увидят свободный слот и одновременно запишут пересекающиеся брони!
NOT NULL, DEFAULT, типы данных.CHECK со сложными предикатами.PRIMARY KEY, UNIQUE, EXCLUDE.FOREIGN KEY с режимами каскадирования.Ограничение должно располагаться на максимально низком уровне абстракции:
Если бизнес-правило касается допустимых значений одного типа — используем DOMAIN. Если связывает поля одной строки — CHECK. Если касается уникальности интервалов — EXCLUDE. Если связей — FOREIGN KEY.
Вместо многократного дублирования CHECK-выражений в десятках таблиц проектировщик определяет предметный домен:
-- Единый домен для проверки формата адреса электронной почты
CREATE DOMAIN email_address AS VARCHAR(255)
CHECK (VALUE ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
-- Домен для государственного номера автомобиля РФ
CREATE DOMAIN car_plate_ru AS VARCHAR(9)
CHECK (VALUE ~ '^[АВЕКМНОРСТУХ]\d{3}[АВЕКМНОРСТУХ]{2}\d{2,3}$');
Преимущество: при изменении нормативных требований достаточно обновить одно доменное правило, и оно мгновенно применится ко всем таблицам системы.
~ — совпадение с шаблоном (с учетом регистра).~* — совпадение с шаблоном (без учета регистра).!~ — не совпадает с учетом регистра.!~* — не совпадает без учета регистра.Без якорей проверяется вхождение подстроки!
VALUE ~ '\d{4}' пропустит строку 'серия 12345678'.
Строгий стандарт: VALUE ~ '^\d{4}$' — строго 4 цифры от начала до конца.
-- Проверка формата СНИЛС (ХХХ-ХХХ-ХХХ ХХ)
ALTER TABLE users ADD CONSTRAINT chk_snils
CHECK (snils ~ '^\d{3}-\d{3}-\d{3} \d{2}$');
Ограничение CHECK отклоняет запись ТОЛЬКО тогда, когда результат предиката равен FALSE!
Если результат вычисления — UNKNOWN (например, из-за присутствия NULL), строка УСПЕШНО ДОБАВЛЯЕТСЯ в таблицу!
Предикат: CHECK (discount_price < base_price)
base_price = 1000, discount_price = 800 → TRUE → Вставка разрешена.base_price = 1000, discount_price = 1200 → FALSE → Отказ!base_price = 1000, discount_price = NULL → UNKNOWN → Вставка разрешена!Классическое ограничение UNIQUE (room_id, start_time) запрещает только полное совпадение момента старта, но бессильно против частичного наложения интервалов:
Бронь №1: [10:00 ----------------------- 12:00)
Бронь №2: [11:00 ----------- 13:00) ← Наложение! UNIQUE не сработает!
Вывод: проверка равенства (=) неспособна выразить перекрытие интервалов (&&). Требуется специализированный механизм интервальных ограничений.
tsrange — диапазон отметок времени без таймзоны.tstzrange — диапазон отметок времени с таймзоной.daterange — диапазон календарных дат.&& — проверка перекрытия (пересечения).@> — содержит элемент или диапазон.<@ — содержится внутри диапазона.-|- — диапазоны смежны (стыкуются без зазора).-- Пример: полуоткрытый интервал [включено, не включено)
SELECT tsrange('2026-10-01 10:00', '2026-10-01 12:00', '[)') &&
tsrange('2026-10-01 11:30', '2026-10-01 13:00', '[)'); -- Результат: TRUE (конфликт!)
Синтаксис ограничения исключения задает пары «столбец + оператор»: для любых двух строк хотя бы одно из условий обязано вернуть FALSE.
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE conference_bookings (
booking_id BIGSERIAL PRIMARY KEY,
room_id INT NOT NULL,
slot TSRANGE NOT NULL,
organizer VARCHAR(100) NOT NULL,
-- Проверка корректности интервала (начало строго раньше конца)
CONSTRAINT chk_slot_order CHECK (lower(slot) < upper(slot)),
-- Исключение: для одного room_id (=) интервалы slot НЕ должны пересекаться (&&)
CONSTRAINT exc_no_overlap EXCLUDE USING gist (
room_id WITH =,
slot WITH &&
)
);
<, <=, =, >=, >.btree_gist позволяет смешивать скаляры (room_id) и диапазоны.Проблема взаимно зависимых сущностей в проектировании схемы:
hotels.suite_id REFERENCES rooms(id).rooms.hotel_id REFERENCES hotels(id).NOT NULL.Какую таблицу заполнять первой? Ни одну невозможно вставить!
Ослабление ограничений до NULL — компромисс, разрушающий целостность.
Профессиональное решение — транзакционная отложенность внешнего ключа (DEFERRABLE): СУБД откладывает проверку связей до команды COMMIT.
Ограничение может быть отложено, но по умолчанию проверяется после каждой команды DML.
Для откладывания требуется явная команда внутри транзакции:
SET CONSTRAINTS fk_name DEFERRED;
Ограничение автоматически откладывается до фиксации транзакции (COMMIT).
Идеально для постоянных циклических зависимостей: прикладной код просто выполняет вставки в рамках BEGIN...COMMIT.
-- Создание отложенного внешнего ключа
ALTER TABLE hotels ADD CONSTRAINT fk_hotel_suite
FOREIGN KEY (presidential_suite_id) REFERENCES rooms(room_id)
DEFERRABLE INITIALLY DEFERRED;
-- Бесконфликтная атомарная транзакция:
BEGIN;
-- Вставляем отель: номер 101 еще не создан, но ошибки нет!
INSERT INTO hotels (hotel_id, name, presidential_suite_id) VALUES (1, 'Grand Palace', 101);
-- Вставляем номер: отель 1 уже существует
INSERT INTO rooms (room_id, hotel_id, room_number) VALUES (101, 1, 'Penthouse 101');
-- Фиксация транзакции: СУБД проверяет fk_hotel_suite. Все сошлось!
COMMIT;
Ограничения не должны создавать логических тупиков. Три правила аудитора схемы:
CHECK (age > 21 AND age < 18) → пустое множество значений, блокировка вставки.
FK ссылается на справочник с ID 1..50, а CHECK требует id > 1000 → ни одна строка не свяжется.
Поле с NOT NULL и правилом внешнего ключа ON DELETE SET NULL гарантированно вызовет аварийный откат при удалении родительской записи.