Пошук уроків, статей та іншого контенту
Застосовуйте NOT NULL, UNIQUE, CHECK, DEFAULT і EXCLUDE для захисту даних на рівні PostgreSQL.
Обмеження цілісності — це правила, які PostgreSQL перевіряє під час вставлення або зміни даних. Вони не дозволяють зберігати в таблиці некоректні значення навіть тоді, коли помилка виникла в коді застосунку.
Основні переваги обмежень:
дані залишаються узгодженими незалежно від клієнта;
помилки виявляються якомога ближче до місця їх виникнення;
правила предметної області зберігаються разом зі схемою бази даних;
кілька застосунків можуть безпечно працювати з одними й тими самими таблицями.
У PostgreSQL для цього використовують NOT NULL, UNIQUE, CHECK, DEFAULT і EXCLUDE.
NOT NULLNOT NULL забороняє зберігати в стовпці значення NULL.
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL,
name text
);У цьому прикладі кожен користувач повинен мати значення email, але name може бути відсутнім.
Спроба вставити рядок без email завершиться помилкою:
INSERT INTO users (name)
VALUES ('Олена');Важливо відрізняти відсутнє значення від порожнього рядка:
NULL означає, що значення невідоме або не задане;
'' — це звичайний рядок із нульовою довжиною.
NOT NULL забороняє лише NULL, але не порожній рядок. Якщо порожній рядок також потрібно заборонити, це можна зробити за допомогою CHECK:
CREATE TABLE profiles (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
display_name text NOT NULL CHECK (length(trim(display_name)) > 0)
);UNIQUEUNIQUE гарантує, що значення або комбінація значень не повторюються в таблиці.
CREATE TABLE accounts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE
);Тепер два рядки з однаковою електронною адресою зберегти не можна:
INSERT INTO accounts (email)
VALUES ('user@example.com');
INSERT INTO accounts (email)
VALUES ('user@example.com');Другий INSERT завершиться помилкою порушення унікальності.
Іноді унікальним має бути не окреме значення, а комбінація:
CREATE TABLE project_members (
project_id bigint NOT NULL,
user_id bigint NOT NULL,
CONSTRAINT project_members_unique
UNIQUE (project_id, user_id)
);Один користувач може брати участь у багатьох проєктах, але не може бути двічі доданий до одного проєкту.
NULL і UNIQUEЗа замовчуванням PostgreSQL дозволяє кілька NULL у стовпці з обмеженням UNIQUE, оскільки NULL не вважається рівним іншому NULL.
CREATE TABLE coupons (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
code text UNIQUE
);У цій таблиці може бути кілька рядків із code = NULL, але однакові непорожні коди заборонені.
Якщо значення повинно бути і заданим, і унікальним, зазвичай використовують обидва обмеження:
CREATE TABLE api_keys (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
key_value text NOT NULL UNIQUE
);CHECKCHECK перевіряє логічний вираз для кожного рядка.
CREATE TABLE products (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
price numeric(10, 2) NOT NULL
CHECK (price >= 0),
stock integer NOT NULL DEFAULT 0
CHECK (stock >= 0)
);Ці правила забороняють:
від’ємну ціну;
від’ємну кількість товару;
відсутню назву;
NULL у price і stock.
Для зрозумілих повідомлень про помилки обмеження варто називати:
CREATE TABLE discounts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
percent numeric(5, 2) NOT NULL,
CONSTRAINT discounts_percent_range
CHECK (percent >= 0 AND percent <= 100)
);Назва обмеження з’явиться в повідомленні PostgreSQL і допоможе швидко знайти причину помилки.
NULL у CHECKОбмеження CHECK вважається виконаним, якщо його вираз повертає TRUE або NULL. Тому CHECK сам по собі не завжди забороняє пропущене значення:
CREATE TABLE temperatures (
value numeric CHECK (value >= -273.15)
);Стовпець value може містити NULL, адже вираз NULL >= -273.15 має результат NULL.
Якщо значення обов’язкове, потрібно явно додати NOT NULL:
CREATE TABLE valid_temperatures (
value numeric NOT NULL
CHECK (value >= -273.15)
);CHECK призначений для перевірки одного рядка. Він не підходить для правил на кшталт «значення не повинно повторюватися» або «два записи не повинні перетинатися». Для цього використовують UNIQUE та EXCLUDE.
DEFAULTDEFAULT задає значення, якщо під час INSERT стовпець не вказали.
CREATE TABLE tasks (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title text NOT NULL,
status text NOT NULL DEFAULT 'new',
created_at timestamptz NOT NULL DEFAULT now()
);Тепер можна вставити завдання без status і created_at:
INSERT INTO tasks (title)
VALUES ('Підготувати звіт');PostgreSQL автоматично використає:
'new' для status;
поточний час для created_at.
DEFAULT застосовується лише тоді, коли стовпець не вказали або використали ключове слово DEFAULT. Явне значення NULL не замінюється значенням за замовчуванням:
INSERT INTO tasks (title, status)
VALUES ('Інше завдання', NULL);Такий запит завершиться помилкою через NOT NULL, а не використає 'new'.
Значенням за замовчуванням може бути вираз або функція:
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
event_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
event_date date NOT NULL DEFAULT CURRENT_DATE
);now() і CURRENT_TIMESTAMP у PostgreSQL повертають час поточної транзакції. Для часу створення запису це зазвичай саме потрібна поведінка.
EXCLUDEEXCLUDE використовується для заборони конфліктів між різними рядками. Воно узагальнює ідею UNIQUE: PostgreSQL не дозволяє одночасно існувати двом рядкам, для яких усі вказані оператори повертають TRUE.
Практичний приклад — бронювання кімнат. Для однієї кімнати часові інтервали не повинні перетинатися.
Для роботи з оператором рівності над цілими числами в GiST-індексі потрібне розширення btree_gist:
CREATE EXTENSION IF NOT EXISTS btree_gist;Таблиця бронювань:
CREATE TABLE room_reservations (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
room_id integer NOT NULL,
reserved_by text NOT NULL,
during tstzrange NOT NULL,
CONSTRAINT room_reservations_valid_range
CHECK (
lower(during) IS NOT NULL
AND upper(during) IS NOT NULL
AND lower(during) < upper(during)
),
CONSTRAINT room_reservations_no_overlap
EXCLUDE USING gist (
room_id WITH =,
during WITH &&
)
);У цьому обмеженні:
room_id WITH = вимагає, щоб кімнати були однаковими;
during WITH && перевіряє перетин часових діапазонів;
конфлікт виникає, якщо одна й та сама кімната має діапазони, що перетинаються.
Повний робочий приклад:
CREATE EXTENSION IF NOT EXISTS btree_gist;
DROP TABLE IF EXISTS room_reservations;
DROP TABLE IF EXISTS products;
CREATE TABLE products (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku text NOT NULL UNIQUE,
name text NOT NULL CHECK (length(trim(name)) > 0),
price numeric(10, 2) NOT NULL
CHECK (price >= 0),
stock integer NOT NULL DEFAULT 0
CHECK (stock >= 0),
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO products (sku, name, price)
VALUES ('KB-001', 'Механічна клавіатура', 2499.00);
CREATE TABLE room_reservations (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
room_id integer NOT NULL,
reserved_by text NOT NULL,
during tstzrange NOT NULL,
CONSTRAINT room_reservations_valid_range
CHECK (
lower(during) IS NOT NULL
AND upper(during) IS NOT NULL
AND lower(during) < upper(during)
),
CONSTRAINT room_reservations_no_overlap
EXCLUDE USING gist (
room_id WITH =,
during WITH &&
)
);
INSERT INTO room_reservations (room_id, reserved_by, during)
VALUES (
101,
'Олена',
tstzrange(
'2026-09-01 10:00+03',
'2026-09-01 12:00+03',
'[)'
)
);
-- Інша кімната: конфлікту немає.
INSERT INTO room_reservations (room_id, reserved_by, during)
VALUES (
102,
'Андрій',
tstzrange(
'2026-09-01 11:00+03',
'2026-09-01 13:00+03',
'[)'
)
);
-- Суміжний інтервал для тієї самої кімнати: конфлікту немає.
INSERT INTO room_reservations (room_id, reserved_by, during)
VALUES (
101,
'Марія',
tstzrange(
'2026-09-01 12:00+03',
'2026-09-01 14:00+03',
'[)'
)
);
-- Приклад помилки EXCLUDE:
-- той самий room_id і часовий інтервал, що перетинається.
-- INSERT INTO room_reservations (room_id, reserved_by, during)
-- VALUES (
-- 101,
-- 'Іван',
-- tstzrange(
-- '2026-09-01 11:30+03',
-- '2026-09-01 12:30+03',
-- '[)'
-- )
-- );Позначення '[)' означає, що початок діапазону включений, а кінець — ні. Тому бронювання з 10:00 до 12:00 і бронювання з 12:00 до 14:00 не перетинаються.
Для EXCLUDE також важливо використовувати NOT NULL. Якщо room_id або during можуть бути NULL, PostgreSQL може не вважати такі рядки конфліктними, оскільки порівняння з NULL не повертає TRUE.
Обмеження можна додати після створення таблиці за допомогою ALTER TABLE:
ALTER TABLE products
ADD CONSTRAINT products_price_limit
CHECK (price <= 1000000);Додавання NOT NULL має окремий синтаксис:
ALTER TABLE products
ALTER COLUMN name SET NOT NULL;Перед цим потрібно переконатися, що в стовпці вже немає NULL. Інакше PostgreSQL не зможе застосувати обмеження.
Обмеження можна видалити за назвою:
ALTER TABLE products
DROP CONSTRAINT products_price_limit;Для NOT NULL використовується інша команда:
ALTER TABLE products
ALTER COLUMN name DROP NOT NULL;У робочих проєктах обмеження зазвичай додають через міграції, щоб зміни структури бази даних були контрольованими та відтворюваними.
Орієнтуйтеся на призначення правила:
NOT NULL — значення обов’язкове;
UNIQUE — значення або комбінація значень не повинні повторюватися;
CHECK — значення має відповідати логічній умові;
DEFAULT — значення потрібно автоматично встановити, якщо його не передали;
EXCLUDE — різні рядки не повинні конфліктувати за заданими операторами.
Часто кілька обмежень використовують разом. Наприклад, для ціни потрібні NOT NULL, DEFAULT і CHECK, а для коду товару — NOT NULL та UNIQUE.
Перевірка в коді не захищає від одночасних запитів із кількох процесів. Два запити можуть одночасно перевірити, що значення вільне, і обидва спробувати його вставити.
Для гарантованої унікальності використовуйте UNIQUE у базі даних.
DEFAULT замінює NULLDEFAULT працює лише для пропущеного стовпця. Якщо передати NULL, буде використано саме NULL. Для обов’язкового значення додавайте NOT NULL.
CHECK замість UNIQUECHECK не призначений для пошуку дублікатів у таблиці. Він перевіряє умову поточного рядка, а UNIQUE забезпечує унікальність між рядками.
NULL у CHECKВираз CHECK із результатом NULL вважається успішним. Якщо значення не може бути пропущеним, використовуйте NOT NULL разом із CHECK.
Автоматичні імена важче читати в повідомленнях про помилки та міграціях. Явні назви на кшталт products_price_non_negative спрощують обслуговування схеми.
NULL у EXCLUDEДля правил, що стосуються всіх рядків, стовпці в EXCLUDE зазвичай потрібно оголосити як NOT NULL. Інакше рядки з NULL можуть обійти перевірку конфліктів.
Обмеження цілісності захищають дані на рівні PostgreSQL.
NOT NULL забороняє відсутні значення.
UNIQUE не дозволяє дублікати значень або їх комбінацій.
CHECK перевіряє умову для кожного рядка.
DEFAULT автоматично задає значення лише для пропущених стовпців.
EXCLUDE забороняє конфлікти між рядками, наприклад перетин бронювань.
CHECK не замінює NOT NULL, а DEFAULT не замінює його автоматично.
Явні назви обмежень полегшують діагностику помилок і підтримку схеми.