TGViewer
SQL Ready | Базы Данных SQL Ready | Базы Данных @sql_ready · 17.5K subscribers
Post #1985 2.67K
Почему CHECK constraint может пропустить неправильные данные из-за NULL!

Редкая, но неприятная ловушка SQL: многие думают, что CHECK constraint требует, чтобы условие всегда было TRUE. Но это не так, CHECK запрещает только FALSE. А результат UNKNOWN — пропускается.

Именно поэтому nullable-колонки внутри CHECK могут вести себя не так, как ожидает разработчик.

Допустим, есть таблица товаров:
products(
id,
price,
discount
)


Хотим запретить скидку больше цены:
ALTER TABLE products
ADD CONSTRAINT chk_discount_price
CHECK (discount <= price);


На первый взгляд всё выглядит правильно.

Теперь такой INSERT действительно не пройдёт:
INSERT INTO products(id, price, discount)
VALUES (1, 100, 150);


Потому что проверка: 150 <= 100 даёт FALSE. А CHECK constraint запрещает строки, где выражение возвращает FALSE. Но дальше начинается важный нюанс SQL и трёхзначной логики.

Вот такой INSERT уже может пройти:
INSERT INTO products(id, price, discount)
VALUES (2, 100, NULL);


И такой тоже:
INSERT INTO products(id, price, discount)
VALUES (3, NULL, 50);


Многие ожидают, что CHECK отклонит такие строки. Но SQL работает иначе, если в сравнении участвует NULL, результатом становится не TRUE и не FALSE, а: UNKNOWN

То есть:
150 <= 100     -- FALSE
NULL <= 100 -- UNKNOWN
50 <= NULL -- UNKNOWN
NULL <= NULL -- UNKNOWN


И вот здесь самая важная мысль: CHECK constraint считает строку валидной, если результат выражения — TRUE или UNKNOWN. Запрещается только явно FALSE.

Это поведение связано с SQL three-valued logic — логикой с тремя состояниями: TRUE, FALSE и UNKNOWN. Именно поэтому CHECK сам по себе НЕ заменяет NOT NULL. Если колонка обязательная — это нужно указывать отдельно.

Правильный вариант:
CREATE TABLE products(
id bigint PRIMARY KEY,
price numeric NOT NULL,
discount numeric NOT NULL,

CONSTRAINT chk_discount_price
CHECK (discount <= price)
);


Теперь NULL уже не сможет пройти, потому что NOT NULL сработает раньше CHECK. Но в реальных системах скидка часто может отсутствовать. То есть NULL — это нормальное состояние: скидки нет.

В таком случае constraint лучше писать явно и читаемо:
ALTER TABLE products
ADD CONSTRAINT chk_discount_price
CHECK (
discount IS NULL
OR discount <= price
);


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

Если price остаётся nullable: price numeric, то выражение: discount <= price снова может вернуть UNKNOWN. Например:
discount = 50
price = NULL


Результат проверки: 50 <= NULL, будет UNKNOWN, а строка снова станет валидной.

Поэтому если цена обязательна — нужен отдельный NOT NULL:
price numeric NOT NULL


Похожая ситуация встречается с датами. Например:
CHECK (end_date >= start_date)


Разработчик может думать, что constraint гарантирует корректный диапазон дат. Но если end_date nullable, такой CHECK спокойно пропускает:
end_date = NULL


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

И это может быть абсолютно нормальным поведением. Например, если NULL означает: период ещё не завершён. Но если обе даты обязательны, это нужно фиксировать явно:
start_date date NOT NULL,
end_date date NOT NULL,

CHECK (end_date >= start_date)


🔥 Вывод: если в CHECK участвуют nullable-поля, constraint может пропускать строки из-за UNKNOWN, CHECK не заменяет NOT NULL. Для обязательных значений всегда нужен отдельный NOT NULL constraint.

➡️ SQL Ready | #практика
  • ❤ 14
  • 👍 8
  • 🔥 5
More from @sql_ready
  1. Oct 6, 2026Проверяйте связи с учётом периода! В PostgreSQL 18 связь может учитывать ещё и период его…
  2. Oct 6, 2026⚡️5 фундаментальных курсов по ИБ по цене одного Это предложение для тех, кто готов войти в…
  3. Oct 6, 2026👍 Устройство PostgreSQL — подробная документация на русском языке! Материалы посвящены не…
  4. Oct 5, 2026COUNT(*) в PostgreSQL: MVCC, visibility map и стоимость выполнения! В PostgreSQL точный CO…
  5. Oct 5, 2026Как оплачивать зарубежные сервисы в 2026 году? Можно бегать между посредниками и бояться б…
  6. Oct 5, 2026Проверяйте JSON без преобразования! Если JSON приходит как text, необязательно делать ::js…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →