INSERT INTO - Простая команда с непростым характером
INSERT — это оператор для добавления новых строк в таблицу.
Классика выглядит так:
INSERT INTO customers (name, age, city)
VALUES ('Оля', 28, 'Москва');
Главные слова:
INSERT INTO — куда добавляем,
VALUES — что именно добавляем.
А пока подписывайся на мой канал На связи: SQL Там я публикую посты про особенности и нюансы SQL. Этот канал про то, как не бояться баз данных, понимать, что такое JOIN, GROUP BY и почему NULL ≠ 0. Его я веду с нуля подписчиков. Присоединяйся!
При вставке значений в таблицу можно выделить одиночную вставку и массовую вставку.
Одиночная - это когда в запросе содержится вставка одной строки, а массовая - нужно вставить больше чем одну строку.
INSERT INTO products (name, price) VALUES ('Яблоки', 100);
INSERT INTO products (name, price) VALUES ('Груши', 120);
-- и так 1000 раз 😅
INSERT INTO products (name, price)
VALUES
('Яблоки', 100),
('Груши', 120),
('Бананы', 150);
Один INSERT на 1000 строк работает быстрее, чем 1000 отдельных запросов, потому что база открывает и закрывает транзакцию только один раз.
Транзакция — это логическая единица работы с базой данных, набор действий (обычно INSERT, UPDATE, DELETE и т. д.), которые выполняются как одно целое.
База гарантирует, что или все действия внутри транзакции будут выполнены, или не выполнится ни одно.
Например:
Перевод денег с карты на карту.
Снять деньги с карты А.
Зачислить деньги на карту Б.
Если выполнить только первый шаг, а второй не получится — деньги «пропадут».
Транзакция гарантирует, что либо оба шага выполнятся, либо оба отменятся.
Так вот, даже если мы явно в коде не прописываем начало и окончание транзакции, то наша база автоматически оборачивает наш запрос началом и окончанием транзакции
BEGIN;
INSERT INTO products (name, price) VALUES ('Яблоки', 100);
COMMIT;BEGIN;
INSERT INTO products (name, price) VALUES ('Груши', 120);
COMMIT;-- и так 1000 раз 😅
BEGIN;
INSERT INTO products (name, price)
VALUES
('Яблоки', 100),
('Груши', 120),
('Бананы', 150);
COMMIT;
Но при этом вставлять одним запросом млн строк - это плохо. Можно словить блокировку. Поэтому для большого объема вставки - лучше дробить на несколько маленьких частей.
Оптимальный размер батча — подбирается экспериментально. Обычно от 5k до 50k строк за один заход.
При вставке данных в таблицу у нас часто используется уникальный идентификатор строки, часто этот идентификатор является автоинкрементом, т.е. база сама записывает значение в это поле.
CREATE TABLE users (
id SERIAL, -- автоинкремент
name TEXT
);
INSERT INTO users (name) VALUES ('Оля'); -- id = 1
INSERT INTO users (name) VALUES ('Маша'); -- id = 2
Каждый INSERT добавляет новую запись, а автоинкремент гарантирует уникальный идентификатор для этой строки.
Без него нужно было бы самому считать, какой следующий номер ставить.
Автоинкремент экономит время и предотвращает ошибки.
Но автоинкремент не гарантирует, что значения в поле id будут "последовательными", он гарантирует, что значения в этом поле будут уникальными.
Получается, что при INSERT в поле с автоинкрементом могут быть "дырки".
Это получается, например, в следующих кейсах:
1. отмененная вставка:
- Сделали INSERT, база выделила id = 5.
- Транзакцию откатили (ROLLBACK).
- Id 5 пропал, следующие вставки идут с 6.
2. Удаление строк:
- Если удалить записи, то номера исчезнут, но новые не «подтянутся» к освободившимся.
3. Параллельная вставка:
- Две транзакции одновременно вставляют строки.
- Каждая получает свой id, даже если одна потом откатится → тоже появляются пропуски.
В PostgreSQL номера генерируются через объект SEQUENCE.
Если ты вручную добавил строку с id = 9999, а sequence «застрял» на 5000.
То, когда при следующих попытках осуществить вставку строки БД дойдет до значения 9999 - этот INSERT упадёт с ошибкой: «дубликат ключа». Потому что sequence не обновляется автоматически! Sequence не смотрит на максимальный id в таблице. Он просто отдаёт своё следующее число.
Решение может быть:
ALTER SEQUENCE … RESTART WITH <нужный номер>.
На вставку строк в таблицу может быть вызов триггера. Потому что вставка строки - это новая информация. А в зависимости от новой информации у нас могут быть разная логика подсчета того или иного показателя. Например, расчет бонусов для клиента зависит от суммы заказа. Поэтому при каждой вставке в таблицу заказов будет вызван триггер по расчету бонусов для клиента.
CREATE TRIGGER bonus
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION bonus_cnt();
Каждая новая строка вызовет bonus_cnt().
Даже если ты вставляешь 1000 строк, триггер вызовется 1000 раз.
Есть еще такое понятие как UPSERT.
UPSERT = INSERT + UPDATE, т.е. «вставить новую запись, а если такая запись уже есть — обновить существующую».
Пример: у нас есть таблица users с колонкой id. Мы хотим добавить пользователя с id = 1.
Если пользователя нет → вставляем.
Если пользователь есть → обновляем его данные.
Но такой INSERT + UPDATE в каждой СУБД реализуется по разному.
Одна и та же логика «вставить или обновить» в коде не переносится напрямую между базами. То, что называется UPSERT в PostgreSQL, будет работать иначе в MySQL и совсем иначе в SQL Server.
INSERT ... ON CONFLICT (id) DO UPDATE ... -- Вставляет запись, если id ещё нет; иначе обновляет эту запись. PostgreSQL
INSERT ... ON DUPLICATE KEY UPDATE ... -- MySQL
MERGE ... -- SQL Server
По сути, UPSERT — это концепция, а не единый универсальный оператор.
На скорость INSERT влияет наличие индексов в таблице. Чем больше индексов, тем долше будет выполняться вставка. Каждая новая строка должна обновить все индексы. При массовых загрузках индексы иногда отключают, а потом создают заново
А еще, иногда база говорит: «Вставка прошла успешно», но на диск ещё ничего не записано.
PostgreSQL и MySQL (InnoDB) используют write-ahead log: запись сначала идёт в журнал, потом — на диск.
Если прямо в этот момент выключить сервер → можно потерять часть данных.
Решается настройкой fsync, commit и уровней надёжности транзакций.
INSERT — это не просто «добавить данные». Это про индексы, блокировки, автоинкременты, батчи и даже про то, как СУБД пишет на диск.
