Все материалы
Продуктовая аналитикагайдстарт

CREATE TABLE в SQL: как создать таблицу, типы и ограничения

CREATE TABLE в SQL: как создать таблицу в PostgreSQL, выбрать типы, задать PRIMARY KEY, NOT NULL, CHECK и внешний ключ, CREATE TABLE AS.

КейсПрактика25 сентября 2026 г.11 мин

Маркетинг каждую неделю присылает расходы по каналам в 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 или в отдельной схеме рабочего хранилища, где у вас есть права на запись.

Таблица рекламных расходов (PostgreSQL)
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 без ограничения точности.

Типы PostgreSQL, которых хватает для большинства аналитических таблиц
Что хранитьТипНа что смотреть
Идентификаторы, счётчикиinteger, bigintinteger заканчивается на 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).

Что отвечает PostgreSQL 14, когда строка нарушает ограничение
ПопыткаОшибка
day = nullERROR: null value in column "day" of relation "campaign_costs" violates not-null constraint
cost = -500ERROR: new row for relation "campaign_costs" violates check constraint "campaign_costs_cost_check"
второй раз 2026-06-01 и socialERROR: duplicate key value violates unique constraint "campaign_costs_day_channel_key" DETAIL: Key (day, channel)=(2026-06-01, social) already exists.
свой cost_id = 100ERROR: 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.

Справочник и таблица со ссылкой на него (PostgreSQL)
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 создаёт таблицу нужной структуры без строк, но ограничений тоже не добавляет.

Первые три дня витрины
daydauevents
2026-06-013270
2026-06-025383
2026-06-0376117
Дневная витрина из событий (PostgreSQL: SELECT 91). Внутренний select работает и в песочнице
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-таблица выигрывает, когда тот же набор строк используется в нескольких запросах подряд или его тяжело считать заново.

Черновая таблица на сессию (PostgreSQL). Select внутри работает и в песочнице: 1042 строки
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.

Продолжить чтение
Вся библиотека