Learning Platform
Урок 12.01 · 18 мин
Средний
CREATE TABLEData typesNOT NULLDEFAULTSERIALIDENTITYPRIMARY KEY

Зачем учить DDL отдельно

До сих пор мы писали почти исключительно SELECT — читали готовую e-commerce-вселенную. На реальной работе схема не падает с неба: её кто-то проектирует и каждое решение в CREATE TABLE влияет на запросы следующие пять лет. Неправильный тип у колонки — медленный индекс. Пропущенный NOT NULLWHERE x = 1 молча отсекает строки с NULL. SERIAL вместо IDENTITY — головная боль при pg_dump и миграциях между мажорными версиями.

В этом модуле собираемся прыгнуть на другую сторону баррикады и научиться проектировать таблицы. Начнём с самого базового — синтаксиса CREATE TABLE и того, что значит каждая его часть.

Анатомия CREATE TABLE

DDL
-команда CREATE TABLE объявляет новое отношение: имя, список атрибутов с типами и ограничениями. Минимальная форма выглядит так:

CREATE TABLE table_name (
  column_name data_type [column_constraints],
  ...
  [table_constraints]
);

Три уровня деталей:

  1. Тип колонки — что вообще туда можно положить (INT, TEXT, TIMESTAMPTZ, JSONB…).
  2. Column constraints — правила на одну колонку (NOT NULL, DEFAULT, UNIQUE, CHECK).
  3. Table constraints — правила на несколько колонок сразу (PRIMARY KEY (a, b), FOREIGN KEY, CHECK через несколько полей).
Структура CREATE TABLE

Каждый CREATE TABLE — это имя, набор колонок с типами и набор ограничений. Ограничения бывают на уровне колонки и на уровне таблицы.

Имяproducts
Тип отношенияобычная таблица (heap)
Колонкиid, sku, name, price_cents, in_stock
Каждая колонкаимя + тип + per-column constraints
Table constraintsPRIMARY KEY, FOREIGN KEY, CHECKЗдесь живут многоколоночные ограничения и именованные индексы

Создадим простую таблицу подписок и посмотрим, как Postgres её принимает:

Минимальная таблица subscriptions: четыре колонки, два ограничения

PostgreSQL

Что произошло: для первой подписки мы не указали plan — Postgres подставил 'free' из DEFAULT. Для обеих не указали created_at — Postgres подставил текущее время. Колонка idPRIMARY KEY, значит автоматически и NOT NULL, и UNIQUE.

Базовые типы PostgreSQL

Тип колонки — это контракт «что туда можно положить и сколько места это займёт». Полная палитра огромна; вот восемь, которые покрывают 90% случаев:

ТипКогда братьРазмер
INT / BIGINTцелые числа: счётчики, FK, цены в копейках4 / 8 байт
NUMERIC(p, s)деньги, точные дробипеременный
TEXTлюбые строки — заголовки, имена, описанияпеременный
BOOLEANфлаги1 байт
DATEдни без времени4 байта
TIMESTAMPTZмомент времени с зоной8 байт
JSONBполуструктурированные данныепеременный
UUIDглобально уникальные ID16 байт

Две тонкости:

  • VARCHAR(n) vs TEXT — в Postgres они хранятся одинаково и работают одинаково быстро. Лимит длины VARCHAR(n) — это CHECK (length(x) <= n) и больше ничего. Если лимит реально нужен — пишите явный CHECK, иначе берите TEXT. Это противоположно интуиции из MySQL.
  • TIMESTAMP vs TIMESTAMPTZ — первый хранит «дату и время как есть, без часового пояса», второй конвертит в UTC при записи и обратно в зону клиента при чтении. Для приложений с пользователями из разных стран почти всегда нужен TIMESTAMPTZ.

NOT NULL и DEFAULT — два разных инструмента

Эти два часто путают, хотя они решают разные задачи.

NOT NULL — это

ограничение целостности
: «здесь не может быть NULL». Если попытаться вставить NULL или не указать значение и нет DEFAULT — Postgres откажет с ошибкой.

DEFAULT — это значение, которое подставится, если в INSERT колонка пропущена. Это не ограничение, а «удобство». Само по себе DEFAULT ничего не запрещает: можно явно вставить NULL в created_at DEFAULT now() — если на колонке нет NOT NULL, всё пройдёт.

Часто эти два используются вместе: created_at TIMESTAMPTZ NOT NULL DEFAULT now() означает «обязательное поле, но если ты не дал значение — я подставлю текущее время».

Проверь, как ведёт себя NOT NULL без DEFAULT и DEFAULT без NOT NULL

PostgreSQL

SERIAL vs IDENTITY — что выбрать в 2026

До Postgres 10 единственный способ автоматически генерировать id был SERIAL. Это псевдотип — Postgres под капотом разворачивал его в три вещи: обычная колонка INT, отдельная SEQUENCE, и DEFAULT nextval(sequence).

В Postgres 10 появился стандарт SQL — GENERATED ... AS IDENTITY. Он делает то же самое, но синтаксис ближе к стандарту и поведение более предсказуемое:

SERIALIDENTITY
Возрастстарый, до Postgres 10Postgres 10+
Стандарт SQLнетда (SQL:2003)
INSERT ... VALUES (DEFAULT, ...)работаетработает
Явная вставка значения в idразрешена тихо, ломает sequenceблокируется ALWAYS, разрешается BY DEFAULT
Принадлежит таблицеSEQUENCE отдельный объектSEQUENCE принадлежит колонке
Поведение при DROP COLUMNsequence остаётся «висеть»sequence удаляется вместе с колонкой

Главная польза IDENTITY — режим ALWAYS. Он запрещает приложению вставлять явное значение в id, что страхует от классического бага: «вставили строку с id=100, sequence не сдвинулся, потом через nextval пришёл id=100, упало по уникальности».

SERIAL vs IDENTITY

IDENTITY ALWAYS защищает от случайной вставки явного id и ломки sequence. SERIAL такой защиты не даёт.

SERIAL (legacy)id SERIAL PRIMARY KEY
INSERT (id, ...) VALUES (100, ...)
результатвставится, sequence не сдвинетсяЧерез несколько вставок nextval вернёт 100, и INSERT упадёт по PRIMARY KEY
IDENTITY ALWAYSid INT GENERATED ALWAYS AS IDENTITY
INSERT (id, ...) VALUES (100, ...)
результатERROR: cannot insert into column idЧтобы всё-таки вставить вручную — INSERT ... OVERRIDING SYSTEM VALUE

Рекомендация: для новых таблиц — IDENTITY (обычно BY DEFAULT; ALWAYS если вы хотите параноидальной защиты).

Сравни два способа генерировать id

PostgreSQL

PRIMARY KEY — что под капотом

PRIMARY KEY выглядит как магическое слово, но никакой особой структуры за ним нет. Это синтаксический сахар над двумя более простыми вещами:

  1. NOT NULL — в первичный ключ не должен попасть NULL.
  2. UNIQUE — все значения должны быть разными.

Плюс одна бонусная штука: Postgres автоматически создаёт уникальный B-tree индекс, чтобы и NOT NULL, и UNIQUE, и быстрый поиск по id обеспечивались одной структурой.

То есть это эквивалентно:

CREATE TABLE t (
  id INT NOT NULL UNIQUE,
  ...
);
CREATE UNIQUE INDEX ON t (id);

…только записанное в одну строку и помеченное как «вот этот ключ — главный». Можно иметь только один PRIMARY KEY на таблицу, но сколько угодно UNIQUE ограничений.

Маленький нюанс: UNIQUE в стандарте SQL допускает несколько NULL (потому что NULL = NULL это NULL, а не TRUE). Это часто удивляет. В Postgres 15+ есть UNIQUE NULLS NOT DISTINCT, чтобы трактовать NULL-ы как равные и не пускать больше одного.

Superkey, candidate key, primary key, alternate key Стратегии генерации surrogate keys

Чек-лист

  • CREATE TABLE объявляет имя, типы колонок и ограничения. Ограничения бывают на колонку (NOT NULL, DEFAULT, UNIQUE, CHECK) и на таблицу (PRIMARY KEY (a,b), FOREIGN KEY).
  • Базовый набор типов в Postgres: INT/BIGINT, NUMERIC, TEXT, BOOLEAN, DATE, TIMESTAMPTZ, JSONB, UUID. VARCHAR(n) в Postgres не быстрее TEXT — берите TEXT + CHECK если нужен лимит.
  • NOT NULL — это запрет на NULL. DEFAULT — это значение «если не указано». Это разные инструменты, часто используются вместе.
  • В 2026 году для автогенерируемых id берите GENERATED ... AS IDENTITY, а не SERIAL. IDENTITY ALWAYS защищает от случайной ломки sequence.
  • PRIMARY KEY под капотом — это NOT NULL UNIQUE плюс автоматический B-tree индекс. Один на таблицу.
Проверка знанийKnowledge check
В чём практическая разница между SERIAL и GENERATED ALWAYS AS IDENTITY с точки зрения «приложение случайно вставило id=100 руками»?
ОтветAnswer
SERIAL — это обычная INT-колонка с DEFAULT nextval(seq). Если приложение пишет INSERT (id, ...) VALUES (100, ...) — оно просто кладёт 100, sequence остаётся на старом значении. Через пару вставок nextval вернёт 100, и новый INSERT упадёт с duplicate key. IDENTITY ALWAYS на ту же попытку сразу отвечает ошибкой 'cannot insert a non-DEFAULT value into column id' — баг ловится в момент его совершения, а не через неделю в production. IDENTITY BY DEFAULT ведёт себя как SERIAL и такой защиты не даёт.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какое из утверждений про NOT NULL и DEFAULT в Postgres верно?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6