DML (Data Manipulation Language) — команды для изменения данных: INSERT, UPDATE, DELETE. В отличие от SELECT, они меняют состояние базы. Разберём каждую команду с практическими примерами.
INSERT — вставка данных
Базовый синтаксис
INSERT INTO products (name, category, price, stock)
VALUES ('Ноутбук', 'Электроника', 85000, 15);
Порядок столбцов в VALUES должен совпадать с порядком столбцов в скобках.
Вставка нескольких строк
INSERT INTO products (name, category, price)
VALUES
('Мышь', 'Электроника', 1500),
('Стол', 'Мебель', 12000),
('Наушники', 'Электроника', 8000);
Один INSERT с несколькими строками значительно быстрее, чем несколько отдельных INSERT.
INSERT с SELECT
-- Скопировать товары из архива в основную таблицу
INSERT INTO products (name, category, price)
SELECT name, category, price
FROM products_archive
WHERE archived_at > NOW() - INTERVAL '1 year';
RETURNING — получить вставленные данные
INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Алиса')
RETURNING id, created_at;
-- Вернёт: id и created_at новой строки без отдельного SELECT
Это очень удобно для получения автоинкрементного ID:
WITH new_user AS (
INSERT INTO users (email) VALUES ('bob@example.com')
RETURNING id
)
INSERT INTO profiles (user_id, bio)
SELECT id, 'Новый пользователь'
FROM new_user;
ON CONFLICT — upsert
-- Если email уже существует — обновить имя
INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Алиса Updated')
ON CONFLICT (email)
DO UPDATE SET
name = EXCLUDED.name,
updated_at = NOW();
-- Если конфликт — ничего не делать
INSERT INTO user_settings (user_id, setting, value)
VALUES (42, 'theme', 'dark')
ON CONFLICT (user_id, setting)
DO NOTHING;
EXCLUDED — ссылка на строку, которую пытались вставить.
UPDATE — обновление данных
Базовый синтаксис
UPDATE products
SET price = 90000, updated_at = NOW()
WHERE id = 1;
⚠️ UPDATE без WHERE обновит все строки! Всегда проверяйте условие.
Обновление нескольких столбцов
UPDATE users
SET
name = 'Иван Иванов',
email = 'ivan@example.com',
updated_at = NOW()
WHERE id = 42;
Обновление с вычислением
-- Поднять цену на 10% для всей электроники
UPDATE products
SET price = price * 1.10
WHERE category = 'Электроника';
-- Увеличить счётчик
UPDATE posts SET views = views + 1 WHERE id = 100;
UPDATE с подзапросом
-- Обновить статус пользователей, у которых истекла подписка
UPDATE users
SET status = 'inactive'
WHERE id IN (
SELECT user_id
FROM subscriptions
WHERE end_date < NOW() AND is_active = true
);
UPDATE с JOIN (через FROM)
PostgreSQL поддерживает UPDATE ... FROM:
-- Присвоить пользователям категорию на основе таблицы сегментов
UPDATE users u
SET segment = s.segment_name
FROM user_segments s
WHERE s.user_id = u.id;
-- Обновить цену товара на основе прайс-листа поставщика
UPDATE products p
SET price = pl.new_price, updated_at = NOW()
FROM supplier_price_list pl
WHERE pl.product_sku = p.sku
AND pl.effective_date = CURRENT_DATE;
RETURNING в UPDATE
UPDATE orders
SET status = 'shipped', shipped_at = NOW()
WHERE id = 555
RETURNING id, status, shipped_at;
DELETE — удаление данных
Базовый синтаксис
DELETE FROM products WHERE id = 5;
⚠️ DELETE без WHERE удалит все строки! Медленнее TRUNCATE, но безопаснее — работает в транзакции, срабатывают триггеры.
DELETE с подзапросом
-- Удалить товары, которые ни разу не продавались и старше года
DELETE FROM products
WHERE created_at < NOW() - INTERVAL '1 year'
AND id NOT IN (SELECT DISTINCT product_id FROM order_items);
-- Безопаснее через NOT EXISTS (избегаем NULL-проблемы)
DELETE FROM products
WHERE created_at < NOW() - INTERVAL '1 year'
AND NOT EXISTS (
SELECT 1 FROM order_items WHERE product_id = products.id
);
DELETE с JOIN (через USING)
-- Удалить заказы пользователей, помеченных как мошенники
DELETE FROM orders o
USING users u
WHERE o.user_id = u.id
AND u.is_fraudster = true;
RETURNING в DELETE
-- Удалить и вернуть удалённые строки
DELETE FROM sessions
WHERE expires_at < NOW()
RETURNING user_id, created_at, expires_at;
Мягкое удаление (soft delete)
Часто вместо DELETE используют пометку:
-- Вместо удаления — пометка
UPDATE users
SET deleted_at = NOW(), is_active = false
WHERE id = 42;
-- Фильтровать в запросах:
SELECT * FROM users WHERE deleted_at IS NULL;
Преимущества: данные не теряются, можно восстановить, есть аудит.
TRUNCATE vs DELETE
-- Удалить все строки (медленнее)
DELETE FROM logs;
-- Очистить таблицу мгновенно (нельзя откатить без транзакции)
TRUNCATE TABLE logs;
-- TRUNCATE с перезапуском счётчиков
TRUNCATE TABLE logs RESTART IDENTITY;
-- TRUNCATE нескольких таблиц
TRUNCATE TABLE sessions, temp_data, cache;
| DELETE | TRUNCATE | |
|---|---|---|
| WHERE | ✅ | ❌ |
| Триггеры | ✅ | ❌ (по умолчанию) |
| Откат | ✅ | ✅ (в транзакции) |
| Скорость | Медленнее | Мгновенно |
| Сбросить ID | ❌ | RESTART IDENTITY |
Безопасные паттерны
Сначала SELECT, потом DELETE/UPDATE
-- 1. Проверить, что попадёт под изменение
SELECT * FROM orders WHERE status = 'pending' AND created_at < '2026-01-01';
-- 2. Убедившись, что всё верно — выполнить
DELETE FROM orders WHERE status = 'pending' AND created_at < '2026-01-01';
Использовать транзакцию
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
-- Проверить результат
SELECT id, balance FROM accounts WHERE id IN (1, 2);
-- Если всё ок
COMMIT;
-- Если что-то не так
ROLLBACK;
Лимит на UPDATE/DELETE
-- Удалять батчами по 1000 строк (безопаснее для больших таблиц)
DELETE FROM logs
WHERE id IN (
SELECT id FROM logs
WHERE created_at < NOW() - INTERVAL '90 days'
LIMIT 1000
);
-- Повторять в цикле
Итог
| Команда | Назначение | Ключевые особенности |
|---|---|---|
INSERT | Добавить строки | RETURNING, ON CONFLICT |
UPDATE | Изменить строки | FROM для JOIN, RETURNING |
DELETE | Удалить строки | USING для JOIN, RETURNING |
TRUNCATE | Очистить таблицу | Быстро, без WHERE |
Всегда используйте WHERE в UPDATE и DELETE. Тестируйте через SELECT перед запуском. Заворачивайте опасные операции в транзакцию.