CREATE TABLE — это команда «сделай мне новую таблицу с такими-то колонками и правилами». Это первая SQL-команда любого нового проекта: пока нет таблиц, и в INSERT нечего писать, и в SELECT неоткуда читать.
Представь, что таблица — это бланк анкеты. CREATE TABLE — это момент, когда ты решаешь, какие поля будут в анкете: «Имя», «Дата рождения», «Email». Какие из них обязательные, какие можно пропустить, какие должны быть только числами. Один раз продумал — и потом тысячи людей заполняют один и тот же бланк.
Зачем нужен CREATE TABLE
Любая работа с данными начинается с того, что данные где-то хранятся. CREATE TABLE — это место.
Хорошо продуманная таблица:
- Защищает от мусора (
NOT NULL, CHECK, UNIQUE).
- Связана с другими через
FOREIGN KEY, поэтому данные согласованы между собой.
- Заранее заточена под нужные типы (числа — числами, даты — датами).
Плохая — наоборот, превращает любой SELECT в страдание: «у трети юзеров email пустой, у пятёрки age — строка, ещё у двоих age — '-1'».
Базовый синтаксис
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()
);
Что тут происходит, по строчкам:
CREATE TABLE users (...) — создаём таблицу с именем users.
id SERIAL PRIMARY KEY — колонка id, тип «автоинкремент» (1, 2, 3...), она же primary key.
email VARCHAR(255) UNIQUE NOT NULL — текст до 255 символов, уникальный, обязательный.
display_name VARCHAR(100) NOT NULL — текст до 100 символов, обязательный.
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() — дата-время с часовым поясом, обязательная, по умолчанию равна моменту создания строки.
Запятые — между колонками, не в конце. Точка с запятой — в самом конце.
Пример: бланк анкеты
Допустим, ты делаешь сайт для записи на стрижку. Нужна таблица записей:
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
);
Теперь в неё можно писать:
INSERT INTO appointments (client_name, phone, service, starts_at)
VALUES ('Аня', '+79991234567', 'стрижка', '2026-05-10 14:00');
Получается:
| id |
client_name |
phone |
service |
starts_at |
notes |
| 1 |
Аня |
+79991234567 |
стрижка |
2026-05-10 14:00:00 |
NULL |
Поле notes опционально (нет NOT NULL) — ничего не написали, в базе оно NULL («пусто»).
id автоматически стал 1 — SERIAL сам считает. Следующий INSERT получит id = 2, и так далее.
Типы колонок — короткая шпаргалка
| Категория |
Тип в Postgres |
Когда |
| Целое число |
INTEGER, BIGINT |
id, счётчики, возраст |
| Дробное (точное) |
NUMERIC(p, s) |
деньги — только это |
| Дробное (неточное) |
REAL, DOUBLE PRECISION |
физика, графика; не деньги |
| Текст |
TEXT, VARCHAR(n) |
в Postgres TEXT ≈ VARCHAR без лимита |
| Дата + время |
TIMESTAMPTZ |
для истории — всегда с TZ |
| Только дата |
DATE |
день рождения, дата записи |
| Логика |
BOOLEAN |
TRUE/FALSE |
| UUID |
UUID |
id когда не хочешь последовательные |
Главные правила:
- Деньги —
NUMERIC(precision, scale). Никогда FLOAT — копейки потеряются. Например, NUMERIC(12, 2) хранит до 10 цифр до запятой и 2 после.
- Дата+время —
TIMESTAMPTZ (с timezone). Без TZ Postgres хранит «голое» время, и расчёты «через час» зависят от настроек сессии.
- Текст —
TEXT в Postgres лучше, чем VARCHAR без причины. Скорость одинаковая, лимит символов накладываешь только если бизнес требует (например, email VARCHAR(255) — потому что RFC).
Ограничения (constraints) — дисциплина для данных
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 — уникальный идентификатор строки. На таблицу один. По нему обычно ищут.
- NOT NULL — нельзя записать
NULL. Если попробуешь — ошибка.
- UNIQUE — никакая другая строка не повторит это значение.
- FOREIGN KEY (
REFERENCES customers(id)) — значение должно существовать в customers.id. Защита от «осиротевших» заказов.
- CHECK — произвольное условие на одной строке:
amount > 0, status IN (...), email LIKE '%@%'.
- DEFAULT — какое значение поставить, если в
INSERT его не указали.
Эти правила экономят сотни часов отладки. БД сразу не пропустит мусор — ошибка прилетит в INSERT, а не через две недели в отчёте.
FOREIGN KEY — связь между таблицами
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
REFERENCES users(id) — значение user_id должно существовать в users.id. Если попытаешься вставить user_id = 999, а такого юзера нет — Postgres откажет.
ON DELETE CASCADE — если удалить users строку, все её comments удалятся автоматически. Бывают другие варианты:
ON DELETE RESTRICT — запретит удаление родителя, пока есть дети.
ON DELETE SET NULL — обнулит user_id у комментариев, но оставит сам комментарий (требует, чтобы колонка была nullable).
Важно: FK не создаёт индекс на дочерней колонке. Чтобы DELETE FROM users WHERE id = 5 не делал full scan по comments, нужен ручной:
CREATE INDEX comments_user_id_idx ON comments(user_id);
Это частая ловушка — все думают, что FK = индекс. Это не так.
Частые ошибки новичков
1. Деньги во FLOAT. price FLOAT — потеря копеек на каждом расчёте. Только NUMERIC(p, s). Это не теоретическая проблема: 0.1 + 0.2 во float-арифметике даёт 0.30000000000000004.
2. Слишком широкие типы. BIGINT для возраста (хватит SMALLINT), TEXT для двухбуквенного кода страны (хватит VARCHAR(2)). Не критично, но на больших таблицах экономит гигабайты.
3. Отсутствие FK там, где они логичны. comments.user_id INT NOT NULL без REFERENCES users(id) — это «договорённость», не гарантия. Когда-нибудь у тебя будет comment с user_id = 999, а такого юзера нет, и кто-то спишет полдня на «откуда».
4. Слишком много NULL-able колонок. NULL — это «нет значения». Куча NULL обычно означает, что схема плохо продумана. Возможно, надо разделить на разные таблицы или использовать enum/jsonb. NULL ломают = (нужно IS NULL), агрегаты (COUNT пропускает их), индексы (Postgres индексирует, но плохо).
5. Имена с пробелами и кавычками. CREATE TABLE "User Profiles" (...) — формально работает, но любой запрос требует кавычек вокруг имени. Используй snake_case (user_profiles).
6. Забыл DEFAULT NOW() для created_at. В каждом INSERT надо помнить про дату — забудешь раз, и в строке NULL. С дефолтом — Postgres сам подставит, ничего не надо передавать.
Мини-резюме
CREATE TABLE объявляет, какие колонки и какого типа будут в таблице. Это «бланк анкеты».
- Колонки разделяются запятыми. Точка с запятой — в самом конце.
NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY, CHECK, DEFAULT — пять основных способов навести дисциплину в данных. Используй заранее, чтобы потом не страдать.
- Деньги —
NUMERIC. Дата+время — TIMESTAMPTZ. Текст — TEXT.
- FK не создаёт индекс автоматически — для производительности добавь руками.
CREATE TABLE— это команда «сделай мне новую таблицу с такими-то колонками и правилами». Это первая SQL-команда любого нового проекта: пока нет таблиц, и вINSERTнечего писать, и вSELECTнеоткуда читать.Представь, что таблица — это бланк анкеты.
CREATE TABLE— это момент, когда ты решаешь, какие поля будут в анкете: «Имя», «Дата рождения», «Email». Какие из них обязательные, какие можно пропустить, какие должны быть только числами. Один раз продумал — и потом тысячи людей заполняют один и тот же бланк.Зачем нужен CREATE TABLE
Любая работа с данными начинается с того, что данные где-то хранятся. CREATE TABLE — это место.
Хорошо продуманная таблица:
NOT NULL,CHECK,UNIQUE).FOREIGN KEY, поэтому данные согласованы между собой.Плохая — наоборот, превращает любой
SELECTв страдание: «у трети юзеров email пустой, у пятёрки age — строка, ещё у двоих age —'-1'».Базовый синтаксис
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() );Что тут происходит, по строчкам:
CREATE TABLE users (...)— создаём таблицу с именемusers.id SERIAL PRIMARY KEY— колонкаid, тип «автоинкремент» (1, 2, 3...), она же primary key.email VARCHAR(255) UNIQUE NOT NULL— текст до 255 символов, уникальный, обязательный.display_name VARCHAR(100) NOT NULL— текст до 100 символов, обязательный.created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()— дата-время с часовым поясом, обязательная, по умолчанию равна моменту создания строки.Запятые — между колонками, не в конце. Точка с запятой — в самом конце.
Пример: бланк анкеты
Допустим, ты делаешь сайт для записи на стрижку. Нужна таблица записей:
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 );Теперь в неё можно писать:
INSERT INTO appointments (client_name, phone, service, starts_at) VALUES ('Аня', '+79991234567', 'стрижка', '2026-05-10 14:00');Получается:
Поле
notesопционально (нетNOT NULL) — ничего не написали, в базе оноNULL(«пусто»).idавтоматически стал1—SERIALсам считает. СледующийINSERTполучитid = 2, и так далее.Типы колонок — короткая шпаргалка
INTEGER,BIGINTNUMERIC(p, s)REAL,DOUBLE PRECISIONTEXT,VARCHAR(n)TEXT≈VARCHARбез лимитаTIMESTAMPTZDATEBOOLEANTRUE/FALSEUUIDГлавные правила:
NUMERIC(precision, scale). НикогдаFLOAT— копейки потеряются. Например,NUMERIC(12, 2)хранит до 10 цифр до запятой и 2 после.TIMESTAMPTZ(с timezone). Без TZ Postgres хранит «голое» время, и расчёты «через час» зависят от настроек сессии.TEXTв Postgres лучше, чемVARCHARбез причины. Скорость одинаковая, лимит символов накладываешь только если бизнес требует (например,email VARCHAR(255)— потому что RFC).Ограничения (constraints) — дисциплина для данных
Constraints — правила, которые БД проверяет на каждый
INSERTиUPDATE. Если правило нарушено — операция упадёт с ошибкой.CREATE TABLE orders ( id SERIAL PRIMARY KEY, -- PRIMARY KEY number TEXT NOT NULL UNIQUE, -- NOT NULL + UNIQUE customer_id INT NOT NULL REFERENCES customers(id), -- FOREIGN KEY amount NUMERIC(12,2) NOT NULL CHECK (amount > 0),-- CHECK status TEXT NOT NULL DEFAULT 'pending' -- DEFAULT CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')) );Разберём:
NULL. Если попробуешь — ошибка.REFERENCES customers(id)) — значение должно существовать вcustomers.id. Защита от «осиротевших» заказов.amount > 0,status IN (...),email LIKE '%@%'.INSERTего не указали.Эти правила экономят сотни часов отладки. БД сразу не пропустит мусор — ошибка прилетит в
INSERT, а не через две недели в отчёте.FOREIGN KEY — связь между таблицами
CREATE TABLE comments ( id SERIAL PRIMARY KEY, user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, body TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );REFERENCES users(id)— значениеuser_idдолжно существовать вusers.id. Если попытаешься вставитьuser_id = 999, а такого юзера нет — Postgres откажет.ON DELETE CASCADE— если удалитьusersстроку, все еёcommentsудалятся автоматически. Бывают другие варианты:ON DELETE RESTRICT— запретит удаление родителя, пока есть дети.ON DELETE SET NULL— обнулитuser_idу комментариев, но оставит сам комментарий (требует, чтобы колонка была nullable).Важно: FK не создаёт индекс на дочерней колонке. Чтобы
DELETE FROM users WHERE id = 5не делал full scan поcomments, нужен ручной:CREATE INDEX comments_user_id_idx ON comments(user_id);Это частая ловушка — все думают, что FK = индекс. Это не так.
Частые ошибки новичков
1. Деньги во FLOAT.
price FLOAT— потеря копеек на каждом расчёте. ТолькоNUMERIC(p, s). Это не теоретическая проблема:0.1 + 0.2во float-арифметике даёт0.30000000000000004.2. Слишком широкие типы.
BIGINTдля возраста (хватитSMALLINT),TEXTдля двухбуквенного кода страны (хватитVARCHAR(2)). Не критично, но на больших таблицах экономит гигабайты.3. Отсутствие FK там, где они логичны.
comments.user_id INT NOT NULLбезREFERENCES users(id)— это «договорённость», не гарантия. Когда-нибудь у тебя будетcommentсuser_id = 999, а такого юзера нет, и кто-то спишет полдня на «откуда».4. Слишком много NULL-able колонок. NULL — это «нет значения». Куча NULL обычно означает, что схема плохо продумана. Возможно, надо разделить на разные таблицы или использовать enum/jsonb. NULL ломают
=(нужноIS NULL), агрегаты (COUNTпропускает их), индексы (Postgres индексирует, но плохо).5. Имена с пробелами и кавычками.
CREATE TABLE "User Profiles" (...)— формально работает, но любой запрос требует кавычек вокруг имени. Используйsnake_case(user_profiles).6. Забыл
DEFAULT NOW()дляcreated_at. В каждомINSERTнадо помнить про дату — забудешь раз, и в строкеNULL. С дефолтом — Postgres сам подставит, ничего не надо передавать.Мини-резюме
CREATE TABLEобъявляет, какие колонки и какого типа будут в таблице. Это «бланк анкеты».NOT NULL,UNIQUE,PRIMARY KEY,FOREIGN KEY,CHECK,DEFAULT— пять основных способов навести дисциплину в данных. Используй заранее, чтобы потом не страдать.NUMERIC. Дата+время —TIMESTAMPTZ. Текст —TEXT.