INSERT — это команда SQL, которая добавляет новые строки в таблицу.
Пока в таблицу ничего не вставили, она пустая. А значит, даже самый красивый SELECT ничего из неё не достанет. Таблица без данных — как блокнот без записей: структура есть, страницы есть, а самих контактов пока нет.
Представь таблицу users как список пользователей. Когда человек регистрируется на сайте, база данных должна запомнить его email, имя, страну, дату регистрации. Вот в этот момент и появляется INSERT: «добавь новую запись вот с такими значениями».
Зачем нужен INSERT
Почти любые данные в приложении когда-то появились через вставку:
- пользователь зарегистрировался;
- клиент оформил заказ;
- студент отправил решение задачи;
- покупатель оставил отзыв;
- система записала событие в лог;
- администратор добавил новый тариф.
Всё это на уровне базы обычно превращается в команду вида:
INSERT INTO table_name (column_1, column_2)
VALUES (value_1, value_2);
То есть смысл очень простой: вставить в такую-то таблицу новую строку с такими-то значениями.
Базовый синтаксис INSERT
Посмотрим на обычную вставку пользователя:
INSERT INTO users (email, display_name, country)
VALUES ('alice@example.com', 'Alice', 'RU');
Разберём по частям.
INSERT INTO users говорит базе: вставляем данные в таблицу users.
(email, display_name, country) — это список колонок, которые мы хотим заполнить.
VALUES (...) — это значения для этих колонок. Очень важно: значения идут в том же порядке, что и колонки.
То есть здесь:
(email, display_name, country)
соответствует:
('alice@example.com', 'Alice', 'RU')
Получается:
| Колонка |
Значение |
email |
alice@example.com |
display_name |
Alice |
country |
RU |
Главное правило для новичка: всегда указывай список колонок явно.
Плохо писать так:
INSERT INTO users
VALUES ('alice@example.com', 'Alice', 'RU');
На первый взгляд короче. Но такой запрос зависит от порядка колонок в таблице. Сегодня он работает, а завтра кто-то добавит новую колонку через ALTER TABLE, и старый код внезапно сломается.
Хорошая привычка:
INSERT INTO users (email, display_name, country)
VALUES ('alice@example.com', 'Alice', 'RU');
Так запрос читается понятнее и переживает изменения таблицы гораздо спокойнее.
Пример с таблицей
Допустим, у нас есть пустая таблица users.
| id |
email |
display_name |
country |
| нет строк |
|
|
|
Добавим первого пользователя:
INSERT INTO users (email, display_name, country)
VALUES ('anya@example.com', 'Anya', 'RU');
После вставки таблица может выглядеть так:
Обрати внимание: в запросе мы не передавали id, но он появился сам.
Так часто делают в таблицах: колонка id создаётся как автоинкрементная. В PostgreSQL для этого могут использоваться SERIAL, BIGSERIAL или IDENTITY. База сама выдаёт следующий номер: первой строке 1, второй 2, третьей 3.
Поэтому обычно id руками не вставляют. Это работа базы данных.
Колонки с DEFAULT
У таблицы могут быть колонки со значением по умолчанию.
Например:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL,
display_name TEXT NOT NULL,
country TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Колонка created_at получает значение автоматически: если мы её не указали, PostgreSQL сам подставит текущее время.
Поэтому такой запрос нормальный:
INSERT INTO users (email, display_name, country)
VALUES ('bob@example.com', 'Bob', 'US');
В таблицу попадут email, имя, страна, а id и created_at база заполнит сама.
Это удобно: приложению не нужно каждый раз думать, какой следующий id и какую дату регистрации поставить.
Вставка нескольких строк за раз
Иногда нужно добавить не одну строку, а сразу несколько. Например, загрузить список пользователей, товаров или справочников.
Можно сделать три отдельных запроса:
INSERT INTO users (email, display_name, country)
VALUES ('anya@example.com', 'Anya', 'RU');
INSERT INTO users (email, display_name, country)
VALUES ('bob@example.com', 'Bob', 'US');
INSERT INTO users (email, display_name, country)
VALUES ('vera@example.com', 'Vera', 'BY');
Но лучше вставить всё одним запросом:
INSERT INTO users (email, display_name, country)
VALUES
('anya@example.com', 'Anya', 'RU'),
('bob@example.com', 'Bob', 'US'),
('vera@example.com', 'Vera', 'BY');
Это называется batch-вставка, то есть вставка пачкой.
Почему так лучше?
База получает один запрос вместо трёх. Меньше сетевых походов между приложением и сервером. Индексы обновляются эффективнее. Вся пачка вставляется как одна операция.
Если строк много, разница становится заметной. На практике данные часто грузят пачками по 1000–10000 строк, но точный размер зависит от базы, таблицы, индексов, ограничений и нагрузки.
Главная мысль простая: если нужно вставить много строк, не отправляй по одному INSERT на каждую строку без необходимости. Пачки почти всегда быстрее.
INSERT FROM SELECT: вставка результата запроса
INSERT умеет вставлять не только готовые значения из VALUES. Он может взять строки из результата другого SELECT.
Например, у нас есть таблица orders с заказами и таблица orders_archive для архива. Мы хотим перенести старые завершённые заказы в архивную таблицу.
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';
Здесь происходит важная вещь: SELECT сначала находит подходящие строки, а INSERT вставляет результат в другую таблицу.
Такой подход используют для:
- архивирования данных;
- миграций;
- заполнения новых таблиц;
- backfill-операций;
- переноса данных между похожими структурами;
- подготовки агрегированных или очищенных данных.
Например, можно не просто копировать значения, а сразу преобразовывать их:
INSERT INTO user_emails (user_id, email_lower)
SELECT id, LOWER(email)
FROM users
WHERE email IS NOT NULL;
Это читается почти как обычная инструкция: «возьми пользователей с email, приведи email к нижнему регистру и вставь результат в другую таблицу».
INSERT работает транзакционно
В нормальных базах данных INSERT участвует в транзакциях.
Это значит: если мы вставляем несколько строк и одна из них нарушает ограничение, база может откатить всю операцию. Не получится странная ситуация, где половина данных вставилась, а половина потерялась.
Например, если колонка email уникальная, а мы пытаемся вставить два одинаковых email, база не должна молча сделать вид, что всё хорошо. Она сообщит об ошибке.
INSERT INTO users (email, display_name, country)
VALUES
('alice@example.com', 'Alice', 'RU'),
('alice@example.com', 'Alice Clone', 'RU');
Если на email есть уникальное ограничение, такой запрос завершится ошибкой. Это не вредность базы, а защита данных от хаоса.
RETURNING: получить вставленную строку сразу
Часто после вставки нужно узнать, какой id база выдала новой строке.
Например, приложение создаёт заказ. Оно вставляет заказ в таблицу orders, а потом хочет показать пользователю номер заказа или создать связанные строки в order_items.
В PostgreSQL для этого есть RETURNING.
INSERT INTO orders (customer_id, amount)
VALUES (5, 100)
RETURNING id, created_at;
Такой запрос не просто вставит строку, но и сразу вернёт нужные значения.
Например:
| id |
created_at |
| 42 |
2026-06-26 10:15:00+00 |
Это намного удобнее и безопаснее, чем делать отдельный запрос после вставки.
Плохая идея — пытаться получить последний id через что-то вроде:
SELECT MAX(id)
FROM orders;
Почему плохо? Потому что в базе могут одновременно работать другие пользователи. Пока ты вставил свой заказ, другой пользователь мог вставить следующий. В итоге MAX(id) может вернуть не то, что ты ожидаешь.
RETURNING решает задачу аккуратно: база возвращает именно те значения, которые относятся к только что вставленной строке.
RETURNING особенно полезен для авто-сгенерированных колонок:
id;
created_at;
- вычисляемых значений;
- значений по умолчанию;
- данных, изменённых триггерами.
Можно вернуть даже всю строку:
INSERT INTO users (email, display_name, country)
VALUES ('kate@example.com', 'Kate', 'RU')
RETURNING *;
UPSERT: вставить или обновить
Очень частая задача: «добавь строку, но если такая уже есть — обнови её».
Например, пользователь уже существует по email. Мы не хотим создавать дубль. Мы хотим обновить его имя или дату последнего входа.
В PostgreSQL это делается через ON CONFLICT.
INSERT INTO users (email, display_name, last_login_at)
VALUES ('anya@example.com', 'Anya', NOW())
ON CONFLICT (email) DO UPDATE
SET
last_login_at = EXCLUDED.last_login_at,
display_name = EXCLUDED.display_name;
Такой запрос называется upsert: смесь INSERT и UPDATE.
Логика такая:
- База пытается вставить новую строку.
- Если строки с таким
email ещё нет, вставка проходит успешно.
- Если строка с таким
email уже есть, возникает конфликт.
- Вместо ошибки выполняется
UPDATE.
Здесь важна псевдо-таблица EXCLUDED.
EXCLUDED хранит значения, которые мы пытались вставить.
В запросе:
VALUES ('anya@example.com', 'Anya', NOW())
мы пытались вставить новое имя и новое время входа. Если произошёл конфликт, эти значения можно взять через EXCLUDED.display_name и EXCLUDED.last_login_at.
То есть строка:
display_name = EXCLUDED.display_name
означает: «обнови имя на то значение, которое пришло в новой вставке».
ON CONFLICT DO NOTHING
Иногда при конфликте ничего обновлять не нужно. Нужно просто не считать дубль ошибкой.
Например, мы импортируем email-адреса из файла. Если какой-то email уже есть в базе, спокойно пропускаем его.
INSERT INTO users (email, display_name, country)
VALUES ('anya@example.com', 'Anya', 'RU')
ON CONFLICT (email) DO NOTHING;
Это читается почти человеческим языком: «вставь пользователя, а если email уже занят — ничего не делай».
Такой подход часто используют для идемпотентных операций.
Идемпотентность означает, что операцию можно повторить несколько раз, и результат не сломается. Например, если импорт запустили дважды, база не должна создать одинаковых пользователей.
ON CONFLICT требует уникальности
ON CONFLICT (email) работает только если база понимает, что по email вообще может быть конфликт.
Для этого на колонке должно быть уникальное ограничение:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
country TEXT
);
Или отдельный уникальный индекс:
CREATE UNIQUE INDEX users_email_key
ON users (email);
Без UNIQUE, PRIMARY KEY или подходящего ограничения PostgreSQL не знает, что считать конфликтом. Для базы две строки с одинаковым email — это просто две строки, если ты заранее не сказал ей: «email должен быть уникальным».
Условный UPSERT
Иногда обновлять строку нужно не всегда.
Например, мы хотим обновить last_login_at только если новое время свежее старого.
INSERT INTO users (email, display_name, last_login_at)
VALUES ('anya@example.com', 'Anya', '2026-06-26 10:00:00')
ON CONFLICT (email) DO UPDATE
SET
last_login_at = EXCLUDED.last_login_at,
display_name = EXCLUDED.display_name
WHERE users.last_login_at < EXCLUDED.last_login_at;
Так база не перетрёт свежие данные более старыми.
Это полезно при импортах, синхронизациях и повторной обработке событий, когда данные могут приходить не строго по порядку.
Чем ON CONFLICT отличается от проверки перед INSERT
Новички часто пишут такую логику в приложении:
- Проверить, есть ли пользователь с таким email.
- Если нет — сделать
INSERT.
- Если есть — сделать
UPDATE или ничего не делать.
На словах звучит нормально. Но в реальной многопользовательской базе это опасно.
Представь две транзакции, которые одновременно проверили email:
SELECT id
FROM users
WHERE email = 'anya@example.com';
Обе увидели: строки нет.
Потом обе попытались вставить:
INSERT INTO users (email, display_name, country)
VALUES ('anya@example.com', 'Anya', 'RU');
Если нет уникального ограничения, можно получить дубль. Если уникальное ограничение есть, одна транзакция упадёт с ошибкой.
Поэтому надёжный способ — не пытаться вручную обмануть гонку, а использовать связку:
- уникальное ограничение;
ON CONFLICT DO NOTHING;
- или
ON CONFLICT DO UPDATE.
Тогда база сама разрулит конфликт атомарно.
Как INSERT выглядит в разных базах
Основная идея INSERT везде похожа:
INSERT INTO users (email, display_name)
VALUES ('alice@example.com', 'Alice');
Но отдельные возможности зависят от СУБД.
В PostgreSQL есть удобные RETURNING и ON CONFLICT.
В MySQL для upsert используют конструкцию:
INSERT INTO users (email, display_name)
VALUES ('alice@example.com', 'Alice')
ON DUPLICATE KEY UPDATE
display_name = VALUES(display_name);
В SQL-стандарте есть команда MERGE, которая тоже решает задачи вставки или обновления в зависимости от совпадения строк. PostgreSQL поддерживает MERGE с 15-й версии, но для простого upsert в PostgreSQL часто удобнее и привычнее использовать ON CONFLICT.
COPY: когда INSERT уже не лучший выбор
INSERT отлично подходит для обычных вставок: одна строка, несколько строк, небольшие пачки, результат SELECT.
Но если нужно загрузить огромный объём данных — сотни тысяч или миллионы строк — в PostgreSQL часто используют специальную команду COPY.
Идея такая: данные лежат в файле, а база загружает их напрямую.
COPY users (email, display_name, country)
FROM '/tmp/users.csv'
WITH (FORMAT csv, HEADER true);
COPY обычно быстрее обычных INSERT, потому что специально создан для массовой загрузки данных.
Но для начинающего важно запомнить не саму команду, а принцип:
- обычные записи из приложения —
INSERT;
- несколько строк — batch-вставка;
- очень большие загрузки из файлов — часто
COPY.
Частые ошибки с INSERT
Не указывать список колонок
Плохой вариант:
INSERT INTO users
VALUES ('alice@example.com', 'Alice', 'RU');
Хороший вариант:
INSERT INTO users (email, display_name, country)
VALUES ('alice@example.com', 'Alice', 'RU');
Список колонок делает запрос понятным и устойчивым к изменениям таблицы.
Перепутать порядок значений
Такой запрос формально может выглядеть нормально, но данные попадут не туда:
INSERT INTO users (email, display_name, country)
VALUES ('Alice', 'alice@example.com', 'RU');
База не всегда поймёт, что ты перепутал email и имя. Она просто вставит строки в указанном порядке.
Правильно:
INSERT INTO users (email, display_name, country)
VALUES ('alice@example.com', 'Alice', 'RU');
Сначала колонка email — значит, сначала значение email. Потом display_name — значит, потом имя.
Передать не то количество значений
Если указали три колонки, значений тоже должно быть три.
INSERT INTO users (email, display_name, country)
VALUES ('alice@example.com', 'Alice');
Здесь три колонки, но только два значения. База выдаст ошибку.
Правильно:
INSERT INTO users (email, display_name, country)
VALUES ('alice@example.com', 'Alice', 'RU');
Использовать двойные кавычки для строк
В SQL строки пишутся в одинарных кавычках.
Правильно:
INSERT INTO users (display_name)
VALUES ('Alice');
Неправильно:
INSERT INTO users (display_name)
VALUES ("Alice");
В PostgreSQL двойные кавычки используются для имён таблиц и колонок, а не для строковых значений. Поэтому база может решить, что "Alice" — это имя колонки.
Вставлять id вручную без необходимости
Почти всегда лучше так:
INSERT INTO users (email, display_name)
VALUES ('alice@example.com', 'Alice');
А не так:
INSERT INTO users (id, email, display_name)
VALUES (1, 'alice@example.com', 'Alice');
Если id генерируется автоматически, пусть база сама его выдаёт.
Ручная вставка id может привести к конфликтам и путанице с последовательностью автоинкремента.
Делать слишком огромный INSERT
Технически можно попытаться вставить гигантскую пачку одним запросом. Но это не всегда хорошая идея.
Большой INSERT может:
- раздуть WAL-журнал;
- создать долгую транзакцию;
- задержать репликацию;
- сильнее нагрузить индексы;
- усложнить откат при ошибке.
Для больших загрузок лучше использовать разумные batch-вставки или специальные инструменты вроде COPY.
Как читать INSERT глазами
Когда видишь запрос:
INSERT INTO orders (customer_id, amount, status)
VALUES (7, 250, 'new')
RETURNING id;
читай его так:
«В таблицу orders добавь новую строку. В колонку customer_id положи 7, в amount — 250, в status — new. После вставки верни id, который база выдала этой строке».
А такой запрос:
INSERT INTO users (email, display_name, last_login_at)
VALUES ('alice@example.com', 'Alice', NOW())
ON CONFLICT (email) DO UPDATE
SET last_login_at = EXCLUDED.last_login_at;
читай так:
«Попробуй добавить пользователя. Если пользователь с таким email уже есть, не падай с ошибкой, а обнови ему дату последнего входа».
Когда начинаешь читать SQL таким образом, запросы перестают выглядеть как магические заклинания. Они становятся обычными инструкциями для базы данных.
Главное про INSERT
INSERT добавляет новые строки в таблицу.
Базовая форма выглядит так:
INSERT INTO table_name (column_1, column_2)
VALUES (value_1, value_2);
Список колонок лучше всегда указывать явно. Это делает запрос понятнее и защищает от проблем после изменения структуры таблицы.
Для нескольких строк можно использовать batch-вставку:
INSERT INTO users (email, display_name)
VALUES
('alice@example.com', 'Alice'),
('bob@example.com', 'Bob');
Для вставки результата другого запроса используют INSERT INTO ... SELECT.
В PostgreSQL RETURNING позволяет сразу получить вставленные значения, например новый id.
ON CONFLICT помогает сделать upsert: вставить строку, а при конфликте обновить её или спокойно пропустить.
Главная идея простая: INSERT — это дверь, через которую новые данные попадают в таблицу. Чем аккуратнее ты указываешь колонки, значения, ограничения и поведение при конфликтах, тем меньше сюрпризов будет в базе.
INSERT— это команда SQL, которая добавляет новые строки в таблицу.Пока в таблицу ничего не вставили, она пустая. А значит, даже самый красивый
SELECTничего из неё не достанет. Таблица без данных — как блокнот без записей: структура есть, страницы есть, а самих контактов пока нет.Представь таблицу
usersкак список пользователей. Когда человек регистрируется на сайте, база данных должна запомнить его email, имя, страну, дату регистрации. Вот в этот момент и появляетсяINSERT: «добавь новую запись вот с такими значениями».Зачем нужен INSERT
Почти любые данные в приложении когда-то появились через вставку:
Всё это на уровне базы обычно превращается в команду вида:
INSERT INTO table_name (column_1, column_2) VALUES (value_1, value_2);То есть смысл очень простой: вставить в такую-то таблицу новую строку с такими-то значениями.
Базовый синтаксис INSERT
Посмотрим на обычную вставку пользователя:
INSERT INTO users (email, display_name, country) VALUES ('alice@example.com', 'Alice', 'RU');Разберём по частям.
INSERT INTO usersговорит базе: вставляем данные в таблицуusers.(email, display_name, country)— это список колонок, которые мы хотим заполнить.VALUES (...)— это значения для этих колонок. Очень важно: значения идут в том же порядке, что и колонки.То есть здесь:
соответствует:
('alice@example.com', 'Alice', 'RU')Получается:
emailalice@example.comdisplay_nameAlicecountryRUГлавное правило для новичка: всегда указывай список колонок явно.
Плохо писать так:
INSERT INTO users VALUES ('alice@example.com', 'Alice', 'RU');На первый взгляд короче. Но такой запрос зависит от порядка колонок в таблице. Сегодня он работает, а завтра кто-то добавит новую колонку через
ALTER TABLE, и старый код внезапно сломается.Хорошая привычка:
INSERT INTO users (email, display_name, country) VALUES ('alice@example.com', 'Alice', 'RU');Так запрос читается понятнее и переживает изменения таблицы гораздо спокойнее.
Пример с таблицей
Допустим, у нас есть пустая таблица
users.Добавим первого пользователя:
INSERT INTO users (email, display_name, country) VALUES ('anya@example.com', 'Anya', 'RU');После вставки таблица может выглядеть так:
Обрати внимание: в запросе мы не передавали
id, но он появился сам.Так часто делают в таблицах: колонка
idсоздаётся как автоинкрементная. В PostgreSQL для этого могут использоватьсяSERIAL,BIGSERIALилиIDENTITY. База сама выдаёт следующий номер: первой строке1, второй2, третьей3.Поэтому обычно
idруками не вставляют. Это работа базы данных.Колонки с DEFAULT
У таблицы могут быть колонки со значением по умолчанию.
Например:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT NOT NULL, display_name TEXT NOT NULL, country TEXT, created_at TIMESTAMPTZ DEFAULT NOW() );Колонка
created_atполучает значение автоматически: если мы её не указали, PostgreSQL сам подставит текущее время.Поэтому такой запрос нормальный:
INSERT INTO users (email, display_name, country) VALUES ('bob@example.com', 'Bob', 'US');В таблицу попадут email, имя, страна, а
idиcreated_atбаза заполнит сама.Это удобно: приложению не нужно каждый раз думать, какой следующий
idи какую дату регистрации поставить.Вставка нескольких строк за раз
Иногда нужно добавить не одну строку, а сразу несколько. Например, загрузить список пользователей, товаров или справочников.
Можно сделать три отдельных запроса:
INSERT INTO users (email, display_name, country) VALUES ('anya@example.com', 'Anya', 'RU'); INSERT INTO users (email, display_name, country) VALUES ('bob@example.com', 'Bob', 'US'); INSERT INTO users (email, display_name, country) VALUES ('vera@example.com', 'Vera', 'BY');Но лучше вставить всё одним запросом:
INSERT INTO users (email, display_name, country) VALUES ('anya@example.com', 'Anya', 'RU'), ('bob@example.com', 'Bob', 'US'), ('vera@example.com', 'Vera', 'BY');Это называется batch-вставка, то есть вставка пачкой.
Почему так лучше?
База получает один запрос вместо трёх. Меньше сетевых походов между приложением и сервером. Индексы обновляются эффективнее. Вся пачка вставляется как одна операция.
Если строк много, разница становится заметной. На практике данные часто грузят пачками по 1000–10000 строк, но точный размер зависит от базы, таблицы, индексов, ограничений и нагрузки.
Главная мысль простая: если нужно вставить много строк, не отправляй по одному
INSERTна каждую строку без необходимости. Пачки почти всегда быстрее.INSERT FROM SELECT: вставка результата запроса
INSERTумеет вставлять не только готовые значения изVALUES. Он может взять строки из результата другогоSELECT.Например, у нас есть таблица
ordersс заказами и таблицаorders_archiveдля архива. Мы хотим перенести старые завершённые заказы в архивную таблицу.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';Здесь происходит важная вещь:
SELECTсначала находит подходящие строки, аINSERTвставляет результат в другую таблицу.Такой подход используют для:
Например, можно не просто копировать значения, а сразу преобразовывать их:
INSERT INTO user_emails (user_id, email_lower) SELECT id, LOWER(email) FROM users WHERE email IS NOT NULL;Это читается почти как обычная инструкция: «возьми пользователей с email, приведи email к нижнему регистру и вставь результат в другую таблицу».
INSERT работает транзакционно
В нормальных базах данных
INSERTучаствует в транзакциях.Это значит: если мы вставляем несколько строк и одна из них нарушает ограничение, база может откатить всю операцию. Не получится странная ситуация, где половина данных вставилась, а половина потерялась.
Например, если колонка
emailуникальная, а мы пытаемся вставить два одинаковых email, база не должна молча сделать вид, что всё хорошо. Она сообщит об ошибке.INSERT INTO users (email, display_name, country) VALUES ('alice@example.com', 'Alice', 'RU'), ('alice@example.com', 'Alice Clone', 'RU');Если на
emailесть уникальное ограничение, такой запрос завершится ошибкой. Это не вредность базы, а защита данных от хаоса.RETURNING: получить вставленную строку сразу
Часто после вставки нужно узнать, какой
idбаза выдала новой строке.Например, приложение создаёт заказ. Оно вставляет заказ в таблицу
orders, а потом хочет показать пользователю номер заказа или создать связанные строки вorder_items.В PostgreSQL для этого есть
RETURNING.INSERT INTO orders (customer_id, amount) VALUES (5, 100) RETURNING id, created_at;Такой запрос не просто вставит строку, но и сразу вернёт нужные значения.
Например:
Это намного удобнее и безопаснее, чем делать отдельный запрос после вставки.
Плохая идея — пытаться получить последний
idчерез что-то вроде:SELECT MAX(id) FROM orders;Почему плохо? Потому что в базе могут одновременно работать другие пользователи. Пока ты вставил свой заказ, другой пользователь мог вставить следующий. В итоге
MAX(id)может вернуть не то, что ты ожидаешь.RETURNINGрешает задачу аккуратно: база возвращает именно те значения, которые относятся к только что вставленной строке.RETURNINGособенно полезен для авто-сгенерированных колонок:id;created_at;Можно вернуть даже всю строку:
INSERT INTO users (email, display_name, country) VALUES ('kate@example.com', 'Kate', 'RU') RETURNING *;UPSERT: вставить или обновить
Очень частая задача: «добавь строку, но если такая уже есть — обнови её».
Например, пользователь уже существует по email. Мы не хотим создавать дубль. Мы хотим обновить его имя или дату последнего входа.
В PostgreSQL это делается через
ON CONFLICT.INSERT INTO users (email, display_name, last_login_at) VALUES ('anya@example.com', 'Anya', NOW()) ON CONFLICT (email) DO UPDATE SET last_login_at = EXCLUDED.last_login_at, display_name = EXCLUDED.display_name;Такой запрос называется upsert: смесь
INSERTиUPDATE.Логика такая:
emailещё нет, вставка проходит успешно.emailуже есть, возникает конфликт.UPDATE.Здесь важна псевдо-таблица
EXCLUDED.EXCLUDEDхранит значения, которые мы пытались вставить.В запросе:
VALUES ('anya@example.com', 'Anya', NOW())мы пытались вставить новое имя и новое время входа. Если произошёл конфликт, эти значения можно взять через
EXCLUDED.display_nameиEXCLUDED.last_login_at.То есть строка:
display_name = EXCLUDED.display_nameозначает: «обнови имя на то значение, которое пришло в новой вставке».
ON CONFLICT DO NOTHING
Иногда при конфликте ничего обновлять не нужно. Нужно просто не считать дубль ошибкой.
Например, мы импортируем email-адреса из файла. Если какой-то email уже есть в базе, спокойно пропускаем его.
INSERT INTO users (email, display_name, country) VALUES ('anya@example.com', 'Anya', 'RU') ON CONFLICT (email) DO NOTHING;Это читается почти человеческим языком: «вставь пользователя, а если email уже занят — ничего не делай».
Такой подход часто используют для идемпотентных операций.
Идемпотентность означает, что операцию можно повторить несколько раз, и результат не сломается. Например, если импорт запустили дважды, база не должна создать одинаковых пользователей.
ON CONFLICT требует уникальности
ON CONFLICT (email)работает только если база понимает, что поemailвообще может быть конфликт.Для этого на колонке должно быть уникальное ограничение:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT UNIQUE NOT NULL, display_name TEXT NOT NULL, country TEXT );Или отдельный уникальный индекс:
CREATE UNIQUE INDEX users_email_key ON users (email);Без
UNIQUE,PRIMARY KEYили подходящего ограничения PostgreSQL не знает, что считать конфликтом. Для базы две строки с одинаковым email — это просто две строки, если ты заранее не сказал ей: «email должен быть уникальным».Условный UPSERT
Иногда обновлять строку нужно не всегда.
Например, мы хотим обновить
last_login_atтолько если новое время свежее старого.INSERT INTO users (email, display_name, last_login_at) VALUES ('anya@example.com', 'Anya', '2026-06-26 10:00:00') ON CONFLICT (email) DO UPDATE SET last_login_at = EXCLUDED.last_login_at, display_name = EXCLUDED.display_name WHERE users.last_login_at < EXCLUDED.last_login_at;Так база не перетрёт свежие данные более старыми.
Это полезно при импортах, синхронизациях и повторной обработке событий, когда данные могут приходить не строго по порядку.
Чем ON CONFLICT отличается от проверки перед INSERT
Новички часто пишут такую логику в приложении:
INSERT.UPDATEили ничего не делать.На словах звучит нормально. Но в реальной многопользовательской базе это опасно.
Представь две транзакции, которые одновременно проверили email:
SELECT id FROM users WHERE email = 'anya@example.com';Обе увидели: строки нет.
Потом обе попытались вставить:
INSERT INTO users (email, display_name, country) VALUES ('anya@example.com', 'Anya', 'RU');Если нет уникального ограничения, можно получить дубль. Если уникальное ограничение есть, одна транзакция упадёт с ошибкой.
Поэтому надёжный способ — не пытаться вручную обмануть гонку, а использовать связку:
ON CONFLICT DO NOTHING;ON CONFLICT DO UPDATE.Тогда база сама разрулит конфликт атомарно.
Как INSERT выглядит в разных базах
Основная идея
INSERTвезде похожа:INSERT INTO users (email, display_name) VALUES ('alice@example.com', 'Alice');Но отдельные возможности зависят от СУБД.
В PostgreSQL есть удобные
RETURNINGиON CONFLICT.В MySQL для upsert используют конструкцию:
INSERT INTO users (email, display_name) VALUES ('alice@example.com', 'Alice') ON DUPLICATE KEY UPDATE display_name = VALUES(display_name);В SQL-стандарте есть команда
MERGE, которая тоже решает задачи вставки или обновления в зависимости от совпадения строк. PostgreSQL поддерживаетMERGEс 15-й версии, но для простого upsert в PostgreSQL часто удобнее и привычнее использоватьON CONFLICT.COPY: когда INSERT уже не лучший выбор
INSERTотлично подходит для обычных вставок: одна строка, несколько строк, небольшие пачки, результатSELECT.Но если нужно загрузить огромный объём данных — сотни тысяч или миллионы строк — в PostgreSQL часто используют специальную команду
COPY.Идея такая: данные лежат в файле, а база загружает их напрямую.
COPY users (email, display_name, country) FROM '/tmp/users.csv' WITH (FORMAT csv, HEADER true);COPYобычно быстрее обычныхINSERT, потому что специально создан для массовой загрузки данных.Но для начинающего важно запомнить не саму команду, а принцип:
INSERT;COPY.Частые ошибки с INSERT
Не указывать список колонок
Плохой вариант:
INSERT INTO users VALUES ('alice@example.com', 'Alice', 'RU');Хороший вариант:
INSERT INTO users (email, display_name, country) VALUES ('alice@example.com', 'Alice', 'RU');Список колонок делает запрос понятным и устойчивым к изменениям таблицы.
Перепутать порядок значений
Такой запрос формально может выглядеть нормально, но данные попадут не туда:
INSERT INTO users (email, display_name, country) VALUES ('Alice', 'alice@example.com', 'RU');База не всегда поймёт, что ты перепутал email и имя. Она просто вставит строки в указанном порядке.
Правильно:
INSERT INTO users (email, display_name, country) VALUES ('alice@example.com', 'Alice', 'RU');Сначала колонка
email— значит, сначала значение email. Потомdisplay_name— значит, потом имя.Передать не то количество значений
Если указали три колонки, значений тоже должно быть три.
INSERT INTO users (email, display_name, country) VALUES ('alice@example.com', 'Alice');Здесь три колонки, но только два значения. База выдаст ошибку.
Правильно:
INSERT INTO users (email, display_name, country) VALUES ('alice@example.com', 'Alice', 'RU');Использовать двойные кавычки для строк
В SQL строки пишутся в одинарных кавычках.
Правильно:
INSERT INTO users (display_name) VALUES ('Alice');Неправильно:
INSERT INTO users (display_name) VALUES ("Alice");В PostgreSQL двойные кавычки используются для имён таблиц и колонок, а не для строковых значений. Поэтому база может решить, что
"Alice"— это имя колонки.Вставлять id вручную без необходимости
Почти всегда лучше так:
INSERT INTO users (email, display_name) VALUES ('alice@example.com', 'Alice');А не так:
INSERT INTO users (id, email, display_name) VALUES (1, 'alice@example.com', 'Alice');Если
idгенерируется автоматически, пусть база сама его выдаёт.Ручная вставка
idможет привести к конфликтам и путанице с последовательностью автоинкремента.Делать слишком огромный INSERT
Технически можно попытаться вставить гигантскую пачку одним запросом. Но это не всегда хорошая идея.
Большой
INSERTможет:Для больших загрузок лучше использовать разумные batch-вставки или специальные инструменты вроде
COPY.Как читать INSERT глазами
Когда видишь запрос:
INSERT INTO orders (customer_id, amount, status) VALUES (7, 250, 'new') RETURNING id;читай его так:
«В таблицу
ordersдобавь новую строку. В колонкуcustomer_idположи7, вamount—250, вstatus—new. После вставки верниid, который база выдала этой строке».А такой запрос:
INSERT INTO users (email, display_name, last_login_at) VALUES ('alice@example.com', 'Alice', NOW()) ON CONFLICT (email) DO UPDATE SET last_login_at = EXCLUDED.last_login_at;читай так:
«Попробуй добавить пользователя. Если пользователь с таким email уже есть, не падай с ошибкой, а обнови ему дату последнего входа».
Когда начинаешь читать SQL таким образом, запросы перестают выглядеть как магические заклинания. Они становятся обычными инструкциями для базы данных.
Главное про INSERT
INSERTдобавляет новые строки в таблицу.Базовая форма выглядит так:
INSERT INTO table_name (column_1, column_2) VALUES (value_1, value_2);Список колонок лучше всегда указывать явно. Это делает запрос понятнее и защищает от проблем после изменения структуры таблицы.
Для нескольких строк можно использовать batch-вставку:
INSERT INTO users (email, display_name) VALUES ('alice@example.com', 'Alice'), ('bob@example.com', 'Bob');Для вставки результата другого запроса используют
INSERT INTO ... SELECT.В PostgreSQL
RETURNINGпозволяет сразу получить вставленные значения, например новыйid.ON CONFLICTпомогает сделать upsert: вставить строку, а при конфликте обновить её или спокойно пропустить.Главная идея простая:
INSERT— это дверь, через которую новые данные попадают в таблицу. Чем аккуратнее ты указываешь колонки, значения, ограничения и поведение при конфликтах, тем меньше сюрпризов будет в базе.