NOT NULL, UNIQUE, CHECK, DEFAULT

NOT NULL, UNIQUE, CHECK, DEFAULT

В предыдущем уроке вы освоили PRIMARY KEY и FOREIGN KEY. Теперь — четыре дополнительных ограничения, которые защищают данные в отдельных колонках: NOT NULL запрещает пустые значения, UNIQUE гарантирует уникальность, CHECK проверяет произвольные условия, DEFAULT задаёт значение по умолчанию.

NOT NULL: обязательное поле

NOT NULL запрещает вставку или обновление строки с NULL в этой колонке:

CREATE TABLE users (
    id       SERIAL PRIMARY KEY,
    username TEXT NOT NULL,
    email    TEXT NOT NULL
);

INSERT INTO users (username) VALUES ('anna');
-- ERROR: null value in column "email" violates not-null constraint

Когда использовать NOT NULL: для всех полей, без которых строка теряет смысл. Username без email — может быть нормой; email без username — нет; имя клиента без заказа — нет смысла.

Когда оставить NULL: для необязательных полей (phone, bio, middle_name). NULL означает «нет данных» — это отличается от пустой строки ''.

NULL vs пустая строка:

-- NULL: телефон не указан (нет данных)
INSERT INTO customers (name, phone) VALUES ('Анна', NULL);

-- Пустая строка: телефон указан пустым (некорректные данные)
INSERT INTO customers (name, phone) VALUES ('Анна', '');

В PostgreSQL '' и NULL — разные вещи. Многие разработчики предпочитают NOT NULL DEFAULT '' для строковых полей вместо NULL — вопрос конвенции в команде.

UNIQUE: уникальное значение

UNIQUE гарантирует, что в колонке нет двух строк с одинаковым значением:

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

INSERT INTO users (username, email) VALUES ('anna', 'anna@example.com');
INSERT INTO users (username, email) VALUES ('anna', 'other@example.com');
-- ERROR: duplicate key value violates unique constraint "users_username_key"

UNIQUE автоматически создаёт индекс — как и PRIMARY KEY. Это делает поиск по уникальным колонкам быстрым.

UNIQUE и NULL: NULL не равен NULL с точки зрения уникальности. Два NULL в UNIQUE-колонке — это допустимо:

-- Оба значения NULL — не конфликт UNIQUE
INSERT INTO customers (name, phone) VALUES ('Анна', NULL);
INSERT INTO customers (name, phone) VALUES ('Борис', NULL);
-- Нет ошибки, хотя phone = UNIQUE: обе строки допустимы

Составной UNIQUE:

-- Уникальная пара (user_id, product_id) — пользователь может лайкнуть товар только раз
UNIQUE (user_id, product_id)

Объявляется на уровне таблицы, как составной PK.

CHECK: произвольное условие

CHECK проверяет, что значение удовлетворяет условию. Любое булево выражение:

CREATE TABLE products (
    id       SERIAL PRIMARY KEY,
    title    TEXT NOT NULL,
    price    NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
    discount NUMERIC(5, 2) DEFAULT 0 CHECK (discount >= 0 AND discount <= 100),
    quantity INTEGER DEFAULT 0 CHECK (quantity >= 0)
);

При вставке или обновлении значения с нарушением CHECK — ошибка:

INSERT INTO products (title, price) VALUES ('Тест', -100);
-- ERROR: new row for relation "products" violates check constraint "products_price_check"

Именованный CHECK:

CONSTRAINT chk_positive_price CHECK (price >= 0)

Сообщение об ошибке будет содержать имя chk_positive_price — намного понятнее.

CHECK на уровне таблицы:

CREATE TABLE sales (
    start_date DATE,
    end_date   DATE,
    CONSTRAINT chk_dates CHECK (end_date >= start_date)
);

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

Проверь себя: как с помощью CHECK ограничить значение status только тремя допустимыми строками?

DEFAULT: значение по умолчанию

DEFAULT задаёт значение, используемое при вставке, если колонка не указана:

CREATE TABLE orders (
    id         SERIAL PRIMARY KEY,
    status     TEXT     DEFAULT 'pending',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    total      NUMERIC(10, 2) DEFAULT 0.00,
    is_paid    BOOLEAN  DEFAULT FALSE
);

INSERT INTO orders (total) VALUES (1500);
-- status = 'pending', created_at = NOW(), is_paid = FALSE (автоматически)

Типичные значения по умолчанию:

  • DEFAULT NOW() — текущее время для временных меток
  • DEFAULT CURRENT_DATE — текущая дата
  • DEFAULT FALSE — отключённые флаги
  • DEFAULT 0 — начальные счётчики
  • DEFAULT 'active' / DEFAULT 'pending' — начальные статусы

DEFAULT и явный NULL: если при INSERT явно передать NULL, DEFAULT игнорируется — вставится NULL. DEFAULT используется только когда колонка вообще не упомянута.

Именование ограничений

Каждое ограничение можно именовать через CONSTRAINT name:

CREATE TABLE products (
    id    SERIAL,
    price NUMERIC(10, 2),
    CONSTRAINT pk_products       PRIMARY KEY (id),
    CONSTRAINT chk_product_price CHECK (price >= 0 AND price < 1000000),
    CONSTRAINT uq_product_sku    UNIQUE (sku)
);

Зачем именовать: при нарушении ограничения PostgreSQL сообщает имя. CONSTRAINT pk_products в ошибке читается лучше, чем products_pkey_123. Особенно полезно в production-логах.

Соглашение по именованию:

  • pk_имя_таблицы для PK
  • fk_таблица_поле для FK
  • uq_таблица_поле для UNIQUE
  • chk_таблица_условие для CHECK

Практический пример: полная схема с ограничениями

CREATE TABLE employees (
    id          SERIAL PRIMARY KEY,
    first_name  TEXT NOT NULL,
    last_name   TEXT NOT NULL,
    email       TEXT UNIQUE NOT NULL,
    department  TEXT NOT NULL,
    salary      NUMERIC(10, 2) NOT NULL
        CONSTRAINT chk_salary_positive CHECK (salary > 0),
    level       TEXT NOT NULL DEFAULT 'Junior'
        CONSTRAINT chk_level CHECK (level IN ('Junior', 'Middle', 'Senior')),
    hired_at    DATE NOT NULL DEFAULT CURRENT_DATE,
    is_active   BOOLEAN NOT NULL DEFAULT TRUE
);

Каждое поле защищено правильным ограничением. CHECK (level IN ('Junior', 'Middle', 'Senior')) — допустимые значения как ограничение.

Ограничения и NULL в CHECK

Важная тонкость: CHECK не нарушается, если значение NULL. Это трёхзначная логика — CHECK (price >= 0) при price = NULL даёт UNKNOWN, что не является нарушением.

-- Не нарушает CHECK (price >= 0) — NULL игнорируется CHECK
UPDATE products SET price = NULL WHERE id = 1;

Если нужно запретить и отрицательные, и NULL — используйте оба ограничения:

price NUMERIC(10, 2) NOT NULL CHECK (price >= 0)

Выбор между ограничениями в БД и проверками в приложении

Ограничения в базе данных vs проверки в коде приложения — распространённый вопрос.

Ограничения в БД:

  • Работают всегда, независимо от того, через какой канал пришли данные
  • Защищают от ошибок в нескольких приложениях, обращающихся к одной БД
  • Не требуют дублирования логики в каждом сервисе

Проверки в приложении:

  • Быстрее для пользователя (без round-trip к БД)
  • Более гибкие (сложные бизнес-правила, которые нельзя выразить в SQL)
  • Нужны для понятных сообщений об ошибках пользователю

Лучшая практика: оба. Ограничения в БД — последний барьер, гарантирующий целостность. Проверки в приложении — первый барьер, для хорошего UX. Дублирование допустимо.

Добавление ограничений к существующей таблице

Ограничения добавляются к существующей таблице через ALTER TABLE:

-- Добавить NOT NULL (требует, чтобы все существующие значения не были NULL)
ALTER TABLE products ALTER COLUMN price SET NOT NULL;

-- Добавить CHECK
ALTER TABLE products ADD CONSTRAINT chk_price CHECK (price >= 0);

-- Добавить UNIQUE
ALTER TABLE users ADD CONSTRAINT uq_users_email UNIQUE (email);

Если в существующих данных есть значения, нарушающие новое ограничение, — команда завершится ошибкой. Сначала очистите данные, потом добавьте ограничение. Подробнее о ALTER TABLE — в следующем уроке.

Краткий итог

  • NOT NULL — запрещает NULL; используйте для всех обязательных полей
  • UNIQUE — гарантирует уникальность; автоматически создаёт индекс; NULL ≠ NULL (допускаются)
  • CHECK (условие) — произвольная проверка; при NULL проверка не нарушается
  • DEFAULT значение — значение при вставке без указания колонки; явный NULL переопределяет
  • Именуйте ограничения через CONSTRAINT name — сообщения об ошибках станут понятнее

Диагностика: проверить какие ограничения нарушены

Когда INSERT или UPDATE падает с ошибкой ограничения, сообщение PostgreSQL содержит имя ограничения:

ERROR: new row for relation "products" violates check constraint "chk_product_price"
DETAIL: Failing row contains (5, Тест, -500.00, ...).

С именованными ограничениями сразу понятно, что именно нарушено. Без имени PostgreSQL генерирует имя автоматически — например products_price_check — что менее читаемо.

Посмотреть все ограничения конкретной таблицы:

SELECT conname, contype, pg_get_constraintdef(oid)
FROM   pg_constraint
WHERE  conrelid = 'products'::regclass;

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

Что дальше

Вы освоили все ограничения. Следующий урок — ALTER TABLE: как изменить существующую структуру таблицы — добавить колонку, изменить тип, добавить или удалить ограничение.

Попробуйте интерактивную версию

Практические задачи, квизы и AI-наставник — бесплатный старт без карты

Перейти к практике