INSERT, UPDATE, DELETE в SQL: шпаргалка

Зачем аналитику DML

На собесе аналитика спрашивают редко, но на позициях «DA + DE» — часто. Плюс нужно уметь не угробить прод.

INSERT

Базовый

INSERT INTO users (name, email) VALUES ('Иван', 'ivan@example.com');

-- Несколько строк
INSERT INTO users (name, email) VALUES
  ('Петр', 'petr@example.com'),
  ('Аня', 'anya@example.com');

INSERT SELECT

INSERT INTO archive_users
SELECT * FROM users WHERE last_active < CURRENT_DATE - INTERVAL '2 year';

INSERT ... ON CONFLICT (UPSERT)

PostgreSQL:

INSERT INTO users (id, email)
VALUES (1, 'new@example.com')
ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email;

EXCLUDED — ссылка на «пытавшуюся вставиться» строку.

MySQL:

INSERT INTO users (id, email) VALUES (1, 'new@example.com')
ON DUPLICATE KEY UPDATE email = 'new@example.com';

RETURNING — возврат данных

INSERT INTO orders (user_id, amount)
VALUES (1, 100)
RETURNING order_id, created_at;

Типичный кейс: получить автогенерированный id сразу.

UPDATE

Базовый

UPDATE users SET status = 'active' WHERE id = 1;

-- Обновить несколько колонок
UPDATE users SET status = 'active', updated_at = NOW() WHERE id = 1;

UPDATE с подзапросом

UPDATE users
SET orders_count = (
  SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id
);

UPDATE ... FROM (PostgreSQL)

UPDATE users u
SET city = c.name
FROM cities c
WHERE c.id = u.city_id;

Удобнее, чем коррелированный подзапрос.

RETURNING

UPDATE orders SET status = 'cancelled'
WHERE status = 'pending' AND created_at < CURRENT_DATE - INTERVAL '7 day'
RETURNING order_id, user_id;

Возвращает обновлённые строки — полезно для audit.

Если хочется сразу закрепить тему на практике — открой тренажёр в Telegram. 10 минут в день — и синтаксис в пальцах.

DELETE

Базовый

DELETE FROM users WHERE status = 'deleted';

DELETE с JOIN (PostgreSQL через USING)

DELETE FROM orders o
USING users u
WHERE o.user_id = u.id AND u.is_spam = TRUE;

DELETE всех строк

DELETE FROM temp_table;
-- vs
TRUNCATE temp_table;

TRUNCATE быстрее (не логирует каждую строку), но не откатывается в транзакции (в некоторых СУБД) и сбрасывает auto-increment.

RETURNING

DELETE FROM sessions
WHERE expired_at < NOW()
RETURNING session_id, user_id;

MERGE (SQL Server, Oracle, новый в PostgreSQL 15+)

Комбо INSERT + UPDATE + DELETE в одной операции:

MERGE INTO users u
USING new_data n ON u.id = n.id
WHEN MATCHED THEN UPDATE SET email = n.email
WHEN NOT MATCHED THEN INSERT (id, email) VALUES (n.id, n.email);

На собесе про MERGE спрашивают редко, но знать полезно.

Безопасность — как не убить прод

1. Всегда начинайте с SELECT

-- Сначала проверьте, что удалится
SELECT * FROM users WHERE last_active < CURRENT_DATE - INTERVAL '2 year';

-- Потом только DELETE
DELETE FROM users WHERE last_active < CURRENT_DATE - INTERVAL '2 year';

2. Используйте транзакции

BEGIN;
DELETE FROM orders WHERE status = 'draft';
-- Проверяем через SELECT
SELECT COUNT(*) FROM orders WHERE status = 'draft';
-- Если норм — COMMIT
COMMIT;
-- Если нет — ROLLBACK

3. Бэкапы перед массовыми операциями

Копия таблицы перед изменениями:

CREATE TABLE users_backup_2026_04_15 AS SELECT * FROM users;
-- Теперь можно работать

4. Ограничение через LIMIT (в UPDATE/DELETE)

-- PostgreSQL
DELETE FROM temp WHERE id IN (
  SELECT id FROM temp LIMIT 1000
);

-- MySQL
DELETE FROM temp LIMIT 1000;

При массовых операциях чанкуйте — не блокируйте всю таблицу на 10 минут.

5. WHERE — обязательно

Классическая авария:

-- 💀 Удалит ВСЕ
DELETE FROM users;

-- ✅ Правильно
DELETE FROM users WHERE id = 1;

Хорошая привычка: писать WHERE 1=2 сначала, потом править.

ACID и DML

INSERT/UPDATE/DELETE — транзакционные операции. В транзакции они либо все применяются, либо все откатываются. См. шпаргалку по транзакциям.

Чтобы не только читать теорию, но и решать реальные задачи — загляните в бот Карьерника. Там по каждой теме подборка вопросов с разборами.

10 задач с собесов

1. Вставить нового пользователя с возвратом id

INSERT INTO users (name, email) VALUES ('Иван', 'ivan@x.com')
RETURNING id;

2. UPSERT — либо создать, либо обновить

INSERT INTO settings (user_id, theme) VALUES (1, 'dark')
ON CONFLICT (user_id) DO UPDATE SET theme = EXCLUDED.theme;

3. Удалить старые сессии

DELETE FROM sessions WHERE expired_at < NOW();

4. Обновить цены всех товаров одной категории +10%

UPDATE products SET price = price * 1.1 WHERE category = 'electronics';

5. Пометить неактивных пользователей

UPDATE users SET status = 'inactive'
WHERE last_login < CURRENT_DATE - INTERVAL '90 day';

6. Массовая вставка из CSV (через COPY)

COPY users FROM '/tmp/users.csv' WITH CSV HEADER;

7. Бэкап перед миграцией

CREATE TABLE users_before_migration AS SELECT * FROM users;

8. Удаление дубликатов

DELETE FROM users a USING users b
WHERE a.id < b.id AND a.email = b.email;

9. DELETE с LIMIT (чанками)

DELETE FROM logs WHERE id IN (
  SELECT id FROM logs WHERE created_at < '2025-01-01' LIMIT 10000
);

Повторять в цикле до конца.

10. UPDATE по JOIN

UPDATE orders o
SET total = oi.sum_total
FROM (SELECT order_id, SUM(price * quantity) AS sum_total FROM order_items GROUP BY order_id) oi
WHERE o.id = oi.order_id;

Как тренироваться

DML — теоретически просто, практически рискованно. Тренируйтесь на dev-базе, потом только выносите на staging/prod.

Совет: в продакшне всегда используйте транзакции для DML. Даже DELETE WHERE id = 1 лучше выполнить в BEGIN/COMMIT — мало ли что.

Читайте также

FAQ

UPSERT — что это?

Insert OR Update: если строки с таким ключом нет — вставить, если есть — обновить. В PostgreSQL через ON CONFLICT. В MySQL — ON DUPLICATE KEY UPDATE.

Почему DELETE медленный?

Логирует каждую строку в WAL, обновляет индексы, проверяет триггеры. TRUNCATE для массового удаления быстрее.

Можно ли UPDATE с JOIN?

В PostgreSQL через FROM-клаузу. В MySQL — UPDATE t1 JOIN t2 SET ... . В стандартном SQL — через коррелированный подзапрос.

Как откатить DELETE?

Только через ROLLBACK (в рамках транзакции). После COMMIT — только из бэкапа. Поэтому в продакшне сначала BEGIN, потом DELETE, потом проверка, потом COMMIT.