CREATE TABLE в SQL: как создать таблицу, типы и ограничения
CREATE TABLE в SQL: как создать таблицу в PostgreSQL, выбрать типы, задать PRIMARY KEY, NOT NULL, CHECK и внешний ключ, CREATE TABLE AS.
Содержание статьи
Маркетинг каждую неделю присылает расходы по каналам в CSV, и вы хотите держать их рядом с событиями, чтобы считать стоимость привлечения одним запросом. Нужна таблица. Написать create table можно за минуту, но от этой минуты зависит, что будет через месяц: примет ли таблица отрицательный расход, второй раз загруженную неделю и пустую дату. Ниже — как создать таблицу в PostgreSQL так, чтобы она сама отбраковывала мусор. Все ошибки в статье настоящие: они получены на PostgreSQL 14 в копии учебной базы SQL-курса.
Коротко
create table имя (столбец тип ограничения, …)создаёт пустую таблицу с заданной структурой.- Деньги храните в
numeric, моменты времени — вtimestamptz, счётчики — вbigint. - Ограничения
primary key,not null,unique,checkиreferencesотклоняют плохую строку при вставке, с понятной ошибкой. - Для автоматического номера строки используйте
generated always as identity, а неserial. create table … as selectкопирует данные и типы, но не копирует ни одного ограничения, индекса или значения по умолчанию.- Черновые таблицы на одну сессию делайте через
create temp table: они исчезнут сами.
Что делает CREATE TABLE и какой у него синтаксис
CREATE TABLE — команда SQL, которая создаёт в базе новую пустую таблицу: задаёт её имя, список столбцов, тип каждого столбца и правила для значений. Данных после неё ещё нет, строки добавляются отдельно через insert или сразу из запроса через create table … as select.
Каждый столбец описывается одной строкой: имя, тип и необязательные ограничения. Ограничения на несколько столбцов сразу, например уникальность пары «день + канал», пишутся отдельной строкой в конце списка.
Учебная песочница SQL-курса принимает только select и with, поэтому команды из статьи выполняйте в своём PostgreSQL: локально, в Docker или в отдельной схеме рабочего хранилища, где у вас есть права на запись.
create table campaign_costs (
cost_id bigint generated always as identity primary key,
day date not null,
channel text not null,
cost numeric(12,2) not null check (cost >= 0),
currency text not null default 'RUB',
loaded_at timestamptz not null default now(),
unique (day, channel)
);Какой тип данных выбрать для столбца
Тип решает, что в столбец вообще можно положить и как значения будут сравниваться и суммироваться. Ошибку в типе дорого исправлять потом: придётся менять тип на заполненной таблице и чинить все запросы, которые на него опирались. Подробно о приведении типов — в статье про CAST.
Самый частый спор — деньги. double precision хранит числа в двоичном виде приближённо: 0.1 + 0.2 в нём равно 0.30000000000000004, а сравнение с 0.3 возвращает false. В numeric та же сумма ровно 0.3. В учебной базе столбец payments.amount имеет тип double precision, и сумма всех оплат 30639 выходит точной только потому, что суммы целые: 19, 29 и 39. С копейками и курсами валют такое везение заканчивается.
numeric(12,2) ещё и округляет при записи. Вставка расхода 199.999 сохранилась как 200.00 без предупреждения. Если важны дробные копейки из выгрузки, задайте больше знаков после запятой или храните numeric без ограничения точности.
| Что хранить | Тип | На что смотреть |
|---|---|---|
| Идентификаторы, счётчики | integer, bigint | integer заканчивается на 2 147 483 647, дальше «integer out of range». Для событий и id сразу берите bigint |
| Деньги | numeric(12,2) | точная десятичная арифметика; double precision — только для мер, где погрешность допустима |
| Доли, коэффициенты модели | double precision | быстро и компактно, но не сравнивайте на точное равенство |
| Строки | text | в PostgreSQL varchar(n) не быстрее; длину, если нужна, проверяйте через check |
| День без времени | date | подходит для дат оплаты, регистрации, дневных витрин |
| Момент события | timestamptz | хранит абсолютное время; timestamp молча отбрасывает смещение: 10:00+03 превращается в 10:00 |
| Флаги | boolean | три состояния: true, false и NULL; если NULL не нужен, добавьте not null default false |
| Полуструктурированные атрибуты | jsonb | удобно для параметров событий, но фильтры по ключам медленнее и сложнее, чем по столбцам |
Как работают ограничения: PRIMARY KEY, NOT NULL, UNIQUE, CHECK, DEFAULT
Ограничение — правило, которое база проверяет при каждой вставке и изменении. Строка, которая его нарушает, не записывается, а вся команда отменяется. Это дешевле, чем искать тот же мусор в отчёте через неделю.
primary key означает «уникально и не пусто»: по этому столбцу строку можно однозначно найти. not null запрещает пустые значения. unique запрещает повторы, в том числе по комбинации столбцов: в таблице выше не может быть двух строк с одинаковыми днём и каналом, значит повторно загруженная неделя упадёт, а не удвоит расходы. check проверяет произвольное условие над строкой. default подставляет значение, если столбец не указан во вставке: валюта станет RUB, время загрузки — текущим.
Имена ограничений PostgreSQL придумывает сам по шаблону таблица_столбец_тип: campaign_costs_pkey, campaign_costs_day_channel_key, campaign_costs_cost_check. Именно их вы увидите в тексте ошибки. Если хочется понятнее, задайте имя явно: constraint cost_not_negative check (cost >= 0).
| Попытка | Ошибка |
|---|---|
day = null | ERROR: null value in column "day" of relation "campaign_costs" violates not-null constraint |
cost = -500 | ERROR: new row for relation "campaign_costs" violates check constraint "campaign_costs_cost_check" |
| второй раз 2026-06-01 и social | ERROR: duplicate key value violates unique constraint "campaign_costs_day_channel_key" DETAIL: Key (day, channel)=(2026-06-01, social) already exists. |
свой cost_id = 100 | ERROR: cannot insert a non-DEFAULT value into column "cost_id" |
Как связать таблицы через REFERENCES
Внешний ключ говорит: значение в этом столбце должно существовать в другой таблице. Классический пример — справочник тарифов и таблица смены тарифов. Подробно о ключах и о том, зачем они нужны в модели данных, — в статье про первичный и внешний ключ.
Внешний ключ работает в обе стороны. Вставка несуществующего тарифа падает: «ERROR: insert or update on table "plan_changes" violates foreign key constraint "plan_changes_plan_fkey"» с пояснением Key (plan)=(enterprise) is not present in table "plans". Удаление тарифа, на который уже есть ссылки, тоже падает: «update or delete on table "plans" violates foreign key constraint … on table "plan_changes"».
Ссылаться можно только на столбец с primary key или unique. В учебной базе у таблицы users нет первичного ключа, и попытка создать user_flags со ссылкой на users (user_id) заканчивается ошибкой «there is no unique constraint matching given keys for referenced table "users"». Сначала ключ нужно добавить на саму users — это уже команда ALTER TABLE.
create table plans (
plan text primary key,
monthly_price numeric(10,2) not null check (monthly_price > 0)
);
create table plan_changes (
change_id bigint generated always as identity primary key,
user_id int not null,
plan text not null references plans (plan),
changed_at timestamp not null
);Чем identity отличается от serial
Оба способа дают столбцу автоматически растущий номер. serial — старая сокращённая запись: PostgreSQL создаёт последовательность и ставит default nextval(…). Default можно перебить явным значением, и последовательность об этом не узнает. В таблице с id serial primary key вставка строки с id = 1 вручную прошла, а следующая обычная вставка упала: «duplicate key value violates unique constraint "notes_serial_pkey"», потому что последовательность тоже выдала 1.
generated always as identity — стандартный синтаксис SQL. Явное значение он отклоняет сразу, с подсказкой Use OVERRIDING SYSTEM VALUE to override, так что рассинхронизация не случается молча. Вариант generated by default as identity ведёт себя как serial и нужен, когда вы переносите таблицу с готовыми номерами.
Номера из identity не обязаны идти подряд. После трёх упавших вставок в campaign_costs следующая успешная строка получила cost_id = 6: неудачные попытки тоже потратили номера. Для ключа это нормально, а вот считать число строк по максимальному id нельзя.
Как создать таблицу из запроса: CREATE TABLE AS
create table … as select создаёт таблицу и сразу наполняет её результатом запроса. Так удобно сохранить промежуточный расчёт или быстро собрать дневную витрину. Из 35 341 события учебной базы получилась таблица на 91 строку — по одной на каждый день с 1 июня по 30 августа.
Типы столбцов берутся из запроса: count(*) в PostgreSQL возвращает bigint, поэтому dau и events стали bigint. Имена — из алиасов, поэтому их стоит задавать всегда: без алиаса столбец будет называться count.
Чего create table as не делает: не копирует ограничения, индексы, значения по умолчанию и даже not null. Копия campaign_costs, сделанная так «на всякий случай», потеряла первичный ключ, identity, check и default — все шесть столбцов стали необязательными. В витрину daily_metrics повторный день 2026-06-01 вставился без ошибки. Если нужна та же структура со всеми правилами, создайте пустую таблицу через create table новая (like старая including all) и перелейте данные через insert … select. Вариант with no data создаёт таблицу нужной структуры без строк, но ограничений тоже не добавляет.
| day | dau | events |
|---|---|---|
| 2026-06-01 | 32 | 70 |
| 2026-06-02 | 53 | 83 |
| 2026-06-03 | 76 | 117 |
create table daily_metrics as
select
event_time::date as day,
count(distinct user_id) as dau,
count(*) as events
from events
group by 1;
-- потом, если витрина живёт дольше одного вечера:
alter table daily_metrics add primary key (day);Что делает IF NOT EXISTS
Повторный create table с тем же именем падает: «ERROR: relation "campaign_costs" already exists». В скриптах, которые запускаются много раз, пишут create table if not exists: вместо ошибки будет NOTICE: relation "campaign_costs" already exists, skipping, и скрипт пойдёт дальше.
Ловушка в том, что PostgreSQL проверяет только имя. Команда create table if not exists campaign_costs (id int) завершилась успехом, хотя существующая таблица устроена совсем иначе. Если структура поменялась, if not exists это скроет, и упадёт уже следующая вставка. Изменения структуры делаются через ALTER TABLE, а не повторным созданием.
Временные таблицы для черновых расчётов
Аналитику часто нужна таблица на один вечер: список пользователей, активных в июне, чтобы несколько раз соединить его с оплатами и подписками. Для этого есть create temp table. Такая таблица видна только вашей сессии и удаляется при отключении.
Временная таблица из запроса ниже содержит 1042 пользователя и лежит в служебной схеме pg_temp_4. Из другого подключения запрос к ней падает: «relation "june_active" does not exist». Два коллеги могут одновременно создать временные таблицы с одним именем и не помешать друг другу.
Временные таблицы не заменяют CTE, когда промежуточный результат нужен один раз: with проще читать и ничего не оставляет после себя. Temp-таблица выигрывает, когда тот же набор строк используется в нескольких запросах подряд или его тяжело считать заново.
create temp table june_active as
select distinct user_id
from events
where event_time >= '2026-06-01'
and event_time < '2026-07-01';Как называть таблицы и столбцы
PostgreSQL приводит имена без кавычек к нижнему регистру. Таблица, созданная как "CampaignCosts" в кавычках, потом не находится запросом select * from CampaignCosts: «relation "campaigncosts" does not exist». Кавычки придётся писать везде и всем, кто будет пользоваться таблицей. Проще сразу выбрать snake_case без кавычек.
- Строчные латинские буквы и подчёркивания:
campaign_costs,daily_metrics. - Имя таблицы отвечает на вопрос «одна строка — это что»:
plan_changes— одна смена тарифа. - Одинаковые сущности называются одинаково во всех таблицах: если в
usersестьuser_id, во всех остальных тожеuser_id, а неuidилиclient. - Суффиксы подсказывают тип:
_atдля моментов времени,_dateилиdayдля дат,is_для флагов. - Черновые и личные таблицы — в отдельной схеме или с префиксом, чтобы их не приняли за продовые.
Частые ошибки при создании таблицы
Ограничение на уже существующие данные ставится только тогда, когда они ему соответствуют. Перед тем как объявлять unique (user_id) для подписок, посчитайте дубли: select count(*) - count(distinct user_id) from subscriptions. В учебной базе это 0, значит ключ встанет. Этот запрос выполняется и в песочнице курса.
- Деньги в
double precisionилиreal— суммы расходятся на копейки при сравнении и округлении. timestampвместоtimestamptzдля событий из разных часовых поясов — смещение теряется при записи.- Нет ключа или
uniqueна естественную комбинацию столбцов — повторная загрузка удваивает данные. create table asкак резервная копия с расчётом на те же правила — ни одно ограничение не переносится.serialплюс ручная вставка номеров — следующая автоматическая вставка падает на дубле.- Имена в кавычках и с заглавными буквами — каждый запрос к таблице превращается в угадывание регистра.
Чеклист перед CREATE TABLE
- Сформулировано, что такое одна строка таблицы, и есть ключ, который её однозначно определяет.
- У каждого столбца выбран тип: деньги —
numeric, время —timestamptz, идентификаторы —bigint. not nullстоит везде, где пустое значение означает ошибку загрузки.- Есть
checkдля очевидных инвариантов: неотрицательные суммы, допустимые статусы. - Внешние ключи ссылаются на столбцы с
primary keyилиunique. - Для витрины из
create table asпотом добавлены ключ и нужные ограничения. - Черновые расчёты сделаны во временных таблицах или в своей схеме.
Что почитать дальше
Запустите в песочнице курса select из примера с дневной витриной и проверьте, какой столбец мог бы стать ключом: сгруппируйте результат по нему и убедитесь, что count(*) нигде не больше единицы. Сами create table выполняйте в своём PostgreSQL.
Материалы по теме

ALTER TABLE в SQL: добавить, изменить и удалить столбец
ALTER TABLE в PostgreSQL: добавить, переименовать и удалить столбец, изменить тип через USING, добавить NOT NULL, DEFAULT и ограничения.

UPDATE в SQL: синтаксис, примеры и обновление по другой таблице
UPDATE в SQL и PostgreSQL: обновление одного и нескольких полей, CASE, UPDATE … FROM по другой таблице, RETURNING и безопасный порядок через транзакцию.

DELETE в SQL: как удалить строки и не потерять лишнее
DELETE в SQL и PostgreSQL: удаление по условию, DELETE … USING, удаление дублей, RETURNING, разница DELETE, TRUNCATE и DROP, внешние ключи.