Ограничения (constraints)
Предпосылки: реляционная модель (первичный ключ, внешний ключ, ссылочная целостность), таблицы и типы (CREATE TABLE, типы данных, DEFAULT), типы данных и NULL.
← Таблицы и типы | Партиционирование →
Три микросервиса пишут в одну таблицу orders. Один вставляет заказы с отрицательной суммой, другой — заказы без пользователя, третий — дубликаты по external_id. Валидация на уровне приложения не спасла: один сервис обновили, в другом забыли проверку, третий вставлял через ручной SQL.
Ограничения (constraints, англ. «ограничения, условия») решают эту проблему на уровне данных: база сама удерживает инварианты, на которые опирается код.
NOT NULL — запрет отсутствия значения
CREATE TABLE users (
id integer,
name text NOT NULL,
email text NOT NULL
);INSERT с NULL в name или email вернёт ошибку. NOT NULL — самый дешёвый способ гарантировать наличие обязательных данных.
PRIMARY KEY — уникальный идентификатор строки
PRIMARY KEY (англ. «первичный ключ») = UNIQUE + NOT NULL. Гарантирует, что каждая строка имеет уникальный, непустой идентификатор:
CREATE TABLE users (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL
);PostgreSQL автоматически создаёт уникальный индекс для PRIMARY KEY. Почти каждая таблица должна иметь PRIMARY KEY. Подробнее о индексах и их роли в производительности см. в соответствующей заметке.
UNIQUE — уникальность значений
CREATE TABLE users (
id BIGINT PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);UNIQUE запрещает дубликаты в столбце (или комбинации столбцов). PostgreSQL реализует UNIQUE через уникальный индекс. Подробнее о индексах.
UNIQUE и NULL
UNIQUE не запрещает несколько NULL — потому что NULL = NULL в SQL возвращает NULL, не TRUE (трёхзначная логика), и два NULL не считаются дубликатами:
CREATE TABLE emails (email TEXT UNIQUE);
INSERT INTO emails(email) VALUES (NULL), (NULL); -- оба вставятсяЕсли нужен «уникальный и не пустой» — это всегда связка NOT NULL UNIQUE.
Частичная уникальность
Уникальность только среди активных записей (soft delete):
CREATE UNIQUE INDEX users_email_active_uq
ON users(email)
WHERE deleted_at IS NULL;Это частичный уникальный индекс — не CONSTRAINT, но выполняет ту же работу.
FOREIGN KEY — ссылочная целостность
FOREIGN KEY (англ. «внешний ключ») гарантирует, что ссылка указывает на существующую строку:
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id)
);Заказ не может ссылаться на несуществующего пользователя. При попытке вставить user_id, отсутствующий в users — ошибка.
Индекс на дочерней стороне FK
Для корректности FK достаточно индекса на родительской стороне (users(id)) — он есть из PRIMARY KEY. Но для производительности почти всегда нужен индекс на дочерней стороне:
CREATE INDEX orders_user_id_idx ON orders(user_id);Без него удаление или обновление users.id вызывает полный скан orders для проверки ссылок.
ON DELETE / ON UPDATE — каскадные действия
Поведение при удалении/изменении родительской строки:
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE
);NO ACTION(по умолчанию) — запретить, если есть ссылки. Проверка откладывается до конца оператора, поэтому совместим сDEFERRABLE.RESTRICT— запретить немедленно, до завершения оператора. Не совместим сDEFERRABLE.CASCADE(англ. «каскад, цепная реакция») — удалить/обновить дочерние строки автоматически.SET NULL— разорвать ссылку (поставить NULL).SET DEFAULT— поставить значение по умолчанию.
CASCADE удобен, но опасен: одно удаление может превратиться в тысячи DELETE.
CHECK — произвольное условие на строку
CHECK (англ. «проверка») проверяет условие при INSERT и UPDATE:
CREATE TABLE payments (
id BIGINT PRIMARY KEY,
amount_cents BIGINT NOT NULL CHECK (amount_cents >= 0),
currency TEXT NOT NULL CHECK (currency IN ('USD', 'EUR', 'RUB'))
);CHECK — самый дешёвый способ закрепить доменные правила прямо в данных.
CHECK и NULL
CHECK не отвергает NULL: если условие вернуло NULL (а не FALSE), строка пропускается. Для столбца без NOT NULL:
CREATE TABLE t (x integer CHECK (x > 0));
INSERT INTO t(x) VALUES (NULL); -- пройдёт! NULL > 0 --> NULL, не FALSEЕсли NULL недопустим — добавьте NOT NULL.
Для инвариантов сложнее равенства (запрет пересечений по времени, перекрытий по диапазонам) PostgreSQL добавляет EXCLUSION constraint.
DEFERRABLE — отложенная проверка
По умолчанию ограничения проверяются немедленно на каждом операторе. DEFERRABLE позволяет отложить проверку до COMMIT:
ALTER TABLE orders
ADD CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES users(id)
DEFERRABLE INITIALLY DEFERRED;Удобно при переносе данных, когда порядок вставки не совпадает с порядком зависимостей. Цена: ошибка обнаруживается не на INSERT, а на COMMIT.
Практические правила
- Почти у каждой таблицы — PRIMARY KEY.
- У каждого FOREIGN KEY — индекс на дочерней стороне.
- UNIQUE без NOT NULL редко делает то, что кажется.
- CHECK — для простых доменных правил.
- Ограничения стоят I/O и создают ожидания блокировок между транзакциями — это нормальная цена за корректность.
- Добавление ограничений на таблицу с данными и трафиком требует осторожности — безопасные паттерны.
Sources
- PostgreSQL Documentation (v16): Constraints. https://www.postgresql.org/docs/16/ddl-constraints.html
- PostgreSQL Documentation (v16): CREATE TABLE, ALTER TABLE. https://www.postgresql.org/docs/16/sql-createtable.html