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_имя_таблицыдля PKfk_таблица_поледля FKuq_таблица_поледля UNIQUEchk_таблица_условиедля 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: как изменить существующую структуру таблицы — добавить колонку, изменить тип, добавить или удалить ограничение.