sqlpostgresqlconstraintscheck

SQL CHECK Constraints: Enforcing Invariants in the Database

Use CHECK to lock business rules into the schema: positive amounts, allowed statuses, date ranges, and multi-column validation.

11 min lugemistReferencesql · postgresql · constraints · check · data-integrity
See artikkel on praegu venekeelne — ingliskeelne tõlge on töös.

CHECK — это ограничение, которое заставляет базу проверять условие перед вставкой или обновлением строки.

Если условие истинно, строка записывается.

Если условие ложно, PostgreSQL отклоняет операцию с ошибкой.

Например:

CREATE TABLE orders (
  id         bigint PRIMARY KEY,
  user_id    bigint NOT NULL,
  amount     numeric(12, 2) NOT NULL CHECK (amount > 0),
  status     text NOT NULL CHECK (status IN ('new', 'paid', 'shipped', 'cancelled')),
  created_at timestamptz NOT NULL DEFAULT now()
);

Здесь есть два правила:

amount должен быть больше 0
status должен быть одним из разрешённых значений

Если попробовать вставить заказ с нулевой суммой:

INSERT INTO orders (id, user_id, amount, status)
VALUES (1, 10, 0, 'new');

PostgreSQL не даст записать такую строку.

Если попробовать вставить неизвестный статус:

INSERT INTO orders (id, user_id, amount, status)
VALUES (2, 10, 100, 'refunded');

PostgreSQL тоже отклонит операцию, потому что 'refunded' не входит в разрешённый список.

Главная идея:

CHECK переносит важное бизнес-правило из приложения в схему базы данных.

Зачем нужны CHECK-ограничения

Можно спросить:

А почему просто не проверять это в приложении?

Проверять в приложении тоже нужно. Но приложение — не единственный путь записи данных.

Данные могут попасть в базу через:

  • другой сервис;
  • админку;
  • миграцию;
  • SQL-скрипт;
  • импорт;
  • ETL-процесс;
  • ручной INSERT;
  • ручной UPDATE;
  • фоновую задачу;
  • временный сервис, который потом забыли удалить.

Если правило живёт только в коде одного приложения, его легко обойти.

Если правило закреплено в базе, некорректная строка не запишется независимо от того, кто отправил запрос.

Например, сумма заказа не должна быть отрицательной.

Можно написать проверку в API, но кто-то потом выполнит:

UPDATE orders
SET amount = -100
WHERE id = 1;

Если в базе нет CHECK, запрос пройдёт.

Если есть:

CHECK (amount > 0)

база остановит ошибку.

CHECK как инвариант

CHECK хорошо подходит для инвариантов.

Инвариант — это правило, которое всегда должно быть правдой для строки.

Например:

цена товара больше 0
зарплата не отрицательная
дата окончания позже даты начала
скидка от 0 до 100
статус входит в разрешённый список
пользователь не может быть менеджером сам себе

Такие правила лучше хранить как можно ближе к данным.

Пример:

CREATE TABLE products (
  id       bigint PRIMARY KEY,
  name     text NOT NULL,
  price    numeric(12, 2) NOT NULL,
  discount numeric(5, 2) NOT NULL DEFAULT 0,

  CHECK (price > 0),
  CHECK (discount >= 0 AND discount <= 100)
);

Теперь база гарантирует:

price всегда больше 0
discount всегда от 0 до 100

Базовый синтаксис CHECK

CHECK можно указать прямо при создании таблицы.

CREATE TABLE employees (
  id     bigint PRIMARY KEY,
  name   text NOT NULL,
  salary numeric NOT NULL CHECK (salary > 0)
);

Здесь ограничение относится к колонке salary.

Можно записать его отдельно на уровне таблицы:

CREATE TABLE employees (
  id     bigint PRIMARY KEY,
  name   text NOT NULL,
  salary numeric NOT NULL,

  CHECK (salary > 0)
);

Для одной колонки оба варианта похожи.

Но если условие использует несколько колонок, его обычно пишут на уровне таблицы.

Например:

CREATE TABLE subscriptions (
  id         bigint PRIMARY KEY,
  user_id    bigint NOT NULL,
  starts_at  date NOT NULL,
  ends_at    date NOT NULL,

  CHECK (ends_at > starts_at)
);

Здесь условие проверяет сразу две колонки:

ends_at должен быть позже starts_at

Именованные ограничения

Если не дать ограничению имя, PostgreSQL сгенерирует его сам.

Например:

orders_amount_check

Иногда автогенерированного имени достаточно, но в реальных проектах лучше задавать имя явно.

CREATE TABLE orders (
  id     bigint PRIMARY KEY,
  amount numeric(12, 2) NOT NULL,

  CONSTRAINT orders_amount_positive CHECK (amount > 0)
);

Или через ALTER TABLE:

ALTER TABLE products
ADD CONSTRAINT products_price_positive CHECK (price > 0);

Почему имя полезно?

Потому что оно:

  • видно в ошибке;
  • понятно в логах;
  • удобно при ревью схемы;
  • нужно для удаления ограничения;
  • нужно для изменения правила через миграцию.

Например, удалить ограничение можно так:

ALTER TABLE products
DROP CONSTRAINT products_price_positive;

Если имя было автогенерированным, его сначала придётся искать.

Хорошие имена для CHECK

Имя ограничения должно объяснять смысл правила.

Хорошо:

orders_amount_positive
products_price_positive
employees_salary_non_negative
subscriptions_dates_valid
users_country_allowed

Плохо:

check1
constraint_123
valid_data
test_check

Хорошее имя помогает быстро понять, почему вставка или обновление упали.

Например, ошибка с ограничением:

orders_amount_positive

сразу говорит: проблема в положительности суммы заказа.

Добавить CHECK к существующей таблице

Ограничение можно добавить к уже существующей таблице.

Например, есть таблица products, и нужно запретить цену меньше или равную нулю.

ALTER TABLE products
ADD CONSTRAINT products_price_positive CHECK (price > 0);

После этого PostgreSQL будет проверять правило при новых вставках и обновлениях.

Но важно: при обычном ADD CONSTRAINT база также проверит уже существующие строки.

Если в таблице уже есть товар с price = 0, добавление ограничения упадёт.

Сначала нужно найти плохие данные:

SELECT id, name, price
FROM products
WHERE price <= 0;

Потом исправить:

UPDATE products
SET price = 1
WHERE price <= 0;

И только потом добавить CHECK.

CHECK со списком допустимых значений

Частый пример — ограничить статус списком значений.

CREATE TABLE orders (
  id     bigint PRIMARY KEY,
  status text NOT NULL,

  CONSTRAINT orders_status_allowed
    CHECK (status IN ('new', 'paid', 'shipped', 'cancelled'))
);

Теперь нельзя записать статус:

unknown
refunded
done
processing

если его нет в списке.

Плюс такого подхода — простота.

Минус — если статусов много или они часто меняются, лучше подумать о другой модели:

  • отдельная таблица справочника;
  • foreign key;
  • enum-тип PostgreSQL;
  • домен;
  • бизнес-логика на уровне приложения плюс справочник.

CHECK status IN (...) хорош для небольших и стабильных наборов.

Например:

gender: male/female/other
currency: USD/EUR/GBP
status: new/paid/cancelled

Но если список управляется пользователями или админкой, лучше справочник.

CHECK для диапазонов

CHECK отлично подходит для диапазонов.

Например, скидка от 0 до 100:

ALTER TABLE products
ADD CONSTRAINT products_discount_range
CHECK (discount >= 0 AND discount <= 100);

Или через BETWEEN:

ALTER TABLE products
ADD CONSTRAINT products_discount_range
CHECK (discount BETWEEN 0 AND 100);

Для зарплаты:

ALTER TABLE employees
ADD CONSTRAINT employees_salary_band
CHECK (salary BETWEEN 30000 AND 500000);

Для рейтинга:

ALTER TABLE reviews
ADD CONSTRAINT reviews_rating_range
CHECK (rating BETWEEN 1 AND 5);

Важно: BETWEEN включает обе границы.

rating BETWEEN 1 AND 5

означает:

rating >= 1 AND rating <= 5

CHECK для нескольких колонок

CHECK может сравнивать колонки одной строки между собой.

Например, дата окончания должна быть позже даты начала:

CREATE TABLE bookings (
  id         bigint PRIMARY KEY,
  room_id    bigint NOT NULL,
  starts_at  timestamptz NOT NULL,
  ends_at    timestamptz NOT NULL,

  CONSTRAINT bookings_dates_valid
    CHECK (ends_at > starts_at)
);

Теперь такая строка не пройдёт:

INSERT INTO bookings (id, room_id, starts_at, ends_at)
VALUES (
  1,
  10,
  '2026-01-10 12:00',
  '2026-01-10 10:00'
);

Потому что окончание раньше начала.

Другой пример: пользователь не может быть менеджером сам себе.

ALTER TABLE employees
ADD CONSTRAINT employees_not_self_manager
CHECK (manager_id IS NULL OR manager_id <> id);

Здесь условие означает:

manager_id может быть NULL
или manager_id должен отличаться от id сотрудника

CHECK видит только одну строку

Очень важное ограничение:

CHECK проверяет только текущую строку.

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

Например, CHECK хорошо проверяет:

amount > 0
ends_at > starts_at
discount BETWEEN 0 AND 100
manager_id <> id

Потому что все нужные данные лежат в той же строке.

Но CHECK не подходит для правил:

у пользователя не больше 3 активных подписок
сумма всех заказов пользователя не больше лимита
booking не пересекается с другими booking
manager_id должен существовать в employees
email должен быть уникальным

Для таких правил нужны другие инструменты:

  • UNIQUE;
  • FOREIGN KEY;
  • exclusion constraints;
  • триггеры;
  • транзакции и блокировки;
  • отдельная бизнес-логика;
  • нормальная модель данных.

Например, чтобы проверить, что manager_id существует в employees, нужен внешний ключ, а не CHECK.

ALTER TABLE employees
ADD CONSTRAINT employees_manager_fk
FOREIGN KEY (manager_id) REFERENCES employees(id);

А CHECK (manager_id <> id) может только запретить ссылку на самого себя.

CHECK и NULL

Это главная ловушка.

CHECK блокирует только условие, которое вернуло FALSE.

Если условие вернуло TRUE, строка проходит.

Если условие вернуло NULL, строка тоже проходит.

Почему?

Потому что SQL использует трёхзначную логику:

TRUE
FALSE
UNKNOWN / NULL

Для CHECK нарушением считается только FALSE.

Пример:

CREATE TABLE users (
  id      bigint PRIMARY KEY,
  email   text NOT NULL,
  country text,

  CONSTRAINT users_country_allowed
    CHECK (country IN ('US', 'GB', 'DE', 'BR'))
);

Кажется, что country теперь обязан быть одним из четырёх значений.

Но это не совсем так.

Такой INSERT пройдёт:

INSERT INTO users (id, email, country)
VALUES (1, 'a@example.com', NULL);

Почему?

Потому что выражение:

country IN ('US', 'GB', 'DE', 'BR')

при country = NULL возвращает не FALSE, а NULL.

А CHECK пропускает NULL.

Как запретить NULL

Если колонка обязательна, лучше добавить NOT NULL.

CREATE TABLE users (
  id      bigint PRIMARY KEY,
  email   text NOT NULL,
  country text NOT NULL,

  CONSTRAINT users_country_allowed
    CHECK (country IN ('US', 'GB', 'DE', 'BR'))
);

Теперь NULL не пройдёт из-за NOT NULL.

Можно также встроить проверку прямо в CHECK:

ALTER TABLE users
ADD CONSTRAINT users_country_required_allowed
CHECK (
  country IS NOT NULL
  AND country IN ('US', 'GB', 'DE', 'BR')
);

Но обычно понятнее разделять:

NOT NULL отвечает за обязательность
CHECK отвечает за допустимые значения

То есть лучше:

country text NOT NULL,
CHECK (country IN ('US', 'GB', 'DE', 'BR'))

CHECK и пустая строка

NULL и пустая строка — разные вещи.

Например:

NULL  -- значения нет
''    -- значение есть, но это пустой текст

Если нужно запретить пустые строки, используйте CHECK.

CREATE TABLE users (
  id    bigint PRIMARY KEY,
  email text NOT NULL,

  CONSTRAINT users_email_not_blank
    CHECK (trim(email) <> '')
);

Теперь такой email не пройдёт:

INSERT INTO users (id, email)
VALUES (1, '');

И такой тоже:

INSERT INTO users (id, email)
VALUES (2, '   ');

потому что trim(email) уберёт пробелы по краям.

Но если колонка допускает NULL, то trim(NULL) <> '' даст NULL, а CHECK это пропустит.

Поэтому для обязательного непустого email лучше:

email text NOT NULL,
CONSTRAINT users_email_not_blank CHECK (trim(email) <> '')

CHECK и now()

Иногда хочется написать:

ALTER TABLE users
ADD CONSTRAINT users_created_not_future
CHECK (created_at <= now());

Технически такие выражения могут выглядеть удобно, но с ними нужно быть осторожным.

CHECK проверяется только в момент вставки или обновления строки.

Он не перепроверяется каждую секунду сам по себе.

Например, правило с текущим временем зависит от момента проверки. Сегодня оно может быть истинным, завтра — уже иметь другой смысл.

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

Лучше использовать CHECK для таких условий:

CHECK (amount > 0)
CHECK (ends_at > starts_at)
CHECK (discount BETWEEN 0 AND 100)

А для правил, завязанных на текущее время или сложную бизнес-логику, часто лучше подходят:

  • DEFAULT now();
  • триггеры;
  • валидация в приложении;
  • периодические проверки;
  • отдельные задачи контроля данных.

Например, если created_at должен автоматически проставляться при создании, обычно достаточно:

created_at timestamptz NOT NULL DEFAULT now()

А не CHECK (created_at <= now()).

CHECK и функции

В CHECK можно использовать выражения и функции.

Например:

ALTER TABLE users
ADD CONSTRAINT users_email_lowercase
CHECK (email = lower(email));

Это запретит email с заглавными буквами.

Или:

ALTER TABLE users
ADD CONSTRAINT users_email_not_blank
CHECK (length(trim(email)) > 0);

Но важно: функция в CHECK должна быть предсказуемой для одной и той же строки.

Если функция меняет результат со временем или зависит от внешних данных, это плохой кандидат для CHECK.

Например, не стоит использовать CHECK как способ обращаться к другой таблице через функцию. Это ломает идею локального инварианта строки.

CHECK при INSERT и UPDATE

CHECK проверяется при вставке строки.

INSERT INTO products (id, name, price)
VALUES (1, 'Keyboard', 100);

Если price > 0, вставка пройдёт.

Если price <= 0, вставка упадёт.

CHECK также проверяется при обновлении.

UPDATE products
SET price = -10
WHERE id = 1;

Если есть ограничение:

CHECK (price > 0)

PostgreSQL не даст выполнить такой UPDATE.

Это важно: ограничение защищает данные не только при создании строки, но и при изменении.

Добавление CHECK на большую таблицу

Если таблица большая, обычное добавление ограничения может быть тяжёлым.

Например:

ALTER TABLE orders
ADD CONSTRAINT orders_amount_positive CHECK (amount > 0);

PostgreSQL должен проверить уже существующие строки.

На миллионах строк это может занять время.

Для больших таблиц в PostgreSQL есть полезный приём:

NOT VALID

Пример:

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

Что это значит:

  • ограничение добавляется в схему;
  • новые строки уже будут проверяться;
  • обновляемые строки тоже будут проверяться;
  • старые строки сразу не проверяются.

Потом можно отдельно запустить валидацию:

ALTER TABLE orders
VALIDATE CONSTRAINT orders_amount_positive;

Это удобно для продакшена, где нельзя надолго блокировать большую таблицу.

Как выкатывать CHECK через NOT VALID

Обычно процесс такой.

Сначала ищем плохие строки:

SELECT id, amount
FROM orders
WHERE amount <= 0;

Потом исправляем данные:

UPDATE orders
SET amount = 1
WHERE amount <= 0;

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

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

Новые данные уже защищены.

Потом, когда можно безопасно проверить старые строки, запускаем:

ALTER TABLE orders
VALIDATE CONSTRAINT orders_amount_positive;

После успешной валидации ограничение становится полностью валидным.

Главная идея:

NOT VALID помогает безопаснее добавить правило на большую существующую таблицу.

CHECK и домены

Если одно и то же правило повторяется во многих таблицах, можно подумать о domain.

Например, положительная сумма:

CREATE DOMAIN positive_amount AS numeric(12, 2)
CHECK (VALUE > 0);

Теперь можно использовать тип:

CREATE TABLE orders (
  id     bigint PRIMARY KEY,
  amount positive_amount NOT NULL
);

Домен хранит правило на уровне типа.

Это удобно, если один и тот же инвариант используется много раз.

Например:

  • положительная сумма;
  • процент от 0 до 100;
  • непустая строка;
  • код страны;
  • рейтинг от 1 до 5.

Но домены тоже нужно использовать осознанно: они добавляют слой абстракции, который команда должна понимать.

CHECK против ENUM

Для статусов иногда выбирают между CHECK и ENUM.

Вариант через CHECK:

CREATE TABLE orders (
  id     bigint PRIMARY KEY,
  status text NOT NULL,
  CONSTRAINT orders_status_allowed
    CHECK (status IN ('new', 'paid', 'shipped', 'cancelled'))
);

Вариант через ENUM:

CREATE TYPE order_status AS ENUM (
  'new',
  'paid',
  'shipped',
  'cancelled'
);

CREATE TABLE orders (
  id     bigint PRIMARY KEY,
  status order_status NOT NULL
);

Что выбрать?

CHECK проще менять через миграцию ограничения, но список хранится как текстовое условие.

ENUM делает тип более строгим, но изменение enum-значений может быть менее гибким, особенно если нужно удалять или переименовывать значения.

Для учебных и простых задач CHECK status IN (...) отлично подходит.

Для устойчивой доменной модели можно рассмотреть ENUM или справочник с foreign key.

CHECK против FOREIGN KEY

CHECK не заменяет внешний ключ.

Плохо:

CHECK (user_id > 0)

Это проверяет только то, что user_id положительный.

Но не проверяет, что такой пользователь существует.

Для существования связанной строки нужен FOREIGN KEY.

CREATE TABLE orders (
  id      bigint PRIMARY KEY,
  user_id bigint NOT NULL REFERENCES users(id),
  amount  numeric NOT NULL CHECK (amount > 0)
);

Здесь:

REFERENCES users(id)

проверяет существование пользователя.

А:

CHECK (amount > 0)

проверяет корректность суммы заказа.

У каждого инструмента своя задача.

CHECK против UNIQUE

CHECK не проверяет уникальность.

Например, нельзя нормально выразить правилом CHECK, что email должен быть уникальным.

Для этого нужен UNIQUE.

CREATE TABLE users (
  id    bigint PRIMARY KEY,
  email text NOT NULL UNIQUE
);

CHECK отвечает за локальное условие строки.

UNIQUE отвечает за уникальность среди строк таблицы.

CHECK против триггера

Триггер мощнее CHECK.

Он может:

  • смотреть в другие таблицы;
  • выполнять сложную логику;
  • менять данные;
  • писать аудит;
  • проверять правила, которые не выразить простым условием.

Но триггеры сложнее читать, тестировать и сопровождать.

Если правило можно выразить через простой CHECK, лучше использовать CHECK.

Например:

CHECK (amount > 0)

лучше, чем триггер, который проверяет положительность суммы.

Правило:

Простые инварианты строки — через CHECK. Сложная межстрочная логика — через другие механизмы.

CHECK и ошибки

Когда CHECK нарушается, PostgreSQL возвращает ошибку с именем ограничения.

Например:

INSERT INTO products (id, name, price)
VALUES (1, 'Keyboard', -10);

Если есть ограничение:

CONSTRAINT products_price_positive CHECK (price > 0)

ошибка будет указывать на products_price_positive.

Именно поэтому хорошие имена ограничений важны.

По имени сразу понятно, какое правило нарушено.

MySQL

В MySQL CHECK начал реально применяться с версии 8.0.16.

В старых версиях MySQL синтаксис CHECK мог приниматься, но фактически игнорировался.

Это очень важная историческая ловушка.

Например, на старой версии можно было написать:

CREATE TABLE products (
  id    bigint PRIMARY KEY,
  price numeric(12, 2),
  CHECK (price > 0)
);

и думать, что база защищает цену.

Но старый MySQL мог просто не проверять это правило.

В современных версиях MySQL CHECK работает, но синтаксис, возможности и нюансы могут отличаться от PostgreSQL.

Также в MySQL нет PostgreSQL-механики:

NOT VALID
VALIDATE CONSTRAINT

Поэтому при переносе миграций между PostgreSQL и MySQL такие места нужно перепроверять отдельно.

ClickHouse

В ClickHouse тоже есть CONSTRAINT ... CHECK.

Например:

CREATE TABLE orders
(
  id UInt64,
  amount Decimal(12, 2),
  CONSTRAINT amount_positive CHECK amount > 0
)
ENGINE = MergeTree
ORDER BY id;

Но ClickHouse — аналитическая колоночная СУБД, а не классическая OLTP-база.

Ограничения там стоит воспринимать иначе, чем в PostgreSQL.

В PostgreSQL CHECK часто используют как строгий инвариант бизнес-данных.

В ClickHouse такие проверки чаще относятся к контролю качества вставляемых аналитических данных, а не к полноценной транзакционной модели с постоянными обновлениями строк.

При переносе логики из PostgreSQL в ClickHouse важно учитывать другую модель хранения и записи данных.

Практические шаблоны

Положительная сумма заказа

CREATE TABLE orders (
  id     bigint PRIMARY KEY,
  amount numeric(12, 2) NOT NULL,
  CONSTRAINT orders_amount_positive CHECK (amount > 0)
);

Статус из списка

CREATE TABLE orders (
  id     bigint PRIMARY KEY,
  status text NOT NULL,
  CONSTRAINT orders_status_allowed
    CHECK (status IN ('new', 'paid', 'shipped', 'cancelled'))
);

Скидка от 0 до 100

ALTER TABLE products
ADD CONSTRAINT products_discount_range
CHECK (discount BETWEEN 0 AND 100);

Дата окончания позже даты начала

ALTER TABLE bookings
ADD CONSTRAINT bookings_dates_valid
CHECK (ends_at > starts_at);

Пользователь не менеджер сам себе

ALTER TABLE employees
ADD CONSTRAINT employees_not_self_manager
CHECK (manager_id IS NULL OR manager_id <> id);

Непустой email

ALTER TABLE users
ADD CONSTRAINT users_email_not_blank
CHECK (trim(email) <> '');

При этом сама колонка тоже должна быть NOT NULL, если NULL запрещён.

Обязательная страна из списка

ALTER TABLE users
ALTER COLUMN country SET NOT NULL;

ALTER TABLE users
ADD CONSTRAINT users_country_allowed
CHECK (country IN ('US', 'GB', 'DE', 'BR'));

Добавить CHECK безопаснее на большой таблице

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

Потом:

ALTER TABLE orders
VALIDATE CONSTRAINT orders_amount_positive;

Удалить CHECK

ALTER TABLE orders
DROP CONSTRAINT orders_amount_positive;

Изменить CHECK

В PostgreSQL обычно правило меняют так:

ALTER TABLE orders
DROP CONSTRAINT orders_status_allowed;

ALTER TABLE orders
ADD CONSTRAINT orders_status_allowed
CHECK (status IN ('new', 'paid', 'shipped', 'cancelled', 'refunded'));

То есть старое ограничение удаляют, новое добавляют.

Частые ошибки

Думают, что CHECK запрещает NULL

Такое ограничение:

CHECK (country IN ('US', 'GB', 'DE'))

не запрещает country = NULL.

Если NULL запрещён, добавьте:

country text NOT NULL

или явно:

CHECK (country IS NOT NULL AND country IN ('US', 'GB', 'DE'))

Пытаются CHECK заменить foreign key

Плохо:

CHECK (user_id > 0)

если нужно проверить, что пользователь существует.

Правильно:

FOREIGN KEY (user_id) REFERENCES users(id)

Пытаются CHECK заменить UNIQUE

Плохо пытаться через CHECK проверять уникальность email.

Правильно:

email text NOT NULL UNIQUE

Используют CHECK для межстрочной логики

Например:

у пользователя должно быть не больше 3 активных подписок

Это не задача для обычного CHECK, потому что нужно смотреть другие строки.

Забывают дать имя ограничению

Плохо:

CHECK (price > 0)

Лучше:

CONSTRAINT products_price_positive CHECK (price > 0)

Используют volatile-логику

Правила, завязанные на текущее время, внешние таблицы или меняющееся окружение, плохо подходят для CHECK.

CHECK лучше использовать для стабильных условий одной строки.

Что важно запомнить

CHECK — это ограничение, которое проверяет условие при вставке или обновлении строки.

Пример:

CREATE TABLE orders (
  id     bigint PRIMARY KEY,
  amount numeric(12, 2) NOT NULL,
  CONSTRAINT orders_amount_positive CHECK (amount > 0)
);

Главные правила:

  • CHECK закрепляет инвариант на уровне базы;
  • если условие возвращает FALSE, строка не записывается;
  • если условие возвращает TRUE, строка проходит;
  • если условие возвращает NULL, строка тоже проходит;
  • чтобы запретить NULL, используйте NOT NULL;
  • CHECK может ссылаться на несколько колонок одной строки;
  • CHECK не должен заменять FOREIGN KEY, UNIQUE или триггеры;
  • именованные ограничения удобнее сопровождать;
  • для больших таблиц в PostgreSQL полезен NOT VALID;
  • для изменения правила обычно удаляют старое ограничение и добавляют новое;
  • в MySQL старые версии могли игнорировать CHECK;
  • в ClickHouse семантика и сценарии применения отличаются от PostgreSQL.

Короткий вывод

CHECK нужен, когда вы хотите гарантировать простое правило для каждой строки.

Например:

CHECK (amount > 0)
CHECK (discount BETWEEN 0 AND 100)
CHECK (ends_at > starts_at)
CHECK (status IN ('new', 'paid', 'cancelled'))

Главная мысль:

CHECK защищает данные там, где они живут, — в базе данных.

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

Используйте CHECK для простых локальных правил одной строки, давайте ограничениям понятные имена, помните про NULL и не пытайтесь решать через CHECK задачи, для которых нужны внешние ключи, уникальные ограничения или триггеры.

Harjuta päris ülesannetel

Lahenda ülesandeid SQL-treeneris kohese hindamise ja vihjetega.

Ava treener