CHECK и UNIQUE в PostgreSQL: проверяем данные на живом примере
В каталоге магазина цена не должна быть отрицательной, а SKU — повторяться. Покажем, как закрепить эти правила в PostgreSQL 16, где CHECK и UNIQUE ведут себя не так, как ждут разработчики, и что проверить перед выкладкой в прод.

В каталоге магазина цена не должна быть отрицательной, скидка — превышать цену, а SKU — повторяться. В PostgreSQL 16 эти правила можно закрепить ограничениями CHECK и UNIQUE: тогда ошибочные данные не пройдут даже через другой сервис или ручной SQL-запрос. Но у ограничений есть тонкости с NULL, несколькими колонками и уже заполненными таблицами.
Сценарий: каталог товаров
Пусть в таблице хранятся артикул, цена и скидка. Правила такие: артикул уникален, цена неотрицательна, скидка лежит между нулём и ценой. Проверять их только в форме недостаточно: данные могут записать импорт, фоновая задача или другой сервис. Ограничение в базе становится последним барьером на входе.
Примеры ниже рассчитаны на PostgreSQL 16. Для запуска нужны созданная база и права на создание таблицы. Используем отдельное имя таблицы, чтобы пример не затронул существующие данные.
CREATE TABLE demo_products (
product_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku text NOT NULL,
price numeric(10, 2) NOT NULL,
discount numeric(10, 2) NOT NULL DEFAULT 0,
CONSTRAINT demo_products_sku_uq UNIQUE (sku),
CONSTRAINT demo_products_price_nonnegative CHECK (price >= 0),
CONSTRAINT demo_products_discount_range CHECK (
discount >= 0 AND discount <= price
)
);
INSERT INTO demo_products (sku, price, discount)
VALUES ('MUG-001', 1200.00, 100.00);
Вставка проходит. Повторный SKU, отрицательная цена или скидка больше цены вызовут ошибку, и строка не попадёт в таблицу. Имена ограничений заданы явно: так в сообщении об ошибке проще понять, какое именно правило нарушено.
CHECK: условие для одной строки
CHECK проверяет логическое выражение для каждой добавляемой или изменяемой строки. Выражение должно быть истинным или неопределённым. Здесь и возникает частый сюрприз: результат NULL — это не то же самое, что FALSE, и сам по себе CHECK такую строку не запрещает.
Например, ограничение CHECK (discount <= price) не запрещает NULL в discount. Если колонка должна быть заполнена, как в нашем примере, добавьте NOT NULL. Если пустое значение разрешено, но при заполнении нужно соблюдать условие, оставьте NULL допустимым и проверьте остальные ограничения отдельно.
CHECK подходит для локальных правил вроде «цена не меньше нуля» или «дата окончания не раньше даты начала». Он не предназначен для сравнения с другими строками или таблицами: нельзя надёжно выразить им правило «у каждого клиента только один активный заказ». Для этого выбирают UNIQUE, внешний ключ или другую модель ограничения.
UNIQUE: одно значение или комбинация
UNIQUE (sku) не даёт сохранить две строки с одинаковым SKU. Ограничение может включать несколько колонок: например, уникальная пара «поставщик и его артикул» допускает одинаковый артикул у разных поставщиков, но не дважды у одного.
CREATE TABLE demo_supplier_products (
supplier_id bigint NOT NULL,
supplier_sku text NOT NULL,
CONSTRAINT demo_supplier_products_supplier_sku_uq
UNIQUE (supplier_id, supplier_sku)
);
Порядок колонок в ограничении описывает набор, который должен быть уникален; это не то же самое, что два отдельных UNIQUE на каждой колонке. Если важна уникальность только среди строк, отвечающих условию, например среди активных записей, понадобится частичный уникальный индекс, а не обычное ограничение UNIQUE.
Что происходит с NULL
По умолчанию PostgreSQL считает NULL отличающимся от любого другого NULL для проверки уникальности. Поэтому в nullable-колонке UNIQUE может допустить несколько строк без значения. В PostgreSQL 15 и новее можно задать UNIQUE NULLS NOT DISTINCT, чтобы NULL тоже считались совпадением. В PostgreSQL 16 такой синтаксис доступен.
Если отсутствие значения не имеет смысла, часто проще объявить колонку NOT NULL. Это понятнее для приложения и не оставляет неоднозначности в данных. Перед выбором проверьте, действительно ли NULL означает «значения пока нет», а не особое состояние предметной области.
Какой способ выбрать
| Задача | Что использовать | Цена решения |
|---|---|---|
| Проверить значение в одной строке | CHECK | Нужно следить, чтобы условие не принимало NULL за допустимый результат |
| Запретить дубликаты по колонке или набору колонок | UNIQUE | PostgreSQL создаёт уникальный индекс; вставки и обновления затронутых значений требуют проверки уникальности |
| Уникальность только для части строк | Частичный уникальный индекс | Это индекс, а не обычное ограничение; условие и запросы нужно поддерживать согласованно |
Валидация в приложении помогает показать человеку понятную ошибку. Ограничение в PostgreSQL защищает сами данные, даже если запись пришла не через это приложение.
Уникальный индекс — структура базы, которая ускоряет поиск и может не допускать повторяющиеся значения. Для ограничения UNIQUE PostgreSQL использует такой индекс, поэтому создание ограничения на большой таблице может потребовать заметных ресурсов и повлиять на запись.
Что ломается при добавлении правил
Если таблица уже заполнена, новый CHECK или UNIQUE должен согласоваться с существующими строками. Отрицательная цена в старой записи помешает добавить проверку, а повторяющиеся SKU — уникальность. Сначала найдите и исправьте нарушения, иначе миграция завершится ошибкой.
Проверьте и конкурентные изменения. Между поиском дубликатов и созданием ограничения другой запрос может вставить такую же строку. Поэтому предварительная проверка в приложении не заменяет само ограничение: окончательное решение принимает база.
Для большого работающего проекта нельзя бездумно выполнять DDL в пиковое время. Создание уникального ограничения строит индекс и может ждать блокировки. Если требуется снизить влияние на запись, изучите создание уникального индекса командой CREATE UNIQUE INDEX CONCURRENTLY и последующее присоединение индекса к ограничению. У concurrent-режима есть собственные ограничения: например, его нельзя выполнять внутри транзакционного блока. Сверьте порядок действий с документацией PostgreSQL 16 и проверьте миграцию на копии данных.
Когда какой вариант брать
- Для арифметического правила внутри строки — CHECK; если пустое значение запрещено, добавьте NOT NULL.
- Для запрета одинаковых идентификаторов — UNIQUE на нужной колонке или комбинации колонок.
- Для «уникально только пока запись активна» — частичный уникальный индекс с условием, соответствующим модели данных.
- Для связи с записью в другой таблице — внешний ключ, а не CHECK с подзапросом.
Что делать дальше
Перед миграцией проверьте старые строки на отрицательные цены, скидки вне диапазона и дубликаты SKU. Затем примените ограничения на тестовой копии базы и убедитесь, что приложение корректно обрабатывает ошибки PostgreSQL при нарушении правил. После выкладки наблюдайте за временем миграции, блокировками и частотой ошибок вставки: именно там станет видно, не расходятся ли реальные данные с выбранными ограничениями.
Инфобез и 152-ФЗ: Аудит политик конфиденциальности, правила сбора cookie.
Спросить за 20 ₽Источники: PostgreSQL 16: ограничения таблиц, PostgreSQL 16: ALTER TABLE
Частые вопросы
CHECK запрещает NULL?
Можно ли проверить в CHECK, что такой записи нет в другой таблице?
UNIQUE разрешит несколько NULL?
Что будет, если таблица уже содержит дубликаты?
Можно ли добавить ограничение без остановки записи?
Подойдёт ли пример для PostgreSQL 14?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.