INSERT — это команда «добавить новую строку в таблицу». Самая базовая операция: пока в таблицу никто ничего не вставил, она пустая, и в SELECT выбирать нечего.
Если возвращаться к аналогии с блокнотом контактов: INSERT — это «новая страница, новый человек: имя такое, телефон такой, email такой».
Зачем нужен INSERT
Любые данные в БД когда-то появились через INSERT. Регистрация пользователя, оформление заказа, новый комментарий — всё это под капотом INSERT INTO .... Без него таблицы остаются пустыми.
Базовый синтаксис
INSERT INTO users (email, display_name, country)
VALUES ('alice@example.com', 'Аня', 'RU');
По частям:
INSERT INTO users — в какую таблицу вставляем.
(email, display_name, country) — список колонок, которые заполняем.
VALUES (...) — значения в том же порядке, что и колонки.
Колонки указывать явно — это правильная привычка. Даже если ты сейчас вставляешь все поля, через месяц в таблицу добавят новую колонку, и INSERT INTO users VALUES (...) без списка колонок упадёт неожиданно.
Пример с таблицей
Пустая таблица users:
| id |
email |
display_name |
country |
| нет строк |
|
|
|
Делаем INSERT:
INSERT INTO users (email, display_name, country)
VALUES ('anya@example.com', 'Аня', 'RU');
Результат:
id мы не передавали — он автоматически стал 1, потому что объявлен как SERIAL (автоинкремент). Следующий INSERT получит id = 2.
Колонки с DEFAULT (created_at TIMESTAMPTZ DEFAULT NOW() и им подобные) тоже можно не передавать — Postgres сам подставит дефолт.
Batch-вставка — несколько строк за раз
Если нужно вставить много строк, не делай N отдельных INSERT — это медленно. Используй batch:
INSERT INTO users (email, display_name, country)
VALUES
('anya@example.com', 'Аня', 'RU'),
('bob@example.com', 'Боб', 'US'),
('vera@example.com', 'Вера', 'BY');
Один запрос, одна транзакция, один проход по индексам — резко быстрее, чем три отдельных INSERT'а. Если льёшь данные тысячами — обязательно batch'ом. Сетевая задержка часто доминирует над временем самой вставки.
На практике батчами по 1000-10000 строк работают комфортно.
INSERT FROM SELECT — копирование из другой таблицы
Иногда нужно скопировать данные из одной таблицы в другую (или преобразовать на лету):
INSERT INTO orders_archive (id, customer_id, amount, archived_at)
SELECT id, customer_id, amount, NOW()
FROM orders
WHERE status = 'completed' AND created_at < '2024-01-01';
Это транзакционно — либо все строки скопируются, либо ни одной (если хоть одна нарушит constraint). Удобно для миграций данных и backfill.
RETURNING — получить вставленные значения
PostgreSQL умеет возвращать вставленные строки прямо из INSERT:
INSERT INTO orders (customer_id, amount)
VALUES (5, 100)
RETURNING id, created_at;
Без RETURNING пришлось бы делать второй запрос (SELECT MAX(id) или currval('orders_id_seq')), и между ними возможна гонка с другой транзакцией. RETURNING решает атомарно.
RETURNING есть в PostgreSQL, SQLite (3.35+), Oracle. В MySQL — нет (там LAST_INSERT_ID()).
UPSERT через ON CONFLICT
«Вставь, а если уже есть — обнови» — типичная задача для миграций, импортов, идемпотентных операций. PostgreSQL решает через ON CONFLICT:
INSERT INTO users (email, display_name, last_login_at)
VALUES ('anya@example.com', 'Аня', NOW())
ON CONFLICT (email) DO UPDATE
SET last_login_at = EXCLUDED.last_login_at,
display_name = EXCLUDED.display_name;
Логика:
- Пытаемся вставить.
- Если возникает конфликт по уникальному ограничению на
email — выполняется UPDATE с указанными полями.
- Псевдо-таблица
EXCLUDED содержит значения, которые мы пытались вставить (из VALUES).
Альтернативные формы:
ON CONFLICT (email) DO NOTHING — игнорировать дубликаты, не считать ошибкой.
ON CONFLICT ON CONSTRAINT users_email_key DO UPDATE ... — указать конкретный constraint по имени.
WHERE в UPDATE — условный апдейт: DO UPDATE SET ... WHERE users.last_login_at < EXCLUDED.last_login_at (обновляем только если новая дата свежее).
ON CONFLICT требует, чтобы существовало уникальное ограничение (UNIQUE, PRIMARY KEY или EXCLUDE) на колонке. Без него Postgres не знает, что считать «конфликтом».
В MySQL аналог — INSERT ... ON DUPLICATE KEY UPDATE. В стандарте SQL есть MERGE, и Postgres поддерживает его с 15-й версии — но ON CONFLICT пока выразительнее.
Частые ошибки новичков
1. Пропущенный список колонок. INSERT INTO users VALUES ('alice@...', 'Аня', 'RU') без списка зависит от порядка колонок в таблице. Кто-то добавит ALTER TABLE ... ADD COLUMN status — и старый код сломается. Всегда пиши список колонок.
2. Несоответствие количества значений. INSERT INTO t (a, b, c) VALUES (1, 2) — три колонки, два значения, ошибка. Иногда видно сразу, иногда нет (если values динамические из приложения).
3. Кавычки в строках. Строки и даты — одинарные кавычки. Двойные кавычки в PostgreSQL — для имён колонок:
INSERT INTO users (name) VALUES ('Аня');
INSERT INTO users (name) VALUES ("Аня");
4. UPSERT без ON CONFLICT. Сценарий «проверь существование → если нет, INSERT» в коде приложения не атомарен — между проверкой и вставкой другая транзакция может вставить ту же строку. Уникальное ограничение + ON CONFLICT DO NOTHING — единственный надёжный способ.
5. id передан вручную, потом ломается автоинкремент. Если ты явно вставил id = 1000, а sequence стоит на 5 — следующий INSERT без id попробует поставить id = 6. И всё хорошо. Но если был insert id = 1, и кто-то уже до тебя вставил такой — конфликт. Лучше вообще не передавать id, пусть БД сама.
6. INSERT миллиарда строк одним запросом. PostgreSQL примет, но WAL-лог распухнет, репликация затормозит, всё. Bulk-импорт — батчами, либо через COPY FROM (специальная команда для bulk-load — в десятки раз быстрее любого INSERT).
Мини-резюме
INSERT INTO table (cols) VALUES (vals) — добавляет новую строку.
- Список колонок указывай явно. Меньше боли при
ALTER TABLE.
- Несколько строк за раз —
VALUES (..), (..), (..) (batch).
INSERT FROM SELECT — для копирования и backfill.
RETURNING — забрать сгенерированный id и другие значения сразу.
ON CONFLICT — UPSERT (вставь или обнови). Требует UNIQUE или PRIMARY KEY на колонке.
INSERT— это команда «добавить новую строку в таблицу». Самая базовая операция: пока в таблицу никто ничего не вставил, она пустая, и вSELECTвыбирать нечего.Если возвращаться к аналогии с блокнотом контактов:
INSERT— это «новая страница, новый человек: имя такое, телефон такой, email такой».Зачем нужен INSERT
Любые данные в БД когда-то появились через
INSERT. Регистрация пользователя, оформление заказа, новый комментарий — всё это под капотомINSERT INTO .... Без него таблицы остаются пустыми.Базовый синтаксис
INSERT INTO users (email, display_name, country) VALUES ('alice@example.com', 'Аня', 'RU');По частям:
INSERT INTO users— в какую таблицу вставляем.(email, display_name, country)— список колонок, которые заполняем.VALUES (...)— значения в том же порядке, что и колонки.Колонки указывать явно — это правильная привычка. Даже если ты сейчас вставляешь все поля, через месяц в таблицу добавят новую колонку, и
INSERT INTO users VALUES (...)без списка колонок упадёт неожиданно.Пример с таблицей
Пустая таблица
users:Делаем
INSERT:INSERT INTO users (email, display_name, country) VALUES ('anya@example.com', 'Аня', 'RU');Результат:
idмы не передавали — он автоматически стал1, потому что объявлен какSERIAL(автоинкремент). Следующий INSERT получитid = 2.Колонки с
DEFAULT(created_at TIMESTAMPTZ DEFAULT NOW()и им подобные) тоже можно не передавать — Postgres сам подставит дефолт.Batch-вставка — несколько строк за раз
Если нужно вставить много строк, не делай N отдельных
INSERT— это медленно. Используй batch:INSERT INTO users (email, display_name, country) VALUES ('anya@example.com', 'Аня', 'RU'), ('bob@example.com', 'Боб', 'US'), ('vera@example.com', 'Вера', 'BY');Один запрос, одна транзакция, один проход по индексам — резко быстрее, чем три отдельных INSERT'а. Если льёшь данные тысячами — обязательно batch'ом. Сетевая задержка часто доминирует над временем самой вставки.
На практике батчами по 1000-10000 строк работают комфортно.
INSERT FROM SELECT — копирование из другой таблицы
Иногда нужно скопировать данные из одной таблицы в другую (или преобразовать на лету):
-- Перенести «архивные» заказы в отдельную таблицу INSERT INTO orders_archive (id, customer_id, amount, archived_at) SELECT id, customer_id, amount, NOW() FROM orders WHERE status = 'completed' AND created_at < '2024-01-01';Это транзакционно — либо все строки скопируются, либо ни одной (если хоть одна нарушит constraint). Удобно для миграций данных и backfill.
RETURNING — получить вставленные значения
PostgreSQL умеет возвращать вставленные строки прямо из
INSERT:INSERT INTO orders (customer_id, amount) VALUES (5, 100) RETURNING id, created_at; -- → возвращает строку с авто-сгенерированным id и created_atБез
RETURNINGпришлось бы делать второй запрос (SELECT MAX(id)илиcurrval('orders_id_seq')), и между ними возможна гонка с другой транзакцией.RETURNINGрешает атомарно.RETURNINGесть в PostgreSQL, SQLite (3.35+), Oracle. В MySQL — нет (тамLAST_INSERT_ID()).UPSERT через ON CONFLICT
«Вставь, а если уже есть — обнови» — типичная задача для миграций, импортов, идемпотентных операций. PostgreSQL решает через
ON CONFLICT:INSERT INTO users (email, display_name, last_login_at) VALUES ('anya@example.com', 'Аня', NOW()) ON CONFLICT (email) DO UPDATE SET last_login_at = EXCLUDED.last_login_at, display_name = EXCLUDED.display_name;Логика:
email— выполняетсяUPDATEс указанными полями.EXCLUDEDсодержит значения, которые мы пытались вставить (изVALUES).Альтернативные формы:
ON CONFLICT (email) DO NOTHING— игнорировать дубликаты, не считать ошибкой.ON CONFLICT ON CONSTRAINT users_email_key DO UPDATE ...— указать конкретный constraint по имени.WHEREвUPDATE— условный апдейт:DO UPDATE SET ... WHERE users.last_login_at < EXCLUDED.last_login_at(обновляем только если новая дата свежее).ON CONFLICTтребует, чтобы существовало уникальное ограничение (UNIQUE,PRIMARY KEYилиEXCLUDE) на колонке. Без него Postgres не знает, что считать «конфликтом».В MySQL аналог —
INSERT ... ON DUPLICATE KEY UPDATE. В стандарте SQL естьMERGE, и Postgres поддерживает его с 15-й версии — ноON CONFLICTпока выразительнее.Частые ошибки новичков
1. Пропущенный список колонок.
INSERT INTO users VALUES ('alice@...', 'Аня', 'RU')без списка зависит от порядка колонок в таблице. Кто-то добавитALTER TABLE ... ADD COLUMN status— и старый код сломается. Всегда пиши список колонок.2. Несоответствие количества значений.
INSERT INTO t (a, b, c) VALUES (1, 2)— три колонки, два значения, ошибка. Иногда видно сразу, иногда нет (если values динамические из приложения).3. Кавычки в строках. Строки и даты — одинарные кавычки. Двойные кавычки в PostgreSQL — для имён колонок:
-- Правильно INSERT INTO users (name) VALUES ('Аня'); -- Неправильно — Postgres решит, что Аня это имя колонки INSERT INTO users (name) VALUES ("Аня");4. UPSERT без
ON CONFLICT. Сценарий «проверь существование → если нет, INSERT» в коде приложения не атомарен — между проверкой и вставкой другая транзакция может вставить ту же строку. Уникальное ограничение +ON CONFLICT DO NOTHING— единственный надёжный способ.5.
idпередан вручную, потом ломается автоинкремент. Если ты явно вставилid = 1000, а sequence стоит на5— следующийINSERTбезidпопробует поставитьid = 6. И всё хорошо. Но если был insertid = 1, и кто-то уже до тебя вставил такой — конфликт. Лучше вообще не передавать id, пусть БД сама.6.
INSERTмиллиарда строк одним запросом. PostgreSQL примет, но WAL-лог распухнет, репликация затормозит, всё. Bulk-импорт — батчами, либо черезCOPY FROM(специальная команда для bulk-load — в десятки раз быстрее любого INSERT).Мини-резюме
INSERT INTO table (cols) VALUES (vals)— добавляет новую строку.ALTER TABLE.VALUES (..), (..), (..)(batch).INSERT FROM SELECT— для копирования и backfill.RETURNING— забрать сгенерированныйidи другие значения сразу.ON CONFLICT— UPSERT (вставь или обнови). ТребуетUNIQUEилиPRIMARY KEYна колонке.