Ограничения целостности

Рейтинг: 70.2% · 15 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Ограничения целостности

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности (вы здесь)
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 18. Ограничения целостности

База данных postgresql хранит не просто набор строк, а модель реального мира, и у этой модели есть правила: у заказа должен быть покупатель, цена не бывает отрицательной, два билета не получают одно и то же место. Ограничения целостности (constraints) - это способ записать такие правила прямо в схему таблицы, чтобы сама СУБД отказывалась принимать данные, которые их нарушают. В этом уроке разберём шесть видов ограничений (NOT NULL, CHECK, UNIQUE, PRIMARY KEY, FOREIGN KEY, EXCLUDE), научимся управлять каскадами внешних ключей и поймём, зачем нужны отложенные (DEFERRABLE) проверки.

Изображение

Как это работает

Главная идея урока такая: правило, защищённое на уровне БД, нельзя обойти. Приложение можно переписать, можно зайти руками через psql, можно подключить второй сервис на другом языке - но если в таблице стоит CHECK (amount > 0), отрицательная сумма туда не попадёт никогда. Проверка в коде приложения - это вежливая просьба, а ограничение в схеме - это закон. Поэтому ключевые инварианты данных держат именно в базе.

NOT NULL запрещает в столбце пустое значение. Это самое дешёвое ограничение: признак хранится прямо в описании столбца. CHECK задаёт логическое условие на одну строку - выражение должно давать истину или NULL (NULL считается прошедшим проверкой, это частая ловушка). UNIQUE требует, чтобы значения в столбце (или наборе столбцов) не повторялись. PRIMARY KEY - это фактически UNIQUE плюс NOT NULL, и в таблице он может быть только один; он объявляет, что именно эта колонка опознаёт строку.

FOREIGN KEY связывает две таблицы: значение в дочерней таблице обязано существовать в родительской. Это и есть ссылочная целостность. Поведение при удалении или изменении родительской строки настраивается: RESTRICT и NO ACTION запрещают операцию, если есть зависимые строки; CASCADE удаляет или меняет их следом; SET NULL и SET DEFAULT подставляют в дочерний столбец NULL или значение по умолчанию.

EXCLUDE - самое мощное и редко используемое ограничение. Оно похоже на UNIQUE, но сравнивает строки не только на равенство. Можно потребовать, чтобы никакие две строки не конфликтовали по заданным операторам. Классика - бронирование: запретить пересечение интервалов времени для одной переговорки. Работает поверх индекса GiST.

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

SQL и примеры

Базовый набор ограничений в одной таблице. Обратите внимание на современный GENERATED IDENTITY вместо устаревшего serial.

Код: Выделить всё

CREATE TABLE orders (
    order_id  bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email     text   NOT NULL,
    amount    numeric(10,2) NOT NULL CHECK (amount > 0),
    status    text   NOT NULL DEFAULT 'new'
              CHECK (status IN ('new','paid','shipped','cancelled')),
    CONSTRAINT email_lower CHECK (email = lower(email))
);
Здесь PRIMARY KEY опознаёт строку, два CHECK ограничивают сумму и набор статусов, а именованный CONSTRAINT email_lower следит за форматом. Имена ограничениям стоит давать самим - так понятнее в тексте ошибки.

Посмотрим на демобазу "Авиаперевозки" (схема bookings). Внешние ключи там связывают перелёты с аэропортами. Запрос показывает, как ссылочная целостность отражается в данных - каждый рейс ссылается на существующие аэропорты:

Код: Выделить всё

SELECT f.flight_no,
       dep.airport_name AS departure,
       arr.airport_name AS arrival
FROM   bookings.flights f
JOIN   bookings.airports dep ON dep.airport_code = f.departure_airport
JOIN   bookings.airports arr ON arr.airport_code = f.arrival_airport
WHERE  f.flight_no = 'PG0405'
LIMIT 5;
JOIN тут отрабатывает гарантированно, потому что departure_airport и arrival_airport - внешние ключи на airports. Невозможно вставить рейс в несуществующий аэропорт.

Внешний ключ с каскадом. Допустим, заводим позиции заказа:

Код: Выделить всё

CREATE TABLE order_items (
    order_id bigint NOT NULL
        REFERENCES orders(order_id) ON DELETE CASCADE,
    sku      text   NOT NULL,
    qty      int    NOT NULL CHECK (qty > 0),
    PRIMARY KEY (order_id, sku)
);
ON DELETE CASCADE означает: удалили заказ - его позиции уезжают автоматически. Если бы стоял RESTRICT, база не дала бы удалить заказ, пока есть позиции.

EXCLUDE против пересечения интервалов. Нужен btree_gist, чтобы сравнивать обычный int оператором равенства внутри GiST:

Код: Выделить всё

CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE room_booking (
    room   int       NOT NULL,
    during tsrange   NOT NULL,
    EXCLUDE USING gist (room WITH =, during WITH &&)
);
Правило читается так: запрещены две строки, где room равны (=) И интервалы пересекаются (&&). Двойное бронирование одной переговорки на накладывающееся время вставить не получится.

UNIQUE и NULL. По умолчанию UNIQUE считает каждый NULL уникальным, поэтому пустых значений может быть много. В PostgreSQL 15 появилась опция, меняющая это поведение:

Код: Выделить всё

CREATE TABLE accounts (
    inn text UNIQUE NULLS NOT DISTINCT
);
Теперь два NULL считаются одинаковыми, и второй пустой ИНН не пройдёт. Это удобно для импорта данных из 1С и других систем, где пустота должна быть одна.

Добавление ограничения без долгой блокировки. На большой таблице проверять весь объём данных дорого. Двухфазный приём - сначала NOT VALID, потом отдельная валидация:

Код: Выделить всё

ALTER TABLE orders
  ADD CONSTRAINT amount_pos CHECK (amount > 0) NOT VALID;

ALTER TABLE orders VALIDATE CONSTRAINT amount_pos;
NOT VALID применяет правило к новым строкам сразу, а проверку старых откладывает; VALIDATE CONSTRAINT берёт более слабую блокировку и не мешает чтению. Это работает для CHECK и FOREIGN KEY.

Частые грабли
  • CHECK пропускает NULL. Условие CHECK (price > 0) истинно для NULL, потому что выражение даёт NULL, а это не FALSE. Хотите запретить пустоту - ставьте ещё и NOT NULL.
  • UNIQUE и куча NULL. По стандарту много NULL в уникальном столбце - это норма. Если нужно одно пустое значение, в PG15 берите NULLS NOT DISTINCT.
  • Внешний ключ без индекса на дочерней стороне. PostgreSQL индексирует только родительский столбец автоматически. При ON DELETE CASCADE удаление родителя будет делать seq scan по дочерней таблице - создавайте индекс на FK-колонке руками.
  • NO ACTION против RESTRICT. NO ACTION можно отложить через DEFERRABLE, а RESTRICT срабатывает немедленно и отложить его нельзя. Это не синонимы.
  • CHECK не умеет подзапросы. Внутри CHECK нельзя обращаться к другим строкам или таблицам - только к текущей строке. Межтабличные правила - это FOREIGN KEY, EXCLUDE или триггеры.
  • EXCLUDE без btree_gist. Для оператора = на обычных типах (int, text) нужен btree_gist, иначе GiST не знает такого класса операторов.
  • Откладывать можно не всё. DEFERRABLE поддерживают UNIQUE, PRIMARY KEY, FOREIGN KEY и EXCLUDE. NOT NULL и CHECK откладывать нельзя - они проверяются построчно.
Мини-лаба
  • Создайте таблицу authors (author_id GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL).
  • Создайте books с внешним ключом author_id REFERENCES authors ON DELETE SET NULL и ограничением CHECK (pages > 0).
  • Вставьте автора и пару книг, затем удалите автора и проверьте SELECT - что стало с author_id в книгах.
  • Добавьте к books ограничение UNIQUE (title) и попробуйте вставить дубль названия. Прочитайте текст ошибки.
  • Объявьте отложенный FK: пересоздайте связь как DEFERRABLE INITIALLY DEFERRED и внутри одной транзакции вставьте книгу раньше автора, потом автора, потом COMMIT.
  • Через btree_gist и EXCLUDE сделайте таблицу расписания аудиторий, запретите пересечение занятий в одной аудитории и проверьте конфликт.
  • Добавьте CHECK NOT VALID на существующие данные и отдельно выполните VALIDATE CONSTRAINT.
Контрольные вопросы
  • Почему ограничение в схеме надёжнее проверки в коде приложения, и какой инвариант вы бы точно вынесли в базу?
  • Чем PRIMARY KEY отличается от UNIQUE, и почему PK в таблице один, а UNIQUE может быть несколько?
  • Как поведут себя дочерние строки при ON DELETE CASCADE, SET NULL и RESTRICT?
  • Почему CHECK (x > 0) не ловит NULL и как это исправить?
  • В каких случаях нужно DEFERRABLE INITIALLY DEFERRED и какие ограничения нельзя откладывать?
  • Зачем для EXCLUDE с оператором равенства на int подключают расширение btree_gist?
👍 ❤️3 🔥1 😄 🤔1
Аватара пользователя
lover_max
Сообщения: 1
Зарегистрирован: 29 май 2026, 04:36

Re: Ограничения целостности

Сообщение lover_max »

А если внешний ключ ставлю ON DELETE SET NULL, а столбец при этом NOT NULL - что победит? По логике конфликт же
👍1 ❤️1 🔥1 😄 🤔1
Аватара пользователя
thoppa
Сообщения: 1
Зарегистрирован: 30 май 2026, 11:41

Re: Ограничения целостности

Сообщение thoppa »

Подтверждаю про индекс на FK: у нас на проде каскадное удаление родителя в таблице на 30 млн строк висело минутами, добавили индекс на дочернюю колонку - стало мгновенно
👍 ❤️1 🔥 😄 🤔1
Ответить
← Предыдущая глава
DDL: CREATE TABLE, схемы, ALTER
Следующая глава →
Представления и материализованные представления

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость