CREATE TABLE создаёт таблицу: после имени в скобках перечисляют столбцы, у каждого — имя, тип и ограничения. Ограничения — PRIMARY KEY, NOT NULL, UNIQUE, CHECK, REFERENCES — проверяет сама база и не пускает строку, которая их нарушает. Ниже — таблицы клиентов и заказов магазина на PostgreSQL 17 с живым выводом psql и разбором ошибок. Все примеры можно повторить в песочнице SQL прямо в браузере.
Что обещает каждая строка CREATE TABLE
Первая таблица
Таблица клиентов: номер, почта, имя и время регистрации.
CREATE TABLE customers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE
Посмотреть, что получилось, можно командой \d в psql. Она показывает столбцы, типы, ограничения и индексы, которые база создала сама:
\d customers
Table "public.customers"
Column | Type | Collation | Nullable | Default
------------+--------------------------+-----------+----------+------------------------------
id | bigint | | not null | generated always as identity
email | text | | not null |
name | text | | not null |
created_at | timestamp with time zone | | not null | now()
Indexes:
"customers_pkey" PRIMARY KEY, btree (id)
"customers_email_key" UNIQUE CONSTRAINT, btree (email)
PRIMARY KEY и UNIQUE создали индексы customers_pkey и customers_email_key: по ним база быстро проверяет уникальность и ищет строки. Как индексы ускоряют запросы, разобрано в материале об индексах PostgreSQL.
Какой тип выбрать
| Что хранишь | Тип | Почему |
|---|---|---|
| целые числа, номера | integer, bigint | bigint для номеров, которые растут годами |
| деньги | numeric(10, 2) | точные копейки, в отличие от real и double precision |
| текст | text | длина без ограничения; varchar(n) — если предел нужен по смыслу |
| да или нет | boolean | значения true и false |
| дата | date | без времени |
| момент времени | timestamptz | хранит момент с учётом часового пояса |
| вложенные данные | jsonb | JSON с индексами и операторами поиска |
Как работать с датами и часовыми поясами, разобрано в материале о датах в PostgreSQL.
Первичный ключ: номер выдаёт база
GENERATED ALWAYS AS IDENTITY значит, что номер строки придумывает база. Вставляешь без id и получаешь номера обратно через RETURNING:
INSERT INTO customers (email, name)
VALUES ('anna@example.com', 'Анна'), ('boris@example.com', 'Борис')
RETURNING id, email;
id | email
----+-------------------
1 | anna@example.com
2 | boris@example.com
(2 rows)
INSERT 0 2
Задать номер руками ALWAYS не даст:
INSERT INTO customers (id, email, name) VALUES (10, 'vera@example.com', 'Вера');
ERROR: cannot insert a non-DEFAULT value into column "id"
DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS.
HINT: Use OVERRIDING SYSTEM VALUE to override.
Если ручные номера иногда нужны, например при переносе данных, пиши GENERATED BY DEFAULT AS IDENTITY. В старом коде вместо IDENTITY встречается serial — он тоже создаёт счётчик, но это способ, придуманный в PostgreSQL, а IDENTITY — тот же счётчик по стандарту SQL.
Ограничения в деле
NOT NULL не пускает пустое значение. Здесь забыли имя:
INSERT INTO customers (email) VALUES ('gleb@example.com');
ERROR: null value in column "name" of relation "customers" violates not-null constraint
DETAIL: Failing row contains (3, gleb@example.com, null, 2026-10-18 17:32:54.761815+00).
UNIQUE не пускает повтор. Почта anna@example.com уже есть:
INSERT INTO customers (email, name) VALUES ('anna@example.com', 'Анна Петрова');
ERROR: duplicate key value violates unique constraint "customers_email_key"
DETAIL: Key (email)=(anna@example.com) already exists.
Как найти и обойти такую ошибку в приложении, разобрано в материале о duplicate key value violates unique constraint, а вставку «добавь или обнови» — в разборе ON CONFLICT.
Обрати внимание на 3 в Failing row contains: неудачная вставка уже взяла номер у счётчика и не вернула его. Следующий клиент получает не 3, а 5 — номер 4 потратила попытка с повтором почты:
INSERT INTO customers (email, name) VALUES ('vera@example.com', 'Вера') RETURNING id;
id
----
5
(1 row)
INSERT 0 1
Дыры в номерах — нормальное поведение: счётчик гарантирует уникальность, а не непрерывность.
Внешний ключ: связь между таблицами
Таблица заказов ссылается на клиентов. REFERENCES customers (id) разрешает только номер существующего клиента, CHECK (total > 0) не пускает нулевую и отрицательную сумму, а DEFAULT заполняет статус и время:
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES customers (id) ON DELETE CASCADE,
total numeric(10, 2) NOT NULL CHECK (total > 0),
status text NOT NULL DEFAULT 'new',
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO orders (customer_id, total)
VALUES (1, 1500.00), (1, 320.50), (2, 990.00)
RETURNING id, customer_id, total, status;
id | customer_id | total | status
----+-------------+---------+--------
1 | 1 | 1500.00 | new
2 | 1 | 320.50 | new
3 | 2 | 990.00 | new
(3 rows)
INSERT 0 3
Отрицательная сумма и несуществующий клиент не проходят:
INSERT INTO orders (customer_id, total) VALUES (1, -5);
INSERT INTO orders (customer_id, total) VALUES (99, 100);
ERROR: new row for relation "orders" violates check constraint "orders_total_check"
DETAIL: Failing row contains (4, 1, -5.00, new, 2026-10-18 17:32:54.929487+00).
ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"
DETAIL: Key (customer_id)=(99) is not present in table "customers".
ON DELETE решает, что будет с заказами, когда удаляют клиента. С CASCADE они удаляются вместе с ним — у клиента 1 было два заказа:
DELETE FROM customers WHERE id = 1;
SELECT id, customer_id, total FROM orders;
DELETE 1
id | customer_id | total
----+-------------+--------
3 | 2 | 990.00
(1 row)
Без ON DELETE действует запрет: пока на клиента ссылается хоть одна строка, удалить его нельзя. Так устроена таблица платежей payments, где customer_id объявлен просто как REFERENCES customers (id):
DELETE FROM customers WHERE id = 2;
ERROR: update or delete on table "customers" violates foreign key constraint "payments_customer_id_fkey" on table "payments"
DETAIL: Key (id)=(2) is still referenced from table "payments".
CASCADE подходит данным, которые без родителя не имеют смысла, например строкам заказа. Деньги и историю платежей так не удаляют — для них оставляют запрет. Как раскладывать данные по связанным таблицам, разобрано в материале о нормализации.
Главная мысль: правило, записанное в
CREATE TABLE, нельзя забыть проверить. Приложение может ошибиться, аNOT NULL,UNIQUE,CHECKи внешний ключ база проверяет при каждой вставке.
IF NOT EXISTS, CREATE TABLE AS и временные таблицы
Повторный CREATE TABLE падает, если таблица уже есть:
ERROR: relation "customers" already exists
IF NOT EXISTS превращает ошибку в предупреждение — удобно в скриптах, которые запускают много раз:
CREATE TABLE IF NOT EXISTS customers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE
);
NOTICE: relation "customers" already exists, skipping
CREATE TABLE
Существующую таблицу эта команда не меняет, даже если описание столбцов другое. Для изменений есть ALTER TABLE.
CREATE TABLE ... AS SELECT создаёт таблицу из результата запроса — например, копию крупных заказов:
CREATE TABLE big_orders AS SELECT id, customer_id, total FROM orders WHERE total > 500;
\d big_orders
SELECT 1
Table "public.big_orders"
Column | Type | Collation | Nullable | Default
-------------+---------------+-----------+----------+---------
id | bigint | | |
customer_id | bigint | | |
total | numeric(10,2) | | |
Копируются данные и типы, но не ограничения: в big_orders нет ни первичного ключа, ни NOT NULL.
Временная таблица CREATE TEMP TABLE живёт до конца сессии. В соседнем подключении её уже нет:
SELECT * FROM draft_orders;
ERROR: relation "draft_orders" does not exist
LINE 1: SELECT * FROM draft_orders;
^
Ошибки, на которых спотыкаются
relation already exists. Таблица с таким именем уже есть. Пиши CREATE TABLE IF NOT EXISTS или удали старую, если она не нужна.
AUTO_INCREMENT из MySQL. В PostgreSQL такого слова нет:
ERROR: syntax error at or near "AUTO_INCREMENT"
LINE 1: CREATE TABLE logs (id int AUTO_INCREMENT PRIMARY KEY, messag...
^
Вместо него — GENERATED ALWAYS AS IDENTITY.
type “datetime” does not exist. Ещё одна привычка из MySQL. Для момента времени в PostgreSQL — timestamptz:
ERROR: type "datetime" does not exist
LINE 1: CREATE TABLE logs (id int PRIMARY KEY, created datetime);
^
syntax error at or near. Чаще всего забыта запятая между столбцами. Стрелка показывает слово, на котором разбор споткнулся, то есть начало следующего столбца:
ERROR: syntax error at or near "message"
LINE 1: CREATE TABLE logs (id int PRIMARY KEY message text);
^
Как читать такие сообщения, разобрано в материале о синтаксических ошибках SQL.
cannot drop table because other objects depend on it. На таблицу ссылаются внешние ключи других таблиц:
ERROR: cannot drop table customers because other objects depend on it
DETAIL: constraint orders_customer_id_fkey on table orders depends on table customers
constraint payments_customer_id_fkey on table payments depends on table customers
HINT: Use DROP ... CASCADE to drop the dependent objects too.
DROP TABLE ... CASCADE удалит сами ограничения в связанных таблицах, а не их строки. Но прежде чем удалять, проверь, что эти связи правда не нужны.
relation does not exist после создания. Таблицу создали в кавычках с заглавными буквами, например "Users", а ищут как users. Этот случай и другие причины разобраны в материале о relation does not exist.
Частые вопросы
Как удалить таблицу
DROP TABLE имя;, а в скриптах — DROP TABLE IF EXISTS имя;: если таблицы нет, будет предупреждение вместо ошибки.
NOTICE: table "draft_orders" does not exist, skipping
DROP TABLE
Чем text отличается от varchar
По скорости разницы нет: varchar(n) только проверяет длину при записи. Поэтому его берут, когда предел есть по смыслу — например, код страны из двух букв, а в остальных случаях пишут text.
Как посмотреть список таблиц
В psql — команда \dt, а описание одной таблицы — \d имя. Из любого клиента подойдёт запрос к information_schema.tables.
Где потренироваться
Все примеры работают в песочнице SQL: это PostgreSQL прямо в браузере, без установки. Создай таблицы клиентов и заказов, попробуй вставить отрицательную сумму и удалить клиента с заказами. Запросы к готовым таблицам — соединения, группировки, оконные функции — отрабатывают задачи тренажёра SQL.