Создание таблиц в SQL: практический пример для PostgreSQL
Разберём, как создать в PostgreSQL таблицы для каталога и заказов: связать их внешними ключами, добавить ограничения и выбрать индексы. Пример рассчитан на PostgreSQL 16 и показывает, почему одна большая таблица быстро становится проблемой.

Для небольшого магазина нужны четыре связанные таблицы: покупатели, товары, заказы и позиции заказов. Если сложить всё в одну таблицу, данные о покупателе и товаре придётся повторять в каждой строке, а исправление адреса или названия превратится в поиск всех копий. Ниже — практическая схема для PostgreSQL 16: её можно создать целиком, а затем проверить простыми запросами.
Сначала выберите структуру, а не тип поля
Перед командой CREATE TABLE решите, какие сущности хранятся отдельно и как они связаны. В примере один покупатель может оформить много заказов, а один заказ — содержать несколько товаров. Для связи заказа с товарами нужна отдельная таблица: в ней хранится и количество, и цена на момент покупки.
| Подход | Что выигрываете | Чем платите |
|---|---|---|
| Одна таблица со всеми данными | Простые выборки на старте | Повторы покупателя и товара; обновление одной сущности требует менять несколько строк |
| Отдельные таблицы и внешние ключи | Целостность связей и меньше дублирования | Для полной карточки заказа понадобится соединять таблицы |
| Естественные ключи, например email или SKU | Ключ понятен человеку и не требует отдельного идентификатора | Значение может измениться; длинный ключ усложняет ссылки и индексы |
Для учебного магазина подойдёт второй вариант с числовыми идентификаторами. Email и SKU останутся уникальными бизнес-значениями, но не будут первичными ключами: если адрес покупателя поменяется, связи заказов сохранятся.
Хорошая таблица не просто хранит значения. Она не позволяет базе принять данные, которые нарушают правила предметной области.
Создаём таблицы в PostgreSQL 16
Команды ниже рассчитаны на PostgreSQL 16. Выполните их в пустой базе данных через любой SQL-клиент с правом создавать схемы и таблицы. Скрипт создаёт отдельную схему shop, чтобы объекты примера не смешивались с таблицами приложения.
CREATE SCHEMA shop;
CREATE TABLE shop.customers (
customer_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
full_name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT customers_email_not_empty CHECK (length(btrim(email)) > 0)
);
CREATE TABLE shop.products (
product_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
price NUMERIC(12, 2) NOT NULL CHECK (price >= 0),
CONSTRAINT products_name_not_empty CHECK (length(btrim(name)) > 0)
);
CREATE TABLE shop.orders (
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES shop.customers (customer_id),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE shop.order_items (
order_id BIGINT NOT NULL REFERENCES shop.orders (order_id),
product_id BIGINT NOT NULL REFERENCES shop.products (product_id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(12, 2) NOT NULL CHECK (unit_price >= 0),
PRIMARY KEY (order_id, product_id)
);
CREATE INDEX orders_customer_created_idx
ON shop.orders (customer_id, created_at DESC);
CREATE INDEX order_items_product_idx
ON shop.order_items (product_id);
В HTML-коде примера символы > обозначают знак больше, то есть в SQL используйте обычный символ >. Если ваш клиент запускает SQL напрямую, скопируйте команды без HTML-разметки.
Зачем нужны ограничения
PRIMARY KEYзадаёт уникальный идентификатор строки и запрещает пустое значение.NOT NULLне даёт сохранить запись без обязательного поля. Оно не заменяет проверку содержимого: пустая строка сама по себе не равнаNULL.UNIQUEзапрещает повторить email или SKU. В этой схеме регистр букв в email автоматически не нормализуется: варианты с разным регистром могут считаться разными значениями.CHECKотсекает невозможные значения на уровне базы: отрицательную цену, нулевое количество и строку из пробелов.REFERENCESзапрещает ссылаться на несуществующего покупателя, заказ или товар.
Сноска. Внешний ключ — ограничение, которое связывает значение в одной таблице с существующей записью в другой. Например, orders.customer_id может указывать только на ключ из customers.
Почему цена хранится в позиции заказа
В таблице товаров есть текущая цена, а в order_items — unit_price, цена единицы на момент оформления. Это не случайный дубль. Если завтра магазин изменит стоимость товара, сумма уже созданного заказа не должна пересчитаться задним числом. При создании заказа приложение записывает актуальную цену в позицию; сама база не копирует её автоматически из таблицы товаров.
Составной первичный ключ (order_id, product_id) не позволяет добавить один и тот же товар в один заказ двумя отдельными строками. Если вашей системе важно хранить такие строки раздельно — например, из-за разных скидок или партий, — добавьте отдельный идентификатор позиции и уберите это ограничение уникальности. Это решение зависит от модели заказов, а не от синтаксиса SQL.
Как проверить, что схема создана правильно
После запуска скрипта проверьте структуру в клиенте базы или запросите каталог PostgreSQL. Затем попробуйте создать одну запись и связать её с другой: сначала добавьте покупателя, затем заказ с его идентификатором. Несуществующий идентификатор покупателя база должна отклонить из-за внешнего ключа. Аналогично, значение количества 0 должно нарушить проверку CHECK.
Индексы в конце примера не заменяют ограничения и не добавляются автоматически для всех внешних ключей. Индекс по (customer_id, created_at) может помочь получать заказы конкретного покупателя по времени, а индекс по product_id — находить позиции с товаром. Но каждый индекс занимает место и замедляет запись: оставляйте его, если он нужен запросам или обслуживанию связей, и проверяйте эффект на реальной нагрузке.
Что обычно ломается
- Повторный запуск скрипта. Создание схемы или таблицы завершится ошибкой, если объект уже существует. Не маскируйте это бездумным добавлением
IF NOT EXISTS: такая команда не исправит структуру уже созданной таблицы. Для изменения действующей схемы подготовьте миграцию. - Неправильный порядок создания. Таблица с внешним ключом должна ссылаться на уже существующую таблицу. Поэтому сначала создаются покупатели и товары, затем заказы, а после — позиции заказа.
- Дубли в уникальном поле. Второй покупатель с тем же email не сохранится. До загрузки старых данных найдите и разберите дубли; не удаляйте их автоматически, если за ними стоят разные заказы.
- Попытка удалить товар из заказа. При ссылке на товар PostgreSQL по умолчанию не даст удалить родительскую запись, пока на неё ссылаются строки заказа. Для истории покупок это часто правильнее, чем каскадно стереть позиции, но удаление каталога нужно продумать отдельно.
- Ожидание, что база заполнит цену позиции. В примере поле обязательное, но автоматической связи с текущей ценой товара нет. Приложение должно передать цену при создании позиции; правила скидок и конкурентного изменения цены требуют отдельного решения.
Когда схема перестаёт подходить
Этот пример подходит как стартовая реляционная модель, но не описывает доставку, оплату, возвраты, налоги и историю изменений товара. Если нужны несколько адресов, скидки на отдельные позиции или повтор одного SKU в заказе, расширяйте модель под конкретные правила, а не добавляйте поля наугад.
Не меняйте уже используемую таблицу вручную на рабочем сервере, если приложение продолжает записывать данные: сначала подготовьте миграцию и оцените блокировки и время выполнения на копии базы. Для новых таблиц также полезно заранее определить, кто создаёт и удаляет схему, и какие права нужны приложению.
Что делать дальше
Добавьте в тестовую базу несколько покупателей, товаров и заказов, затем проверьте три сценария: повтор SKU, заказ с неизвестным покупателем и отрицательное количество. После этого измерьте реальные запросы списка заказов и карточки товара планировщиком PostgreSQL. Если индексы не помогают, проверьте форму запроса и объём данных, прежде чем добавлять новые.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: PostgreSQL 16: CREATE TABLE, PostgreSQL 16: Constraints
Частые вопросы
Можно ли выполнить этот SQL в MySQL или SQLite?
Что делать, если скрипт сообщает, что схема уже существует?
Почему в таблице позиций хранится цена, если она уже есть у товара?
Нужен ли индекс на каждый внешний ключ?
Почему нельзя повторно запустить весь скрипт без ошибок?
Можно ли удалить товар, который уже попал в заказ?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.