PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, CHECK
📋 Полное руководство по ограничениям в PostgreSQL
Ограничения (constraints) — это фундамент надёжной базы данных. Они обеспечивают целостность данных на уровне СУБД, предотвращая попадание некорректной информации. Этот материал охватывает все типы ограничений с глубокими объяснениями и практическими примерами.
Представьте базу данных без ограничений:
-- Что может пойти не так?
INSERT INTO users (id, email, age) VALUES (1, 'invalid-email', -5);
INSERT INTO users (id, email, age) VALUES (1, 'duplicate-id', 25);
INSERT INTO users (id, email, age) VALUES (2, NULL, 200);
INSERT INTO orders (user_id, amount) VALUES (999, -100); -- Пользователя 999 не существует!Результат: мусор в базе, нарушенная целостность, неработающие отчёты.
CREATE TABLE users (
id INTEGER PRIMARY KEY, -- Уникальный ID
email TEXT NOT NULL UNIQUE, -- Обязательный, уникальный
age INTEGER CHECK (age >= 0 AND age <= 150), -- Реалистичный возраст
created_at TIMESTAMPTZ DEFAULT NOW() -- Автоматическая дата
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id), -- Существующий пользователь
amount NUMERIC CHECK (amount > 0) -- Положительная сумма
);Преимущества:
💡 Важно: Ограничения проверяются на уровне СУБД, а не приложения. Это последний рубеж защиты данных.
PRIMARY KEY — это уникальный идентификатор каждой строки в таблице. Это самое важное ограничение в реляционной базе данных.
| Свойство | Описание |
|---|---|
| Уникальность | Никакие две строки не могут иметь одинаковый первичный ключ |
| NOT NULL | Первичный ключ не может быть NULL |
| Единственность | Только один PRIMARY KEY на таблицу |
| Автоматический индекс | PostgreSQL создаёт уникальный индекс автоматически |
| Неизменяемость | Значение первичного ключа не должно меняться |
-- Простой первичный ключ (один столбец)
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
-- Явное указание имени ограничения (рекомендуется)
CREATE TABLE users (
id INTEGER CONSTRAINT users_pkey PRIMARY KEY,
name TEXT NOT NULL
);
-- Первичный ключ в конце определения таблицы
CREATE TABLE users (
id INTEGER,
name TEXT NOT NULL,
PRIMARY KEY (id)
);Когда одного столбца недостаточно для уникальной идентификации:
-- Таблица связи многие-ко-многим
CREATE TABLE order_items (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER NOT NULL,
price NUMERIC(10, 2) NOT NULL,
-- Составной первичный ключ
PRIMARY KEY (order_id, product_id)
);
-- Теперь каждая пара (order_id, product_id) уникальна
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (1, 100, 2, 99.99); -- OK
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (1, 100, 5, 149.99); -- ОШИБКА: дубликат первичного ключа!⚠️ Важно: Составные первичные ключи усложняют работу с ORM и внешними ключами. Рассмотрите использование суррогатного ключа (id) с UNIQUE ограничением на естественные ключи.
PostgreSQL поддерживает несколько способов автоматической генерации ID:
-- SERIAL (устаревший, но популярный)
-- Создаёт последовательность и устанавливает её как DEFAULT
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
-- Под капотом:
-- CREATE SEQUENCE users_id_seq;
-- CREATE TABLE users (id INTEGER DEFAULT nextval('users_id_seq') PRIMARY KEY);
-- BIGSERIAL для больших таблиц
CREATE TABLE logs (
id BIGSERIAL PRIMARY KEY,
message TEXT NOT NULL
);
-- IDENTITY (стандарт SQL:2003, рекомендуется)
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL
);
-- IDENTITY с опциями
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY (
START WITH 1
INCREMENT BY 1
MINVALUE 1
MAXVALUE 2147483647
CACHE 1
) PRIMARY KEY,
name TEXT NOT NULL
);| Характеристика | SERIAL | IDENTITY |
|---|---|---|
| Стандарт | Расширение PostgreSQL | Стандарт SQL:2003 |
| Обход ограничения | Можно вставить свой ID | Требует OVERRIDING SYSTEM VALUE |
| Изменение последовательности | Через ALTER SEQUENCE | Через ALTER TABLE ... ALTER COLUMN |
| Рекомендация | ❌ Устаревает | ✅ Используйте IDENTITY |
-- SERIAL позволяет вставить свой ID (может быть проблемой)
INSERT INTO users (id, name) VALUES (1, 'Manual ID'); -- OK
-- IDENTITY требует явного указания
INSERT INTO users (id, name)
OVERRIDING SYSTEM VALUE
VALUES (1, 'Manual ID'); -- OK, но явно видно намерениеFOREIGN KEY создаёт связь между таблицами, обеспечивая ссылочную целостность. Значение внешнего ключа должно существовать в referenced таблице.
-- Родительская таблица
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
-- Дочерняя таблица с внешним ключом
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
amount NUMERIC(10, 2) NOT NULL
);
-- С явным именем ограничения (рекомендуется)
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER CONSTRAINT orders_user_fk REFERENCES users(id)
);
-- С именем и указанием столбцов
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER,
CONSTRAINT orders_user_fk
FOREIGN KEY (user_id) REFERENCES users(id)
);-- Создаём таблицы
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id)
);
-- Вставляем пользователя
INSERT INTO users (id, name) VALUES (1, 'Alice');
-- ✅ OK: user_id = 1 существует
INSERT INTO orders (id, user_id) VALUES (100, 1);
-- ❌ ОШИБКА: user_id = 999 не существует
INSERT INTO orders (id, user_id) VALUES (101, 999);
-- ERROR: insert or update on table "orders"
-- violates foreign key constraint "orders_user_id_fkey"
-- DETAIL: Key (user_id)=(999) is not present in table "users".
-- ❌ ОШИБКА: нельзя удалить пользователя с заказами
DELETE FROM users WHERE id = 1;
-- ERROR: update or delete on table "users"
-- violates foreign key constraint "orders_user_id_fkey"
-- on table "orders"Что делать с дочерними записями при изменении родительской?
-- CASCADE: удалить дочерние записи
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
);
DELETE FROM users WHERE id = 1; -- Все заказы пользователя тоже удалятся
-- SET NULL: установить NULL в дочерних записях
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL
);
DELETE FROM users WHERE id = 1; -- user_id в заказах станет NULL
-- Требуется: user_id INTEGER NULL (без NOT NULL)
-- SET DEFAULT: установить значение по умолчанию
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER DEFAULT 1 REFERENCES users(id) ON DELETE SET DEFAULT
);
DELETE FROM users WHERE id = 1; -- user_id станет 1 (или другим DEFAULT)
-- RESTRICT: запретить удаление (проверка сразу)
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE RESTRICT
);
DELETE FROM users WHERE id = 1; -- Ошибка, если есть заказы
-- NO ACTION: запретить удаление (проверка в конце транзакции)
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE NO ACTION
);
-- Разница с RESTRICT видна только в транзакциях:
BEGIN;
DELETE FROM users WHERE id = 1; -- OK пока что
DELETE FROM orders WHERE user_id = 1; -- Удаляем заказы
COMMIT; -- Здесь проверка NO ACTION и успех-- CASCADE: обновить ключи в дочерних таблицах
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON UPDATE CASCADE
);
UPDATE users SET id = 2 WHERE id = 1; -- user_id в заказах тоже обновится
-- RESTRICT / NO ACTION: запретить изменение ключа
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON UPDATE RESTRICT
);
UPDATE users SET id = 2 WHERE id = 1; -- Ошибка, если есть заказы| Вариант | Когда использовать | Пример |
|---|---|---|
| CASCADE | Дочерние записи бессмысленны без родителя | Заказы → Пользователь |
| SET NULL | Дочерние записи могут существовать без родителя | Посты → Автор (архив) |
| SET DEFAULT | Есть значение по умолчанию для «удалённых» | Товары → Категория (удалена → «Разное») |
| RESTRICT | Категорически нельзя удалять родителя | Транзакции → Счёт |
| NO ACTION | То же, но с отложенной проверкой | Сложные транзакции |
-- Родительская таблица с составным ключом
CREATE TABLE products (
warehouse_id INTEGER,
product_id INTEGER,
name TEXT NOT NULL,
PRIMARY KEY (warehouse_id, product_id)
);
-- Дочерняя таблица со ссылками на составной ключ
CREATE TABLE shipments (
id INTEGER PRIMARY KEY,
warehouse_id INTEGER,
product_id INTEGER,
quantity INTEGER NOT NULL,
CONSTRAINT shipments_product_fk
FOREIGN KEY (warehouse_id, product_id)
REFERENCES products(warehouse_id, product_id)
ON DELETE CASCADE
);-- Иерархия сотрудников
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
manager_id INTEGER REFERENCES employees(id) ON DELETE SET NULL
);
-- Пример данных
INSERT INTO employees (id, name, manager_id) VALUES
(1, 'CEO', NULL), -- Нет начальника
(2, 'CTO', 1), -- Начальник — CEO
(3, 'Developer', 2); -- Начальник — CTO
-- Удаление начальника
DELETE FROM employees WHERE id = 2; -- CTO уволен
-- Developer.manager_id станет NULLПроблема: две таблицы ссылаются друг на друга.
-- ❌ Не работает: таблица users ссылается на profiles, которой ещё нет
CREATE TABLE users (
id INTEGER PRIMARY KEY,
profile_id INTEGER REFERENCES profiles(id)
);
CREATE TABLE profiles (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id)
);Решение 1: Создать FK после создания таблиц
-- ✅ Работает
CREATE TABLE users (
id INTEGER PRIMARY KEY,
profile_id INTEGER -- Без FK пока
);
CREATE TABLE profiles (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id)
);
-- Добавляем FK после создания обеих таблиц
ALTER TABLE users
ADD CONSTRAINT users_profile_fk
FOREIGN KEY (profile_id) REFERENCES profiles(id);Решение 2: Использовать DEFERRABLE
-- FK проверяются в конце транзакции
CREATE TABLE users (
id INTEGER PRIMARY KEY,
profile_id INTEGER REFERENCES profiles(id) DEFERRABLE INITIALLY DEFERRED
);
CREATE TABLE profiles (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id) DEFERRABLE INITIALLY DEFERRED
);
-- Теперь можно вставить взаимозависимые данные
BEGIN;
INSERT INTO users (id, profile_id) VALUES (1, 1);
INSERT INTO profiles (id, user_id) VALUES (1, 1);
COMMIT; -- Проверка FK произойдёт здесьОграничение UNIQUE обеспечивает, что все значения в столбце (или группе столбцов) различны.
-- Уникальный email
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL
);
-- Явное имя ограничения
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT CONSTRAINT users_email_unique UNIQUE NOT NULL
);
-- Несколько UNIQUE ограничений
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
phone TEXT UNIQUE,
passport_number TEXT UNIQUE
);Важная особенность: NULL не равен NULL в SQL.
CREATE TABLE users (
id INTEGER PRIMARY KEY,
phone TEXT UNIQUE
);
INSERT INTO users (id, phone) VALUES (1, '123-456'); -- OK
INSERT INTO users (id, phone) VALUES (2, '123-456'); -- ❌ ОШИБКА: дубликат
-- Но с NULL:
INSERT INTO users (id, phone) VALUES (3, NULL); -- OK
INSERT INTO users (id, phone) VALUES (4, NULL); -- ✅ OK! NULL != NULL
INSERT INTO users (id, phone) VALUES (5, NULL); -- ✅ Тоже OK⚠️ Внимание: Несколько NULL значений допускаются в UNIQUE столбце. Если нужна уникальность с запретом NULL, используйте
UNIQUE NOT NULL.
-- Уникальная пара столбцов
CREATE TABLE subscriptions (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
subscribed_at TIMESTAMPTZ DEFAULT NOW(),
-- Уникальная подписка на категорию
UNIQUE (user_id, category_id)
);
-- Примеры
INSERT INTO subscriptions (user_id, category_id) VALUES (1, 100); -- OK
INSERT INTO subscriptions (user_id, category_id) VALUES (1, 101); -- OK
INSERT INTO subscriptions (user_id, category_id) VALUES (1, 100); -- ❌ Дубликат!
INSERT INTO subscriptions (user_id, category_id) VALUES (2, 100); -- OK (другой user)| Характеристика | PRIMARY KEY | UNIQUE |
|---|---|---|
| Количество на таблицу | Только один | Несколько |
| NULL значения | Не допускаются | Допускаются (множественно) |
| Индекс | Создаётся автоматически | Создаётся автоматически |
| Назначение | Идентификатор строки | Ограничение значений |
-- Уникальный slug для URL
CREATE TABLE articles (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
content TEXT
);
-- Уникальный комбинацией email + тип
CREATE TABLE contacts (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
type TEXT NOT NULL, -- 'work', 'personal', 'other'
value TEXT NOT NULL,
UNIQUE (user_id, type) -- Один контакт каждого типа
);
-- Уникальность с учётом регистра (через индекс)
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL
);
CREATE UNIQUE INDEX users_email_case_insensitive ON users (LOWER(email));NULL — это не значение, а отсутствие значения. Это не пустая строка '', не ноль 0, не ложь false.
-- NULL — это отсутствие данных
INSERT INTO users (id, name, email) VALUES (1, 'Alice', NULL); -- Email неизвестен
INSERT INTO users (id, name, email) VALUES (2, 'Bob', ''); -- Email известен как пустой
-- Сравнения с NULL
SELECT * FROM users WHERE email = NULL; -- ❌ Ничего не найдётся!
SELECT * FROM users WHERE email IS NULL; -- ✅ Правильно
SELECT * FROM users WHERE email IS NOT NULL; -- ✅ Не-NULL значения-- При создании таблицы
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
age INTEGER NOT NULL
);
-- Обязательные поля с DEFAULT
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);-- ❌ Не сработает, если есть NULL значения
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
-- ERROR: column "email" contains null values
-- ✅ Правильный подход:
-- 1. Найти NULL значения
SELECT id, name FROM users WHERE email IS NULL;
-- 2. Заполнить или удалить
UPDATE users SET email = 'unknown@example.com' WHERE email IS NULL;
-- 3. Теперь установить NOT NULL
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
-- 4. Проверить результат
\d users -- В psql покажет ограниченияALTER TABLE users ALTER COLUMN email DROP NOT NULL;-- NOT NULL столбцы не могут иметь NULL в индексе
-- Это упрощает индекс и может улучшить производительность
-- Частичный индекс только для не-NULL
CREATE INDEX idx_users_phone ON users (phone) WHERE phone IS NOT NULL;CHECK — это произвольное условие, которое должно быть истинным для каждой строки. Это самый гибкий тип ограничений.
-- Простая проверка
CREATE TABLE products (
id INTEGER PRIMARY KEY,
price NUMERIC(10, 2) CHECK (price > 0),
name TEXT NOT NULL
);
INSERT INTO products (price, name) VALUES (99.99, 'Widget'); -- OK
INSERT INTO products (price, name) VALUES (0, 'Freebie'); -- ❌ ОШИБКА
INSERT INTO products (price, name) VALUES (-10, 'Negative'); -- ❌ ОШИБКА-- Диапазон значений
CREATE TABLE users (
id INTEGER PRIMARY KEY,
age INTEGER CHECK (age >= 0 AND age <= 150),
score INTEGER CHECK (score BETWEEN 0 AND 100)
);
-- Перечисление значений (enum)
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
status TEXT CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled', 'refunded'))
);
-- Проверка формата (простая)
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT CHECK (email LIKE '%@%.%'),
phone TEXT CHECK (phone ~ '^\+?[0-9]{10,15}$') -- Regex проверка
);
-- Сравнение столбцов
CREATE TABLE events (
id INTEGER PRIMARY KEY,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
CHECK (end_date >= start_date)
);
-- Сложное условие
CREATE TABLE discounts (
id INTEGER PRIMARY KEY,
original_price NUMERIC(10, 2),
discount_percent NUMERIC(5, 2),
final_price NUMERIC(10, 2),
CHECK (
discount_percent >= 0
AND discount_percent <= 100
AND final_price = original_price * (1 - discount_percent / 100)
AND final_price >= 0
)
);-- Без имени (PostgreSQL сгенерирует автоматически)
CREATE TABLE products (
id INTEGER PRIMARY KEY,
price NUMERIC CHECK (price > 0)
);
-- С именем (рекомендуется для понятности)
CREATE TABLE products (
id INTEGER PRIMARY KEY,
price NUMERIC,
CONSTRAINT positive_price CHECK (price > 0),
CONSTRAINT reasonable_price CHECK (price < 1000000)
);-- Проверка взаимосвязи столбцов
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
quantity INTEGER NOT NULL,
unit_price NUMERIC(10, 2) NOT NULL,
total_amount NUMERIC(10, 2) NOT NULL,
-- total_amount должен равняться quantity * unit_price
CONSTRAINT correct_total
CHECK (total_amount = quantity * unit_price)
);
-- Взаимоисключающие поля
CREATE TABLE payments (
id INTEGER PRIMARY KEY,
card_number TEXT,
paypal_email TEXT,
-- Только один способ оплаты
CONSTRAINT one_payment_method
CHECK (
(card_number IS NOT NULL AND paypal_email IS NULL) OR
(card_number IS NULL AND paypal_email IS NOT NULL)
)
);-- ❌ Нельзя использовать подзапросы
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER,
amount NUMERIC,
CHECK (amount < (SELECT AVG(amount) FROM orders)) -- ❌ ОШИБКА
);
-- ❌ Нельзя использовать функции, изменяющие данные
CREATE TABLE events (
id INTEGER PRIMARY KEY,
event_date DATE,
CHECK (log_event(event_date)) -- ❌ ОШИБКА
);
-- ✅ Можно использовать IMMUTABLE функции
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT,
CHECK (LENGTH(email) > 5),
CHECK (email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
);| Метод | Когда использовать |
|---|---|
| CHECK | Простые условия, диапазоны, сравнения столбцов |
| ENUM | Фиксированный набор значений (статусы, типы) |
| TRIGGER | Сложная логика, требующая подзапросов или изменений |
-- ENUM для фиксированных статусов
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'cancelled');
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
status order_status NOT NULL DEFAULT 'pending'
);
-- CHECK для сложных условий
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
status TEXT NOT NULL,
paid_at TIMESTAMPTZ,
CHECK (
(status = 'paid' AND paid_at IS NOT NULL) OR
(status != 'paid' AND paid_at IS NULL)
)
);DEFAULT задаёт значение, которое используется, если при вставке столбец не указан или явно указано DEFAULT.
CREATE TABLE users (
id INTEGER PRIMARY KEY,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
status TEXT DEFAULT 'pending',
login_count INTEGER DEFAULT 0
);
-- Вставка с использованием DEFAULT
INSERT INTO users (id, name) VALUES (1, 'Alice');
-- is_active = TRUE, created_at = NOW(), status = 'pending', login_count = 0
-- Явное указание DEFAULT
INSERT INTO users (id, name, is_active)
VALUES (2, 'Bob', DEFAULT); -- is_active = TRUE
-- Переопределение DEFAULT
INSERT INTO users (id, name, is_active)
VALUES (3, 'Charlie', FALSE); -- is_active = FALSE-- Константы
CREATE TABLE users (
status TEXT DEFAULT 'active',
is_verified BOOLEAN DEFAULT FALSE,
login_count INTEGER DEFAULT 0
);
-- Функции
CREATE TABLE posts (
id INTEGER PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
slug TEXT DEFAULT GEN_RANDOM_UUID() -- UUID
);
-- Выражения
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
total NUMERIC(10, 2) DEFAULT 0,
tax NUMERIC(10, 2) DEFAULT (total * 0.20), -- Вычисляется при вставке
items_count INTEGER DEFAULT 0
);
-- Приведение типов
CREATE TABLE settings (
config JSONB DEFAULT '{}'::jsonb,
tags TEXT[] DEFAULT ARRAY[]::TEXT[]
);CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT DEFAULT 'unknown@example.com'
);
INSERT INTO users (id) VALUES (1); -- email = 'unknown@example.com'
INSERT INTO users (id, email) VALUES (2, NULL); -- email = NULL (DEFAULT игнорируется!)⚠️ Важно: Если явно указать
NULL, DEFAULT не применяется. Для применения DEFAULT используйте явноеDEFAULT.
-- Установить новое значение по умолчанию
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'active';
-- Удалить DEFAULT
ALTER TABLE users ALTER COLUMN status DROP DEFAULT;
-- Изменение влияет только на новые вставки
INSERT INTO users (id, name) VALUES (3, 'Charlie'); -- status = 'active'-- Все ограничения таблицы
SELECT
conname AS constraint_name,
contype AS constraint_type,
conkey AS columns,
pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'users'::regclass;
-- Расширенная информация
SELECT
c.conname AS constraint_name,
CASE c.contype
WHEN 'p' THEN 'PRIMARY KEY'
WHEN 'f' THEN 'FOREIGN KEY'
WHEN 'u' THEN 'UNIQUE'
WHEN 'c' THEN 'CHECK'
WHEN 'n' THEN 'NOT NULL'
ELSE c.contype::TEXT
END AS constraint_type,
a.attname AS column_name,
pg_get_constraintdef(c.oid) AS definition
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
WHERE c.conrelid = 'users'::regclass
ORDER BY c.contype, c.conname;
-- В psql
\d+ users -- Подробная информация о таблице
\d users -- Краткая информация-- NOT NULL (только ALTER COLUMN)
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
-- UNIQUE
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
-- PRIMARY KEY
ALTER TABLE users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
-- FOREIGN KEY
ALTER TABLE orders
ADD CONSTRAINT orders_user_fk
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- CHECK
ALTER TABLE products
ADD CONSTRAINT positive_price
CHECK (price > 0);
-- DEFAULT
ALTER TABLE users
ALTER COLUMN status SET DEFAULT 'active';-- По имени
ALTER TABLE users DROP CONSTRAINT users_email_unique;
-- NOT NULL (через ALTER COLUMN)
ALTER TABLE users ALTER COLUMN email DROP NOT NULL;
-- PRIMARY KEY
ALTER TABLE users DROP CONSTRAINT users_pkey;
-- Каскадное удаление (удалит зависимые FK)
ALTER TABLE users DROP CONSTRAINT users_pkey CASCADE;-- Временно отключить проверку FK (для миграций)
ALTER TABLE orders ALTER CONSTRAINT orders_user_fk DEFERRABLE INITIALLY DEFERRED;
-- Отключить триггеры (не ограничения!)
ALTER TABLE users DISABLE TRIGGER ALL;
ALTER TABLE users ENABLE TRIGGER ALL;
-- Отключить autovacuum для массовой вставки
ALTER TABLE users SET (autovacuum_enabled = false);-- Добавить CHECK с проверкой существующих данных
ALTER TABLE products ADD CONSTRAINT positive_price CHECK (price > 0); -- Проверит все строки
-- Добавить без проверки существующих данных (PostgreSQL 12+)
ALTER TABLE products
ADD CONSTRAINT positive_price
CHECK (price > 0) NOT VALID;
-- Позже проверить существующие данные
ALTER TABLE products VALIDATE CONSTRAINT positive_price;| Ограничение | Влияние на запись | Влияние на чтение |
|---|---|---|
| PRIMARY KEY | Низкое (индекс) | Положительное |
| FOREIGN KEY | Среднее (проверка ссылки) | Низкое |
| UNIQUE | Низкое (индекс) | Положительное |
| NOT NULL | Отсутствует | Положительное |
| CHECK | Зависит от сложности | Отсутствует |
-- ❌ Плохо: нет первичного ключа
CREATE TABLE users (
name TEXT,
email TEXT
);
-- ✅ Хорошо
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT,
email TEXT
);-- ❌ Плохо: автоматические имена
CREATE TABLE orders (
user_id INTEGER REFERENCES users(id)
);
-- Имя: orders_user_id_fkey (непонятно в логах ошибок)
-- ✅ Хорошо: явные имена
CREATE TABLE orders (
user_id INTEGER CONSTRAINT orders_user_fk REFERENCES users(id)
);
-- Имя: orders_user_fk (понятно сразу)-- ❌ Опасно: CASCADE может удалить больше, чем планировалось
CREATE TABLE orders (
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
);
-- ✅ Безопаснее: явно удалять дочерние записи
CREATE TABLE orders (
user_id INTEGER REFERENCES users(id) ON DELETE RESTRICT
);
-- Перед удалением пользователя:
DELETE FROM orders WHERE user_id = 1;
DELETE FROM users WHERE id = 1;-- ❌ Плохо: проверка в приложении
if (order.amount < 0) {
throw new Error('Invalid amount');
}
-- ✅ Хорошо: проверка на уровне БД
CREATE TABLE orders (
amount NUMERIC CHECK (amount >= 0)
);-- ❌ Плохо: допускает NULL
CREATE TABLE users (
email TEXT -- Может быть NULL!
);
-- ✅ Хорошо: явно указано NOT NULL
CREATE TABLE users (
email TEXT NOT NULL
);-- ❌ Избыточно: PRIMARY KEY уже включает NOT NULL и UNIQUE
CREATE TABLE users (
id INTEGER PRIMARY KEY NOT NULL UNIQUE
);
-- ✅ Достаточно
CREATE TABLE users (
id INTEGER PRIMARY KEY
);-- ============================================
-- Пользователи и аутентификация
-- ============================================
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE CHECK (email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
password_hash TEXT NOT NULL CHECK (LENGTH(password_hash) >= 60),
display_name TEXT NOT NULL CHECK (LENGTH(display_name) BETWEEN 1 AND 50),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_login_at TIMESTAMPTZ,
CONSTRAINT valid_display_name CHECK (display_name !~ '^[0-9]+$') -- Не только цифры
);
-- ============================================
-- Сессии
-- ============================================
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT future_expiration CHECK (expires_at > NOW())
);
-- ============================================
-- Категории товаров
-- ============================================
CREATE TABLE categories (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
parent_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
name TEXT NOT NULL UNIQUE CHECK (LENGTH(name) BETWEEN 1 AND 100),
slug TEXT NOT NULL UNIQUE CHECK (slug ~ '^[a-z0-9-]+$'),
sort_order INTEGER NOT NULL DEFAULT 0 CHECK (sort_order >= 0)
);
-- ============================================
-- Товары
-- ============================================
CREATE TABLE products (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE RESTRICT,
name TEXT NOT NULL CHECK (LENGTH(name) BETWEEN 1 AND 200),
slug TEXT NOT NULL UNIQUE CHECK (slug ~ '^[a-z0-9-]+$'),
description TEXT,
price NUMERIC(10, 2) NOT NULL CHECK (price > 0),
old_price NUMERIC(10, 2) CHECK (old_price IS NULL OR old_price > price),
stock_quantity INTEGER NOT NULL DEFAULT 0 CHECK (stock_quantity >= 0),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- Заказы
-- ============================================
CREATE TYPE order_status AS ENUM ('pending', 'confirmed', 'paid', 'shipped', 'delivered', 'cancelled', 'refunded');
CREATE TABLE orders (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status order_status NOT NULL DEFAULT 'pending',
total_amount NUMERIC(10, 2) NOT NULL CHECK (total_amount >= 0),
discount_amount NUMERIC(10, 2) NOT NULL DEFAULT 0 CHECK (discount_amount >= 0),
shipping_address TEXT NOT NULL,
notes TEXT,
confirmed_at TIMESTAMPTZ,
paid_at TIMESTAMPTZ,
shipped_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT valid_discount CHECK (discount_amount <= total_amount),
CONSTRAINT status_confirmed CHECK (
(status IN ('confirmed', 'paid', 'shipped', 'delivered') AND confirmed_at IS NOT NULL) OR
(status NOT IN ('confirmed', 'paid', 'shipped', 'delivered'))
),
CONSTRAINT status_paid CHECK (
(status IN ('paid', 'shipped', 'delivered') AND paid_at IS NOT NULL) OR
(status NOT IN ('paid', 'shipped', 'delivered'))
)
);
-- ============================================
-- Элементы заказа
-- ============================================
CREATE TABLE order_items (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(10, 2) NOT NULL CHECK (unit_price > 0),
total_price NUMERIC(10, 2) NOT NULL CHECK (total_price = quantity * unit_price)
);
-- ============================================
-- Индексы для производительности
-- ============================================
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
CREATE INDEX idx_categories_parent ON categories(parent_id);
CREATE INDEX idx_categories_slug ON categories(slug);
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_slug ON products(slug);
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_products_active ON products(is_active) WHERE is_active = TRUE;
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
CREATE INDEX idx_orders_pending ON orders(user_id, created_at) WHERE status = 'pending';
CREATE INDEX idx_order_items_order ON order_items(order_id);
CREATE INDEX idx_order_items_product ON order_items(product_id);-- Найти все нарушения CHECK ограничения
-- (после добавления NOT VALID)
SELECT * FROM table_name WHERE NOT (condition);
-- Найти сиротские записи (нарушение FK)
SELECT child.*
FROM child_table child
LEFT JOIN parent_table parent ON child.parent_id = parent.id
WHERE parent.id IS NULL;
-- Найти дубликаты UNIQUE столбца
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;Готовы проверить знания? Переходите к вопросам! 🚀
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.