CREATE TABLE — это команда, которая создаёт новую таблицу в базе данных.
Если говорить совсем просто, она отвечает на вопрос:
Какие данные мы хотим хранить и по каким правилам?
Таблица в базе похожа на хорошо продуманную анкету. До того как люди начнут её заполнять, нужно решить:
- какие поля в ней будут;
- какие поля обязательные;
- где можно хранить текст, а где только числа;
- какие значения должны быть уникальными;
- какие строки связаны с другими таблицами;
- что делать, если пользователь что-то не указал.
Например, если мы делаем таблицу пользователей, нам заранее нужно решить: у пользователя будет email, имя, дата регистрации, уникальный id. Email должен быть обязательным и не должен повторяться. Дата регистрации должна подставляться автоматически.
Вот всё это и описывает CREATE TABLE.
Пока таблицы нет, в неё нельзя вставить данные через INSERT и из неё нельзя ничего прочитать через SELECT. Поэтому CREATE TABLE — одна из первых команд, с которой начинается любой реальный проект.
Зачем нужен CREATE TABLE
База данных — это не просто «коробка, куда складывают информацию». Хорошая база сразу помогает хранить данные аккуратно.
Плохо спроектированная таблица быстро превращается в кладовку без полок: вроде всё где-то лежит, но найти, проверить и использовать это больно.
Например, если никак не ограничить данные, в таблице может появиться такое:
Сразу несколько проблем:
- у одного пользователя нет email;
- email повторяется;
- возраст отрицательный;
- возраст записан текстом;
- email может быть в неправильном формате.
Часть таких проблем можно ловить в коде приложения. Но надёжнее заложить базовые правила прямо в таблицу. Тогда база сама не пропустит мусор.
Хорошо продуманная таблица:
- хранит числа как числа, даты как даты, текст как текст;
- не даёт записать
NULL там, где значение обязательно;
- следит, чтобы уникальные значения не повторялись;
- проверяет простые условия через
CHECK;
- связывает строки между таблицами через
FOREIGN KEY;
- подставляет значения по умолчанию через
DEFAULT.
Именно поэтому CREATE TABLE — не просто техническая команда. Это момент, когда вы проектируете порядок в данных.
Базовый синтаксис CREATE TABLE
Посмотрим на пример:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Здесь мы создаём таблицу users с четырьмя колонками:
id — уникальный идентификатор пользователя;
email — почта пользователя;
display_name — отображаемое имя;
created_at — дата и время создания строки.
Разберём по частям.
CREATE TABLE users (
...
);
Так начинается создание таблицы. После CREATE TABLE указываем имя таблицы, а внутри скобок перечисляем колонки и правила для них.
id SERIAL PRIMARY KEY
Колонка id получает тип SERIAL. В PostgreSQL это удобный способ сказать: «пусть база сама выдаёт значения 1, 2, 3, 4 и так далее».
PRIMARY KEY означает, что это главный идентификатор строки. Значение должно быть уникальным и не должно быть NULL.
email VARCHAR(255) UNIQUE NOT NULL
Колонка email хранит текст длиной до 255 символов.
UNIQUE запрещает повторения. Два пользователя с одним и тем же email не пройдут.
NOT NULL говорит, что email обязателен. Нельзя создать пользователя без email.
display_name VARCHAR(100) NOT NULL
Колонка display_name тоже текстовая и тоже обязательная.
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
Колонка created_at хранит дату и время. Тип TIMESTAMPTZ в PostgreSQL удобен для событий во времени: регистраций, заказов, оплат, сообщений.
DEFAULT NOW() означает: если при вставке строки мы не указали created_at, база сама поставит текущий момент.
Запятые и точка с запятой
Внутри CREATE TABLE колонки разделяются запятыми:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
price NUMERIC(12, 2) NOT NULL
);
После последней колонки запятая обычно не ставится.
В конце всей команды ставится точка с запятой:
);
Это как точка в конце предложения: команда закончилась, можно выполнять.
Пример из жизни: таблица записей на услугу
Допустим, мы делаем небольшой сервис для записи клиентов. Нужно хранить записи: кто пришёл, какой телефон оставил, какую услугу выбрал и на какое время записался.
Создадим таблицу:
CREATE TABLE appointments (
id SERIAL PRIMARY KEY,
client_name VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
service VARCHAR(50) NOT NULL,
starts_at TIMESTAMPTZ NOT NULL,
notes TEXT
);
Что здесь важно:
client_name — обязательное имя клиента;
phone — обязательный телефон;
service — обязательное название услуги;
starts_at — обязательные дата и время записи;
notes — необязательная заметка.
У notes нет NOT NULL, значит это поле можно не заполнять. Если заметки нет, в колонке будет NULL.
Теперь можно вставить запись:
INSERT INTO appointments (client_name, phone, service, starts_at)
VALUES ('Anna', '+79991234567', 'haircut', '2026-05-10 14:00:00+03');
Результат в таблице будет примерно таким:
| id |
client_name |
phone |
service |
starts_at |
notes |
| 1 |
Anna |
+79991234567 |
haircut |
2026-05-10 14:00:00+03 |
NULL |
Мы не передавали id, потому что SERIAL сделал это сам.
Мы не передавали notes, потому что заметка необязательная.
Так таблица работает как аккуратный бланк: обязательные поля требует, необязательные разрешает пропустить.
Колонка — это имя, тип и правила
Внутри таблицы каждая колонка обычно состоит из трёх частей:
column_name data_type constraints
Например:
email VARCHAR(255) UNIQUE NOT NULL
Здесь:
email — имя колонки;
VARCHAR(255) — тип данных;
UNIQUE NOT NULL — ограничения.
Можно читать эту строку почти обычным языком:
Создай колонку email, в ней будет текст до 255 символов, значение не должно повторяться и не может быть пустым.
Чем лучше вы описали колонки на этапе создания таблицы, тем меньше хаоса получите позже.
Основные типы данных
Тип данных отвечает за то, что можно хранить в колонке.
Если колонка числовая, в неё нельзя спокойно положить произвольный текст. Если колонка с датой, с ней можно делать операции как с датой: сравнивать, сортировать, считать интервалы.
Вот короткая шпаргалка по частым типам в PostgreSQL.
| Что храним |
Тип |
Пример |
| Целое число |
INTEGER |
количество товаров |
| Большое целое число |
BIGINT |
крупные счётчики |
| Небольшое целое число |
SMALLINT |
возраст, рейтинг от 1 до 5 |
| Деньги и точные дробные числа |
NUMERIC(12, 2) |
цена, сумма заказа |
| Неточные дробные числа |
REAL, DOUBLE PRECISION |
измерения, расчёты, координаты |
| Текст |
TEXT, VARCHAR(n) |
имя, описание, комментарий |
| Дата |
DATE |
день рождения |
| Дата и время |
TIMESTAMPTZ |
дата регистрации, время оплаты |
| Логическое значение |
BOOLEAN |
активен пользователь или нет |
| UUID |
UUID |
непоследовательный уникальный идентификатор |
Деньги — только NUMERIC
Для денег лучше использовать точный тип:
price NUMERIC(12, 2) NOT NULL
NUMERIC(12, 2) означает: всего до 12 цифр, из них 2 после запятой.
Например:
9999999999.99
Почему не FLOAT?
Потому что FLOAT, REAL и DOUBLE PRECISION хранят числа приблизительно. Это нормально для научных расчётов, графики или координат, но плохо для денег.
Классический пример проблемы:
SELECT 0.1::FLOAT + 0.2::FLOAT AS result;
Результат может оказаться не ровно 0.3, а чем-то вроде:
0.30000000000000004
Для цены товара, баланса пользователя или суммы заказа такое поведение не подходит.
Поэтому правило простое:
Деньги, цены, комиссии, балансы — через NUMERIC, а не через FLOAT.
Дата и время — TIMESTAMPTZ
Для событий во времени в PostgreSQL чаще всего выбирают TIMESTAMPTZ.
Например:
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
Так удобно хранить:
- дату регистрации;
- дату оплаты;
- дату создания заказа;
- дату отправки сообщения;
- момент изменения статуса.
Важно понимать: TIMESTAMPTZ не хранит «название часового пояса» рядом с каждой строкой. PostgreSQL приводит момент времени к единому виду и показывает его с учётом настроек текущей сессии.
Практический смысл такой: если событие произошло в конкретный момент времени, TIMESTAMPTZ обычно безопаснее, чем «голое» TIMESTAMP.
А вот если вам нужна просто дата без времени, например день рождения, используйте DATE:
birth_date DATE
День рождения не зависит от часового пояса. Это просто календарная дата.
TEXT или VARCHAR
В PostgreSQL TEXT и VARCHAR(n) очень похожи по производительности.
Например:
description TEXT
подходит для описаний, комментариев и длинных текстов.
А вот VARCHAR(255) нужен, когда у ограничения есть смысл:
email VARCHAR(255) NOT NULL
Если бизнес-правило говорит, что поле не должно быть длиннее определённого размера, можно использовать VARCHAR(n).
Но не стоит ставить ограничения просто «на всякий случай». Если текст может быть любой разумной длины, в PostgreSQL часто спокойно используют TEXT.
Ограничения: правила, которые защищают данные
Ограничения, или constraints, — это правила, которые база проверяет при INSERT и UPDATE.
Если правило нарушено, база не запишет строку.
Посмотрим на пример таблицы заказов:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
number TEXT NOT NULL UNIQUE,
customer_id INT NOT NULL REFERENCES customers(id),
amount NUMERIC(12, 2) NOT NULL CHECK (amount > 0),
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled'))
);
Здесь сразу несколько правил.
PRIMARY KEY
PRIMARY KEY — главный идентификатор строки.
id SERIAL PRIMARY KEY
Он должен быть:
- уникальным;
- обязательным;
- стабильным.
По PRIMARY KEY удобно ссылаться на строку из других таблиц. Например, заказ может ссылаться на пользователя через user_id.
Обычно в таблице один PRIMARY KEY.
NOT NULL
NOT NULL запрещает пустое значение.
email TEXT NOT NULL
Если попробовать вставить строку без email, база выдаст ошибку.
Это полезно для всего, без чего строка теряет смысл:
- email пользователя;
- сумма заказа;
- дата создания;
- название товара;
- статус платежа.
Если поле обязательно по смыслу, лучше написать NOT NULL сразу.
UNIQUE
UNIQUE запрещает повторяющиеся значения.
email TEXT UNIQUE NOT NULL
Так база не даст создать двух пользователей с одной и той же почтой.
Ещё примеры:
number TEXT UNIQUE NOT NULL
для номера заказа;
slug TEXT UNIQUE NOT NULL
для адреса страницы;
phone TEXT UNIQUE
для телефона, если по правилам бизнеса он должен быть уникальным.
UNIQUE — это не просто украшение. Это защита от дублей, которые потом очень неприятно чистить.
DEFAULT
DEFAULT подставляет значение автоматически, если его не указали в INSERT.
Например:
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
Теперь при вставке строки не нужно каждый раз передавать дату создания.
INSERT INTO users (email, display_name)
VALUES ('anna@example.com', 'Anna');
created_at база заполнит сама.
Другой пример:
status TEXT NOT NULL DEFAULT 'pending'
Если при создании заказа статус не указали, заказ станет pending.
Это удобно: меньше ручной работы и меньше забытых значений.
CHECK
CHECK проверяет условие для строки.
Например, сумма заказа должна быть больше нуля:
amount NUMERIC(12, 2) NOT NULL CHECK (amount > 0)
Статус должен быть одним из разрешённых:
status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled'))
Возраст не должен быть отрицательным:
age INT CHECK (age >= 0)
CHECK помогает перенести простую здравую логику прямо в базу.
Без CHECK можно случайно получить заказ на отрицательную сумму или пользователя с возрастом -5.
С CHECK база просто не примет такую строку.
FOREIGN KEY: связь между таблицами
FOREIGN KEY нужен, чтобы связать одну таблицу с другой.
Например, есть таблица пользователей:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
);
И есть таблица комментариев:
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Колонка user_id ссылается на users(id).
Это значит: комментарий не может принадлежать пользователю, которого нет.
Если попробовать вставить комментарий с несуществующим user_id, база не даст этого сделать:
INSERT INTO comments (user_id, body)
VALUES (999, 'Nice post');
Если пользователя с id = 999 нет, будет ошибка.
Это защищает от «осиротевших» данных: комментариев без пользователя, заказов без клиента, платежей без заказа.
Что такое ON DELETE
Когда есть связь между таблицами, возникает вопрос: что делать с дочерними строками, если удалить родительскую?
Например, есть пользователь и его комментарии.
Можно выбрать разное поведение.
ON DELETE CASCADE
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
body TEXT NOT NULL
);
ON DELETE CASCADE означает: если удалить пользователя, его комментарии удалятся автоматически.
Это удобно, когда дочерние данные не имеют смысла без родителя.
Например:
- позиции заказа без заказа;
- временные токены пользователя;
- черновики, принадлежащие удалённому объекту.
Но с CASCADE нужно быть осторожным: одно удаление может потянуть за собой много других строк.
ON DELETE RESTRICT
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
body TEXT NOT NULL
);
ON DELETE RESTRICT запрещает удалять пользователя, пока у него есть комментарии.
Это безопасный вариант, когда вы не хотите случайно потерять связанные данные.
ON DELETE SET NULL
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE SET NULL,
body TEXT NOT NULL
);
ON DELETE SET NULL означает: если удалить пользователя, комментарий останется, но user_id станет NULL.
Важно: для этого колонка user_id должна разрешать NULL. Если написать user_id INT NOT NULL, то SET NULL не сможет сработать.
Такой вариант подходит, когда сам комментарий можно оставить, но связь с пользователем уже неизвестна или не нужна.
FOREIGN KEY не создаёт индекс автоматически
Это частая ловушка новичков.
В PostgreSQL внешний ключ на дочерней колонке не создаёт индекс автоматически.
Например:
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
body TEXT NOT NULL
);
Связь есть. База проверяет, что user_id существует в users.
Но индекса на comments(user_id) автоматически нет.
Если комментариев много, запросы по user_id могут работать медленнее:
SELECT *
FROM comments
WHERE user_id = 10;
Поэтому индекс часто добавляют отдельно:
CREATE INDEX comments_user_id_idx ON comments(user_id);
Особенно это важно, если вы часто:
- ищете дочерние строки по родителю;
- удаляете родительские строки;
- делаете
JOIN по внешнему ключу;
- проверяете связанные данные в больших таблицах.
Запомните коротко:
FOREIGN KEY отвечает за правильность данных, а индекс — за скорость поиска.
Это разные вещи.
Полный пример: пользователи и заказы
Соберём небольшой пример ближе к реальному проекту.
Есть пользователи:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Есть заказы:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
number TEXT NOT NULL UNIQUE,
amount NUMERIC(12, 2) NOT NULL CHECK (amount > 0),
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'cancelled')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
И добавим индекс на внешний ключ:
CREATE INDEX orders_user_id_idx ON orders(user_id);
Теперь база защищает нас от многих ошибок:
- нельзя создать заказ без пользователя;
- нельзя создать заказ на отрицательную сумму;
- нельзя записать неизвестный статус;
- нельзя повторить номер заказа;
- дата создания подставится сама;
- заказы пользователя можно быстрее находить по
user_id.
Вот так CREATE TABLE превращается из простой команды в фундамент нормальной структуры данных.
Имена таблиц и колонок
Имена лучше писать в стиле snake_case:
CREATE TABLE user_profiles (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
birth_date DATE
);
Такой стиль удобен:
- всё в нижнем регистре;
- слова разделены подчёркиванием;
- не нужны кавычки;
- легко читать в запросах.
Плохая идея — имена с пробелами и заглавными буквами:
CREATE TABLE "User Profiles" (
"User ID" INT
);
Формально это работает, но потом в каждом запросе придётся помнить кавычки и точное написание:
SELECT "User ID"
FROM "User Profiles";
Это быстро надоедает и создаёт лишние ошибки.
Лучше сразу писать просто:
SELECT user_id
FROM user_profiles;
NULL: разрешать или запрещать
Каждая колонка либо разрешает NULL, либо нет.
Если вы пишете:
phone TEXT
значение можно не указывать, и тогда будет NULL.
Если пишете:
phone TEXT NOT NULL
значение обязательно.
NULL — это не ноль и не пустая строка. Это «значение отсутствует» или «значение неизвестно».
Иногда NULL полезен. Например, у записи может не быть заметки:
notes TEXT
Но если почти все важные поля nullable, схема становится слабой. База перестаёт защищать данные, а код приложения начинает постоянно проверять: «а тут точно что-то есть?»
Хорошее правило:
- если поле обязательно по смыслу — ставьте
NOT NULL;
- если поле действительно может отсутствовать — разрешайте
NULL;
- если колонка часто заполнена только для части строк, возможно, стоит подумать об отдельной таблице.
Например, не всегда хорошо делать одну огромную таблицу пользователей с десятками необязательных полей. Иногда лучше вынести дополнительные данные в user_profiles, user_settings или другую связанную таблицу.
Частые ошибки новичков
Ошибка 1. Хранить деньги во FLOAT
Плохо:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
price FLOAT NOT NULL
);
Для денег так делать не стоит. FLOAT хранит приблизительные значения, а деньги требуют точности.
Лучше:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
price NUMERIC(12, 2) NOT NULL
);
Ошибка 2. Не ставить NOT NULL на обязательные поля
Плохо:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT,
display_name TEXT
);
Так база разрешит пользователя без email и имени.
Лучше:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL,
display_name TEXT NOT NULL
);
Если поле обязательно для нормальной работы приложения, это должно быть видно в схеме.
Ошибка 3. Забыть UNIQUE для email или номера заказа
Плохо:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL
);
Так можно создать двух пользователей с одним email.
Лучше:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);
То же самое с номерами заказов, slug страниц, внешними идентификаторами платежей и другими значениями, которые не должны повторяться.
Ошибка 4. Хранить связь без FOREIGN KEY
Плохо:
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
body TEXT NOT NULL
);
На вид всё нормально: есть user_id, значит комментарий связан с пользователем.
Но база этого не знает. Она спокойно примет комментарий с user_id, которого нет в таблице пользователей.
Лучше:
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
body TEXT NOT NULL
);
Теперь связь — не просто договорённость в голове разработчика, а настоящее правило базы данных.
Ошибка 5. Думать, что FOREIGN KEY сам создаст индекс
Плохо думать так:
user_id INT NOT NULL REFERENCES users(id)
и считать, что индекс появился автоматически.
В PostgreSQL индекс на дочернюю колонку нужно добавить отдельно:
CREATE INDEX comments_user_id_idx ON comments(user_id);
FOREIGN KEY защищает данные, но не всегда ускоряет ваши запросы.
Ошибка 6. Использовать неудобные имена
Плохо:
CREATE TABLE "Order Items" (
"Order ID" INT
);
Лучше:
CREATE TABLE order_items (
order_id INT
);
Чем проще имена, тем приятнее потом писать SELECT, JOIN, UPDATE и любые другие запросы.
Ошибка 7. Забыть DEFAULT для created_at
Плохо:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL
);
Теперь при каждом INSERT нужно помнить про created_at.
Лучше:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Теперь база сама подставит дату создания.
Ошибка 8. Делать слишком много nullable-колонок
Плохо:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT,
company_name TEXT,
company_tax_id TEXT,
student_group TEXT,
teacher_subject TEXT
);
Если в одной таблице смешиваются обычные пользователи, компании, студенты и преподаватели, половина колонок будет постоянно NULL.
Это сигнал, что модель данных, возможно, стоит разделить на несколько таблиц.
Например:
users;
company_profiles;
student_profiles;
teacher_profiles.
Не всегда нужно дробить сразу, но если таблица превращается в поле из NULL, стоит остановиться и подумать.
Как читать CREATE TABLE глазами разработчика
Когда видите CREATE TABLE, не пытайтесь читать его как набор случайных слов.
Читайте по смыслу:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id),
amount NUMERIC(12, 2) NOT NULL CHECK (amount > 0),
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Это можно перевести так:
Создаём таблицу заказов.
У каждого заказа есть уникальный id.
Каждый заказ принадлежит существующему пользователю.
Сумма заказа обязательна и должна быть больше нуля.
Статус обязателен, по умолчанию pending.
Дата создания обязательна, по умолчанию текущий момент.
Вот это и есть хорошее понимание SQL: вы видите не просто синтаксис, а правила предметной области.
Главное
CREATE TABLE создаёт таблицу и описывает её структуру: какие в ней будут колонки, какие у них типы и какие правила база должна проверять.
Таблица — это не просто место для хранения. Это договор с базой данных: какие значения допустимы, какие обязательны, какие должны быть уникальными и как строки связаны между собой.
Самое важное, что нужно запомнить:
CREATE TABLE создаёт новую таблицу;
- каждая колонка имеет имя, тип данных и дополнительные ограничения;
PRIMARY KEY задаёт главный идентификатор строки;
NOT NULL запрещает пустые значения;
UNIQUE запрещает дубли;
CHECK проверяет условие;
DEFAULT подставляет значение автоматически;
FOREIGN KEY связывает таблицы и защищает от «осиротевших» данных;
- деньги лучше хранить в
NUMERIC, а не во FLOAT;
- дату и время событий в PostgreSQL чаще всего удобно хранить в
TIMESTAMPTZ;
- внешний ключ не создаёт индекс на дочерней колонке автоматически, поэтому индекс часто нужно добавить отдельно.
Хорошая таблица экономит много времени. Она не молчит, когда в данные пытаются положить мусор, а сразу говорит: «Так нельзя». И это именно то поведение, которое нужно от надёжной базы данных.
CREATE TABLE— это команда, которая создаёт новую таблицу в базе данных.Если говорить совсем просто, она отвечает на вопрос:
Таблица в базе похожа на хорошо продуманную анкету. До того как люди начнут её заполнять, нужно решить:
Например, если мы делаем таблицу пользователей, нам заранее нужно решить: у пользователя будет
email, имя, дата регистрации, уникальныйid. Email должен быть обязательным и не должен повторяться. Дата регистрации должна подставляться автоматически.Вот всё это и описывает
CREATE TABLE.Пока таблицы нет, в неё нельзя вставить данные через
INSERTи из неё нельзя ничего прочитать черезSELECT. ПоэтомуCREATE TABLE— одна из первых команд, с которой начинается любой реальный проект.Зачем нужен CREATE TABLE
База данных — это не просто «коробка, куда складывают информацию». Хорошая база сразу помогает хранить данные аккуратно.
Плохо спроектированная таблица быстро превращается в кладовку без полок: вроде всё где-то лежит, но найти, проверить и использовать это больно.
Например, если никак не ограничить данные, в таблице может появиться такое:
Сразу несколько проблем:
Часть таких проблем можно ловить в коде приложения. Но надёжнее заложить базовые правила прямо в таблицу. Тогда база сама не пропустит мусор.
Хорошо продуманная таблица:
NULLтам, где значение обязательно;CHECK;FOREIGN KEY;DEFAULT.Именно поэтому
CREATE TABLE— не просто техническая команда. Это момент, когда вы проектируете порядок в данных.Базовый синтаксис CREATE TABLE
Посмотрим на пример:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, display_name VARCHAR(100) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );Здесь мы создаём таблицу
usersс четырьмя колонками:id— уникальный идентификатор пользователя;email— почта пользователя;display_name— отображаемое имя;created_at— дата и время создания строки.Разберём по частям.
CREATE TABLE users ( ... );Так начинается создание таблицы. После
CREATE TABLEуказываем имя таблицы, а внутри скобок перечисляем колонки и правила для них.id SERIAL PRIMARY KEYКолонка
idполучает типSERIAL. В PostgreSQL это удобный способ сказать: «пусть база сама выдаёт значения 1, 2, 3, 4 и так далее».PRIMARY KEYозначает, что это главный идентификатор строки. Значение должно быть уникальным и не должно бытьNULL.email VARCHAR(255) UNIQUE NOT NULLКолонка
emailхранит текст длиной до 255 символов.UNIQUEзапрещает повторения. Два пользователя с одним и тем же email не пройдут.NOT NULLговорит, что email обязателен. Нельзя создать пользователя без email.display_name VARCHAR(100) NOT NULLКолонка
display_nameтоже текстовая и тоже обязательная.created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()Колонка
created_atхранит дату и время. ТипTIMESTAMPTZв PostgreSQL удобен для событий во времени: регистраций, заказов, оплат, сообщений.DEFAULT NOW()означает: если при вставке строки мы не указалиcreated_at, база сама поставит текущий момент.Запятые и точка с запятой
Внутри
CREATE TABLEколонки разделяются запятыми:CREATE TABLE products ( id SERIAL PRIMARY KEY, title TEXT NOT NULL, price NUMERIC(12, 2) NOT NULL );После последней колонки запятая обычно не ставится.
В конце всей команды ставится точка с запятой:
Это как точка в конце предложения: команда закончилась, можно выполнять.
Пример из жизни: таблица записей на услугу
Допустим, мы делаем небольшой сервис для записи клиентов. Нужно хранить записи: кто пришёл, какой телефон оставил, какую услугу выбрал и на какое время записался.
Создадим таблицу:
CREATE TABLE appointments ( id SERIAL PRIMARY KEY, client_name VARCHAR(100) NOT NULL, phone VARCHAR(20) NOT NULL, service VARCHAR(50) NOT NULL, starts_at TIMESTAMPTZ NOT NULL, notes TEXT );Что здесь важно:
client_name— обязательное имя клиента;phone— обязательный телефон;service— обязательное название услуги;starts_at— обязательные дата и время записи;notes— необязательная заметка.У
notesнетNOT NULL, значит это поле можно не заполнять. Если заметки нет, в колонке будетNULL.Теперь можно вставить запись:
INSERT INTO appointments (client_name, phone, service, starts_at) VALUES ('Anna', '+79991234567', 'haircut', '2026-05-10 14:00:00+03');Результат в таблице будет примерно таким:
Мы не передавали
id, потому чтоSERIALсделал это сам.Мы не передавали
notes, потому что заметка необязательная.Так таблица работает как аккуратный бланк: обязательные поля требует, необязательные разрешает пропустить.
Колонка — это имя, тип и правила
Внутри таблицы каждая колонка обычно состоит из трёх частей:
Например:
email VARCHAR(255) UNIQUE NOT NULLЗдесь:
email— имя колонки;VARCHAR(255)— тип данных;UNIQUE NOT NULL— ограничения.Можно читать эту строку почти обычным языком:
Чем лучше вы описали колонки на этапе создания таблицы, тем меньше хаоса получите позже.
Основные типы данных
Тип данных отвечает за то, что можно хранить в колонке.
Если колонка числовая, в неё нельзя спокойно положить произвольный текст. Если колонка с датой, с ней можно делать операции как с датой: сравнивать, сортировать, считать интервалы.
Вот короткая шпаргалка по частым типам в PostgreSQL.
INTEGERBIGINTSMALLINTNUMERIC(12, 2)REAL,DOUBLE PRECISIONTEXT,VARCHAR(n)DATETIMESTAMPTZBOOLEANUUIDДеньги — только NUMERIC
Для денег лучше использовать точный тип:
price NUMERIC(12, 2) NOT NULLNUMERIC(12, 2)означает: всего до 12 цифр, из них 2 после запятой.Например:
Почему не
FLOAT?Потому что
FLOAT,REALиDOUBLE PRECISIONхранят числа приблизительно. Это нормально для научных расчётов, графики или координат, но плохо для денег.Классический пример проблемы:
SELECT 0.1::FLOAT + 0.2::FLOAT AS result;Результат может оказаться не ровно
0.3, а чем-то вроде:Для цены товара, баланса пользователя или суммы заказа такое поведение не подходит.
Поэтому правило простое:
Деньги, цены, комиссии, балансы — через
NUMERIC, а не черезFLOAT.Дата и время — TIMESTAMPTZ
Для событий во времени в PostgreSQL чаще всего выбирают
TIMESTAMPTZ.Например:
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()Так удобно хранить:
Важно понимать:
TIMESTAMPTZне хранит «название часового пояса» рядом с каждой строкой. PostgreSQL приводит момент времени к единому виду и показывает его с учётом настроек текущей сессии.Практический смысл такой: если событие произошло в конкретный момент времени,
TIMESTAMPTZобычно безопаснее, чем «голое»TIMESTAMP.А вот если вам нужна просто дата без времени, например день рождения, используйте
DATE:birth_date DATEДень рождения не зависит от часового пояса. Это просто календарная дата.
TEXT или VARCHAR
В PostgreSQL
TEXTиVARCHAR(n)очень похожи по производительности.Например:
подходит для описаний, комментариев и длинных текстов.
А вот
VARCHAR(255)нужен, когда у ограничения есть смысл:email VARCHAR(255) NOT NULLЕсли бизнес-правило говорит, что поле не должно быть длиннее определённого размера, можно использовать
VARCHAR(n).Но не стоит ставить ограничения просто «на всякий случай». Если текст может быть любой разумной длины, в PostgreSQL часто спокойно используют
TEXT.Ограничения: правила, которые защищают данные
Ограничения, или constraints, — это правила, которые база проверяет при
INSERTиUPDATE.Если правило нарушено, база не запишет строку.
Посмотрим на пример таблицы заказов:
CREATE TABLE orders ( id SERIAL PRIMARY KEY, number TEXT NOT NULL UNIQUE, customer_id INT NOT NULL REFERENCES customers(id), amount NUMERIC(12, 2) NOT NULL CHECK (amount > 0), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')) );Здесь сразу несколько правил.
PRIMARY KEY
PRIMARY KEY— главный идентификатор строки.id SERIAL PRIMARY KEYОн должен быть:
По
PRIMARY KEYудобно ссылаться на строку из других таблиц. Например, заказ может ссылаться на пользователя черезuser_id.Обычно в таблице один
PRIMARY KEY.NOT NULL
NOT NULLзапрещает пустое значение.email TEXT NOT NULLЕсли попробовать вставить строку без email, база выдаст ошибку.
Это полезно для всего, без чего строка теряет смысл:
Если поле обязательно по смыслу, лучше написать
NOT NULLсразу.UNIQUE
UNIQUEзапрещает повторяющиеся значения.email TEXT UNIQUE NOT NULLТак база не даст создать двух пользователей с одной и той же почтой.
Ещё примеры:
number TEXT UNIQUE NOT NULLдля номера заказа;
slug TEXT UNIQUE NOT NULLдля адреса страницы;
phone TEXT UNIQUEдля телефона, если по правилам бизнеса он должен быть уникальным.
UNIQUE— это не просто украшение. Это защита от дублей, которые потом очень неприятно чистить.DEFAULT
DEFAULTподставляет значение автоматически, если его не указали вINSERT.Например:
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()Теперь при вставке строки не нужно каждый раз передавать дату создания.
INSERT INTO users (email, display_name) VALUES ('anna@example.com', 'Anna');created_atбаза заполнит сама.Другой пример:
status TEXT NOT NULL DEFAULT 'pending'Если при создании заказа статус не указали, заказ станет
pending.Это удобно: меньше ручной работы и меньше забытых значений.
CHECK
CHECKпроверяет условие для строки.Например, сумма заказа должна быть больше нуля:
amount NUMERIC(12, 2) NOT NULL CHECK (amount > 0)Статус должен быть одним из разрешённых:
status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled'))Возраст не должен быть отрицательным:
age INT CHECK (age >= 0)CHECKпомогает перенести простую здравую логику прямо в базу.Без
CHECKможно случайно получить заказ на отрицательную сумму или пользователя с возрастом-5.С
CHECKбаза просто не примет такую строку.FOREIGN KEY: связь между таблицами
FOREIGN KEYнужен, чтобы связать одну таблицу с другой.Например, есть таблица пользователей:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT UNIQUE NOT NULL );И есть таблица комментариев:
CREATE TABLE comments ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id), body TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );Колонка
user_idссылается наusers(id).Это значит: комментарий не может принадлежать пользователю, которого нет.
Если попробовать вставить комментарий с несуществующим
user_id, база не даст этого сделать:INSERT INTO comments (user_id, body) VALUES (999, 'Nice post');Если пользователя с
id = 999нет, будет ошибка.Это защищает от «осиротевших» данных: комментариев без пользователя, заказов без клиента, платежей без заказа.
Что такое ON DELETE
Когда есть связь между таблицами, возникает вопрос: что делать с дочерними строками, если удалить родительскую?
Например, есть пользователь и его комментарии.
Можно выбрать разное поведение.
ON DELETE CASCADE
CREATE TABLE comments ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, body TEXT NOT NULL );ON DELETE CASCADEозначает: если удалить пользователя, его комментарии удалятся автоматически.Это удобно, когда дочерние данные не имеют смысла без родителя.
Например:
Но с
CASCADEнужно быть осторожным: одно удаление может потянуть за собой много других строк.ON DELETE RESTRICT
CREATE TABLE comments ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id) ON DELETE RESTRICT, body TEXT NOT NULL );ON DELETE RESTRICTзапрещает удалять пользователя, пока у него есть комментарии.Это безопасный вариант, когда вы не хотите случайно потерять связанные данные.
ON DELETE SET NULL
CREATE TABLE comments ( id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id) ON DELETE SET NULL, body TEXT NOT NULL );ON DELETE SET NULLозначает: если удалить пользователя, комментарий останется, ноuser_idстанетNULL.Важно: для этого колонка
user_idдолжна разрешатьNULL. Если написатьuser_id INT NOT NULL, тоSET NULLне сможет сработать.Такой вариант подходит, когда сам комментарий можно оставить, но связь с пользователем уже неизвестна или не нужна.
FOREIGN KEY не создаёт индекс автоматически
Это частая ловушка новичков.
В PostgreSQL внешний ключ на дочерней колонке не создаёт индекс автоматически.
Например:
CREATE TABLE comments ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id), body TEXT NOT NULL );Связь есть. База проверяет, что
user_idсуществует вusers.Но индекса на
comments(user_id)автоматически нет.Если комментариев много, запросы по
user_idмогут работать медленнее:SELECT * FROM comments WHERE user_id = 10;Поэтому индекс часто добавляют отдельно:
CREATE INDEX comments_user_id_idx ON comments(user_id);Особенно это важно, если вы часто:
JOINпо внешнему ключу;Запомните коротко:
FOREIGN KEYотвечает за правильность данных, а индекс — за скорость поиска.Это разные вещи.
Полный пример: пользователи и заказы
Соберём небольшой пример ближе к реальному проекту.
Есть пользователи:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );Есть заказы:
CREATE TABLE orders ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id), number TEXT NOT NULL UNIQUE, amount NUMERIC(12, 2) NOT NULL CHECK (amount > 0), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'cancelled')), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );И добавим индекс на внешний ключ:
CREATE INDEX orders_user_id_idx ON orders(user_id);Теперь база защищает нас от многих ошибок:
user_id.Вот так
CREATE TABLEпревращается из простой команды в фундамент нормальной структуры данных.Имена таблиц и колонок
Имена лучше писать в стиле
snake_case:CREATE TABLE user_profiles ( id SERIAL PRIMARY KEY, user_id INT NOT NULL, birth_date DATE );Такой стиль удобен:
Плохая идея — имена с пробелами и заглавными буквами:
CREATE TABLE "User Profiles" ( "User ID" INT );Формально это работает, но потом в каждом запросе придётся помнить кавычки и точное написание:
SELECT "User ID" FROM "User Profiles";Это быстро надоедает и создаёт лишние ошибки.
Лучше сразу писать просто:
SELECT user_id FROM user_profiles;NULL: разрешать или запрещать
Каждая колонка либо разрешает
NULL, либо нет.Если вы пишете:
значение можно не указывать, и тогда будет
NULL.Если пишете:
phone TEXT NOT NULLзначение обязательно.
NULL— это не ноль и не пустая строка. Это «значение отсутствует» или «значение неизвестно».Иногда
NULLполезен. Например, у записи может не быть заметки:Но если почти все важные поля nullable, схема становится слабой. База перестаёт защищать данные, а код приложения начинает постоянно проверять: «а тут точно что-то есть?»
Хорошее правило:
NOT NULL;NULL;Например, не всегда хорошо делать одну огромную таблицу пользователей с десятками необязательных полей. Иногда лучше вынести дополнительные данные в
user_profiles,user_settingsили другую связанную таблицу.Частые ошибки новичков
Ошибка 1. Хранить деньги во FLOAT
Плохо:
CREATE TABLE products ( id SERIAL PRIMARY KEY, price FLOAT NOT NULL );Для денег так делать не стоит.
FLOATхранит приблизительные значения, а деньги требуют точности.Лучше:
CREATE TABLE products ( id SERIAL PRIMARY KEY, price NUMERIC(12, 2) NOT NULL );Ошибка 2. Не ставить NOT NULL на обязательные поля
Плохо:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT, display_name TEXT );Так база разрешит пользователя без email и имени.
Лучше:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT NOT NULL, display_name TEXT NOT NULL );Если поле обязательно для нормальной работы приложения, это должно быть видно в схеме.
Ошибка 3. Забыть UNIQUE для email или номера заказа
Плохо:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT NOT NULL );Так можно создать двух пользователей с одним email.
Лучше:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE );То же самое с номерами заказов, slug страниц, внешними идентификаторами платежей и другими значениями, которые не должны повторяться.
Ошибка 4. Хранить связь без FOREIGN KEY
Плохо:
CREATE TABLE comments ( id SERIAL PRIMARY KEY, user_id INT NOT NULL, body TEXT NOT NULL );На вид всё нормально: есть
user_id, значит комментарий связан с пользователем.Но база этого не знает. Она спокойно примет комментарий с
user_id, которого нет в таблице пользователей.Лучше:
CREATE TABLE comments ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id), body TEXT NOT NULL );Теперь связь — не просто договорённость в голове разработчика, а настоящее правило базы данных.
Ошибка 5. Думать, что FOREIGN KEY сам создаст индекс
Плохо думать так:
user_id INT NOT NULL REFERENCES users(id)и считать, что индекс появился автоматически.
В PostgreSQL индекс на дочернюю колонку нужно добавить отдельно:
CREATE INDEX comments_user_id_idx ON comments(user_id);FOREIGN KEYзащищает данные, но не всегда ускоряет ваши запросы.Ошибка 6. Использовать неудобные имена
Плохо:
CREATE TABLE "Order Items" ( "Order ID" INT );Лучше:
CREATE TABLE order_items ( order_id INT );Чем проще имена, тем приятнее потом писать
SELECT,JOIN,UPDATEи любые другие запросы.Ошибка 7. Забыть DEFAULT для created_at
Плохо:
CREATE TABLE orders ( id SERIAL PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL );Теперь при каждом
INSERTнужно помнить проcreated_at.Лучше:
CREATE TABLE orders ( id SERIAL PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );Теперь база сама подставит дату создания.
Ошибка 8. Делать слишком много nullable-колонок
Плохо:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT, company_name TEXT, company_tax_id TEXT, student_group TEXT, teacher_subject TEXT );Если в одной таблице смешиваются обычные пользователи, компании, студенты и преподаватели, половина колонок будет постоянно
NULL.Это сигнал, что модель данных, возможно, стоит разделить на несколько таблиц.
Например:
users;company_profiles;student_profiles;teacher_profiles.Не всегда нужно дробить сразу, но если таблица превращается в поле из
NULL, стоит остановиться и подумать.Как читать CREATE TABLE глазами разработчика
Когда видите
CREATE TABLE, не пытайтесь читать его как набор случайных слов.Читайте по смыслу:
CREATE TABLE orders ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id), amount NUMERIC(12, 2) NOT NULL CHECK (amount > 0), status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );Это можно перевести так:
Вот это и есть хорошее понимание SQL: вы видите не просто синтаксис, а правила предметной области.
Главное
CREATE TABLEсоздаёт таблицу и описывает её структуру: какие в ней будут колонки, какие у них типы и какие правила база должна проверять.Таблица — это не просто место для хранения. Это договор с базой данных: какие значения допустимы, какие обязательны, какие должны быть уникальными и как строки связаны между собой.
Самое важное, что нужно запомнить:
CREATE TABLEсоздаёт новую таблицу;PRIMARY KEYзадаёт главный идентификатор строки;NOT NULLзапрещает пустые значения;UNIQUEзапрещает дубли;CHECKпроверяет условие;DEFAULTподставляет значение автоматически;FOREIGN KEYсвязывает таблицы и защищает от «осиротевших» данных;NUMERIC, а не воFLOAT;TIMESTAMPTZ;Хорошая таблица экономит много времени. Она не молчит, когда в данные пытаются положить мусор, а сразу говорит: «Так нельзя». И это именно то поведение, которое нужно от надёжной базы данных.