Средства проектирования структур БД: ограничения

1. Почему проверки в коде бэкенда недостаточно?

Иллюзия надежности веб-кода

Типичный код валидации перед вставкой:

if (checkFreeSlot(room, start, end)) {
    db.insertBooking(room, start, end);
}

При параллельных запросах возникает Race Condition: обе сессии увидят свободный слот и одновременно запишут пересекающиеся брони!

Декларативная защита СУБД

  • Единая точка истины: СУБД обслуживает веб, мобильные приложения, скрипты и админов.
  • Аппаратные инварианты: ядро СУБД гарантирует атомарность и блокирует некорректное состояние транзакции.
  • Zero-Tolerance к искажению: ни одна строка с поврежденными связями не попадет на диск.

2. Пирамида ограничений целостности

Уровни абстракции
  1. Домены (CREATE DOMAIN): семантические типы с проверкой формата.
  2. Атрибуты: NOT NULL, DEFAULT, типы данных.
  3. Кортежи: CHECK со сложными предикатами.
  4. Отношения: PRIMARY KEY, UNIQUE, EXCLUDE.
  5. Межтабличные связи: FOREIGN KEY с режимами каскадирования.

Ключевой принцип проектирования

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

Если бизнес-правило касается допустимых значений одного типа — используем DOMAIN. Если связывает поля одной строки — CHECK. Если касается уникальности интервалов — EXCLUDE. Если связей — FOREIGN KEY.

3. Ограничения доменов: переиспользуемая валидация

Вместо многократного дублирования 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}$');

Преимущество: при изменении нормативных требований достаточно обновить одно доменное правило, и оно мгновенно применится ко всем таблицам системы.

4. Предикаты CHECK с регулярными выражениями (POSIX regex)

Операторы сопоставления

  • ~ — совпадение с шаблоном (с учетом регистра).
  • ~* — совпадение с шаблоном (без учета регистра).
  • !~ — не совпадает с учетом регистра.
  • !~* — не совпадает без учета регистра.

Опасность забытых якорей (^ и $)

Без якорей проверяется вхождение подстроки!

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}$');

5. Трехзначная логика SQL: подводные камни CHECK

Правило отсечения строк

Ограничение CHECK отклоняет запись ТОЛЬКО тогда, когда результат предиката равен FALSE!

Если результат вычисления — UNKNOWN (например, из-за присутствия NULL), строка УСПЕШНО ДОБАВЛЯЕТСЯ в таблицу!

Пример расчета с 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 → Вставка разрешена!

6. Аномалии бронирования: почему UNIQUE бессилен?

Классическое ограничение UNIQUE (room_id, start_time) запрещает только полное совпадение момента старта, но бессильно против частичного наложения интервалов:

Бронь №1: [10:00 ----------------------- 12:00)

Бронь №2: [11:00 ----------- 13:00) ← Наложение! UNIQUE не сработает!

Вывод: проверка равенства (=) неспособна выразить перекрытие интервалов (&&). Требуется специализированный механизм интервальных ограничений.

7. Интервальные типы: tsrange и daterange

Встроенные диапазоны

  • 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 (конфликт!)

8. Ограничения исключения: EXCLUDE USING gist

Синтаксис ограничения исключения задает пары «столбец + оператор»: для любых двух строк хотя бы одно из условий обязано вернуть 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 &&
    )
);

9. Почему именно GiST-индекс?

B-Tree индекс

  • Ориентирован строго на линейный порядок: <, <=, =, >=, >.
  • Не умеет индексировать многомерные и интервальные пересечения.
  • Идеален для точечного поиска чисел и строк.

GiST (Generalized Search Tree)

  • Обобщенное дерево поиска для геометрических объектов и диапазонов.
  • Эффективно группирует ограничивающие прямоугольники и интервалы (Bounding Boxes).
  • Модуль btree_gist позволяет смешивать скаляры (room_id) и диапазоны.

10. Циклические внешние ключи: парадокс «курицы и яйца»

Проблема взаимно зависимых сущностей в проектировании схемы:

Конфликтная схема

  • Отель содержит Президентский люкс → hotels.suite_id REFERENCES rooms(id).
  • Номер физически находится в Отеле → rooms.hotel_id REFERENCES hotels(id).
  • Оба поля NOT NULL.

Какую таблицу заполнять первой? Ни одну невозможно вставить!

Инженерный выход

Ослабление ограничений до NULL — компромисс, разрушающий целостность.

Профессиональное решение — транзакционная отложенность внешнего ключа (DEFERRABLE): СУБД откладывает проверку связей до команды COMMIT.

11. Режимы работы DEFERRABLE в транзакции

INITIALLY IMMEDIATE

Ограничение может быть отложено, но по умолчанию проверяется после каждой команды DML.

Для откладывания требуется явная команда внутри транзакции:

SET CONSTRAINTS fk_name DEFERRED;

INITIALLY DEFERRED

Ограничение автоматически откладывается до фиксации транзакции (COMMIT).

Идеально для постоянных циклических зависимостей: прикладной код просто выполняет вставки в рамках BEGIN...COMMIT.

12. Эталонный скрипт циклической вставки

-- Создание отложенного внешнего ключа
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;

13. Аудит схемы: выявление взаимных противоречий

Ограничения не должны создавать логических тупиков. Три правила аудитора схемы:

1. Взаимоисключающие условия

CHECK (age > 21 AND age < 18) → пустое множество значений, блокировка вставки.

2. Конфликт FK и диапазона CHECK

FK ссылается на справочник с ID 1..50, а CHECK требует id > 1000 → ни одна строка не свяжется.

3. Конфликт каскадных правил

Поле с NOT NULL и правилом внешнего ключа ON DELETE SET NULL гарантированно вызовет аварийный откат при удалении родительской записи.

14. Архитектурная строгость: цена небрежности в ограничениях

Exploits of a Mom (xkcd #327)
Тематический интернет-мем: «Exploits of a Mom» (культовый комикс про «Little Bobby Tables», xkcd №327).
Автор: Randall Munroe. Первоисточник: xkcd.com/327. Лицензия: Creative Commons Attribution-NonCommercial 2.5 Generic (CC BY-NC 2.5).
Связь с темой занятия: СУБД должна обладать непробиваемой системой внутренней защиты. Ограничения доменов, предикаты CHECK с регулярными выражениями и декларативные ограничения целостности физически не пропускают опасный или искаженный ввод в ядро базы данных, превращая любую попытку внедрения аномалий в предсказуемую синтаксическую ошибку валидации.
1 / 15