Learning Platform
Урок 04.06 · 16 мин
Начальный
CHECK constraintsENUMDOMAINData validationIntegrity

Зачем валидировать на уровне БД

«Это же проверит код на бэкенде, зачем дублировать в базе?» — типичный вопрос. Ответ: код бэкенда — не последняя линия обороны. Базу читают и пишут:

  • Несколько микросервисов, написанных разными командами.
  • Скрипты миграции данных, которые обходят application layer.
  • Аналитики через psql и DBeaver.
  • Бывают и баги — забыли валидацию, пропустили проверку в новом эндпоинте.

CHECK-ограничения — это декларативная валидация, гарантированная самой СУБД. Никакой код её не обойдёт, кроме явного ALTER TABLE ... DROP CONSTRAINT. Это и есть «последняя линия обороны».

CHECK — синтаксис и места применения

CHECK (предикат) можно навесить:

  • На колонку — обычно при CREATE TABLE или ALTER TABLE ADD COLUMN.
  • На таблицу — отдельным CONSTRAINT после колонок, может ссылаться на несколько столбцов.
CREATE TABLE products (
  id          SERIAL PRIMARY KEY,
  sku         TEXT NOT NULL UNIQUE,
  name        TEXT NOT NULL CHECK (length(name) > 0),
  price_cents INT  NOT NULL CHECK (price_cents > 0),
  in_stock    INT  NOT NULL DEFAULT 0 CHECK (in_stock >= 0),
  CONSTRAINT sku_format CHECK (sku ~ '^[A-Z]{2}-[0-9]{3}$')
);

Первые три CHECK — на колонках. Последний — табличный, и в нём

регулярка
требует, чтобы SKU был «две буквы, дефис, три цифры».

Условие может использовать

только IMMUTABLE-функции
— никаких now(), current_user, обращений к другим таблицам. PostgreSQL хочет гарантировать, что условие всегда даёт одинаковый ответ для одной и той же строки.

Когда срабатывает CHECK

На каждом INSERT и UPDATE строки. Если предикат не TRUE — операция откатывается.

INSERTновая строка
CHECKоценить предикат
TRUEстрока вставлена
FALSEошибка, откат
UPDATEизменение строки
CHECKоценить на новой версии
TRUEизменение принято
FALSEошибка, откат
DELETE
CHECKне проверяетсяПри удалении новой версии нет, CHECK нечего проверять

Создаём таблицу с CHECK и пробуем нарушить:

PostgreSQL

Третий INSERT получит ошибку: new row for relation "inventory" violates check constraint "inventory_qty_check". Транзакция откатится.

CHECK и NULL — снова трёхзначная логика

CHECK пропускает строку, если предикат не FALSE. То есть TRUE и NULL оба считаются «прошедшими». Это сделано специально: иначе колонки с пропусками было бы невозможно проектировать.

CHECK (price_cents > 0)

Эта проверка не отвергнет строку с price_cents = NULL. Чтобы запретить и NULL, и неположительные значения, нужно:

price_cents INT NOT NULL CHECK (price_cents > 0)

NOT NULL отвечает за NULL, CHECK — за остальное. Это разные сущности.

CHECK не запрещает NULL — обрати внимание:

PostgreSQL

UPDATE и CHECK: что происходит при «провале»

Если UPDATE нарушает CHECK на одной из строк, вся UPDATE-операция откатывается. Не только эта строка — а вся транзакция, в которой шёл UPDATE.

UPDATE inventory SET qty = qty - 100;  -- если у какой-то строки qty станет < 0, весь UPDATE откатывается

Это сильное гарантирование атомарности. Если хочется «обновить только те строки, для которых это безопасно» — нужно явно фильтровать в WHERE:

UPDATE inventory SET qty = qty - 100 WHERE qty >= 100;

UPDATE падает целиком из-за одной плохой строки:

PostgreSQL

После ошибки таблица не изменилась — это и есть атомарность.

ENUM vs CHECK + TEXT — что выбрать

В PostgreSQL есть отдельный тип ENUM (создаётся через CREATE TYPE ... AS ENUM (...)). Альтернатива — TEXT колонка с CHECK (col IN ('a', 'b', 'c')).

ENUMTEXT + CHECK IN
Расход места4 байта (oid)varlena — несколько байт + длина строки
Скорость сравнениябыстро (целое)медленнее (строки)
Добавление значенияALTER TYPE ADD VALUEALTER TABLE ... DROP/ADD CONSTRAINT
Удаление значенияочень сложноредактировать список в CHECK
ПереименованиеALTER TYPE RENAME VALUEUPDATE данных + CHECK
Сортировкав порядке объявленияалфавитная

Кратко:

ENUM хорош для очень стабильных перечислений
('yes'/'no', 'asc'/'desc'). Для бизнес-перечислений, которые могут эволюционировать, лучше TEXT + CHECK или отдельная справочная таблица с FK.

В нашей схеме orders.status сделан через CHECK — именно по причине гибкости:

status TEXT NOT NULL CHECK (status IN ('pending','paid','shipped','delivered','cancelled','refunded'))

Посмотри, что в нашей таблице orders уже есть CHECK на status:

PostgreSQL

Второй запрос получит ошибку violates check constraint "orders_status_check".

DOMAIN — типы с встроенной валидацией

DOMAIN — это пользовательский тип, привязанный к существующему, но с дополнительными ограничениями (CHECK, NOT NULL, DEFAULT). Полезен, когда одно и то же ограничение нужно применить во многих таблицах.

CREATE DOMAIN email_addr AS TEXT
  CHECK (VALUE ~* '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$');

CREATE TABLE users (
  id    SERIAL PRIMARY KEY,
  email email_addr NOT NULL UNIQUE
);

Теперь email-валидация регуляркой не дублируется в каждой таблице с email-колонкой — она зашита в сам тип. Поменять регулярку — ALTER DOMAIN, и она вступит в силу везде.

Создай DOMAIN и проверь валидацию:

PostgreSQL

DOMAIN используется реже, чем заслуживает, — но в проектах с десятком таблиц на похожих типах он сильно экономит время.

NOT VALID — добавить CHECK без проверки старых данных

В больших таблицах добавление CHECK может занять часы — придётся прогнать предикат по всем строкам. PostgreSQL даёт способ обойти это:

ALTER TABLE huge_table ADD CONSTRAINT no_negatives CHECK (amount >= 0) NOT VALID;

NOT VALID говорит: новые строки и UPDATE-ы проверяй, но старые данные не трогай. Потом, в окно низкой нагрузки, можно сделать ALTER TABLE ... VALIDATE CONSTRAINT ... — это запустит проверку существующих строк.

Это критичная опция для миграций на горячих таблицах. Без NOT VALID миграция может заблокировать таблицу на десятки минут.

Проверка знанийKnowledge check
У тебя в БД CHECK (status IN ('active','inactive')). Появилось требование добавить статус 'archived'. Как изменить CHECK с минимальным даунтаймом?
ОтветAnswer
PostgreSQL не умеет «менять» CHECK on-place. Нужно: (1) ALTER TABLE ADD CONSTRAINT new_status_check CHECK (status IN ('active','inactive','archived')) NOT VALID — добавили новое ограничение без проверки старых строк; (2) ALTER TABLE VALIDATE CONSTRAINT new_status_check — валидация, при необходимости в окне низкой нагрузки; (3) ALTER TABLE DROP CONSTRAINT old_status_check — убрали старое. Альтернатива в одной транзакции: BEGIN; DROP + ADD; COMMIT — но это требует короткой блокировки таблицы. На больших таблицах NOT VALID-путь безопаснее. И отдельно: рассмотри замену CHECK на отдельную справочную таблицу с FK — менять «список разрешённых значений» там становится INSERT'ом.
Constraints как часть модели: domain, key, referential, check Constraints как код модели: CHECK, UNIQUE, NOT NULL, exclusion

Чек-лист

  • CHECK (предикат) — декларативная валидация на уровне БД, обходится только DROP CONSTRAINT.
  • Условие проверяется при INSERT и UPDATE на новой версии строки. При DELETE — нет.
  • CHECK пропускает строки, где предикат TRUE или NULL. Чтобы запретить NULL — нужен NOT NULL.
  • Если UPDATE нарушает CHECK хоть на одной строке — вся операция откатывается.
  • ENUM хорош для стабильных перечислений; TEXT + CHECK IN (...) — для эволюционирующих.
  • DOMAIN — переиспользуемый тип с встроенным CHECK. Полезен в проектах с повторяющимися правилами.
  • ALTER TABLE ADD CONSTRAINT ... NOT VALID — добавить CHECK без блокировки старых данных. Потом VALIDATE в окно низкой нагрузки.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Колонка `qty INT CHECK (qty > 0)` (без NOT NULL). Какие из значений пройдут проверку?

Закончили урок?

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

Войдите чтобы оценить урок

Прогресс модуля
0 из 6