19. DELETE
🧭 Введение: почему удаление данных опаснее, чем кажется
DELETE выглядит простой командой, но именно она чаще всего приводит к необратимым ошибкам.Один неверный фильтр может удалить рабочие данные, которые потом трудно восстановить.
В этой лекции разберём ключевые production-паттерны:
DELETEс точнымWHERE;DELETE ... USINGи anti-join удаление;- каскадное удаление и поведение внешних ключей;
- soft delete как безопасную альтернативу физическому удалению;
- правила безопасного выполнения операций удаления.
💡 Совет:
Перед удалением всегда запускайте
SELECT с тем же фильтром, чтобы видеть целевой набор строк.✅ Вывод:
Надёжный
DELETE — это контроль границ удаления, а не «быстрый cleanup».⚠️ Проблема -> решение
Типичная проблема:
- удаляют без проверенного
WHERE; - не учитывают связи по
FK; - смешивают физическое удаление и бизнес-требование хранить историю.
Последствия:
- потеря данных и инциденты;
- ошибки целостности при связанных таблицах;
- конфликт с аудитом/аналитикой.
Решение:
- сначала превью через
SELECT; - явно понимать стратегию
ON DELETEу связей; - для большинства бизнес-сущностей использовать soft delete.
🟢 Если совсем просто:
Сначала определяем, можно ли удалять физически, и только потом пишем
DELETE.🎯 Как понять, что этап прошёл успешно:
Удалены только нужные строки, связи не сломаны, а история сохранена по правилам проекта.
🛠️ Чем помогает и как работает
DELETE нужен в реальной работе постоянно:- удаление тестовых и технических данных;
- cleanup устаревших сессий и временных сущностей;
- каскадная очистка зависимых данных;
- логическое скрытие записей через soft delete.
🟢 Если совсем просто:
DELETE удаляет строки из таблицы, а WHERE определяет какие именно.🎯 Как понять, что этап прошёл успешно:
Вы умеете выбирать правильный тип удаления под бизнес-контекст.
Чем помогает:
- поддерживает чистоту данных;
- уменьшает объём «мусора» в таблицах;
- упрощает соблюдение retention-политик.
Как это работает:
- Шаг 1: определяем стратегию (hard delete, cascade, soft delete);
- Шаг 2: проверяем целевой набор (
SELECT); - Шаг 3: выполняем удаление (часто в транзакции);
- Шаг 4: проверяем итог и число затронутых строк;
- Шаг 5: фиксируем результат и наблюдаем эффект в системе.
✅ Вывод:
Сильный
DELETE — это управляемый процесс с проверкой до и после.📚 Ключевые термины (простыми словами)
Перед практикой синхронизируем словарь.
🟢 Если совсем просто:
Нужно чётко различать физическое удаление и логическое скрытие.
🎯 Как понять, что этап прошёл успешно:
Вы уверенно различаете hard delete, cascade и soft delete.
- Hard delete — физическое удаление строки из таблицы.
- Soft delete — пометка записи как удалённой (
deleted_at,is_deleted) без физического удаления. - Cascade delete — автоматическое удаление зависимых строк по
FK-связи. - Restrict/No Action — запрет удаления родителя при наличии детей.
- Orphan rows — «сиротские» записи без валидной родительской связи.
- Retention policy — правило, сколько хранить данные.
- Purge — физическая очистка soft-deleted данных спустя срок хранения.
✅ Вывод:
Эти термины закрывают базу для безопасного проектирования удаления.
🗑️ 1. DELETE с WHERE: базовый и обязательный паттерн
DELETE без фильтра почти всегда ошибка. Базовая дисциплина — удалять только целевой сегмент.🟢 Если совсем просто:
WHERE в DELETE — это страховка от массовой потери данных.🎯 Как понять, что этап прошёл успешно:
Удалены именно те строки, которые планировали удалить.
Назначение:
Точечно удалять записи по бизнес-условию.
Простыми словами:
Сначала определяем кого удаляем, потом выполняем
DELETE.Для новичка:
Никогда не запускайте
DELETE, пока не проверили тот же фильтр через SELECT.Аналогия:
Это как удалять один документ по номеру, а не чистить весь архив.
Пример:
DELETE FROM sessionsWHERE id = 'sess_42';Production-бонус:
RETURNING (PostgreSQL):DELETE FROM sessionsWHERE expires_at < NOW() - INTERVAL '30 days'RETURNING id, user_id, expires_at;✅ Вывод:
DELETE ... RETURNING повышает наблюдаемость и упрощает аудит удаления.🔎 Как это происходит на практике:
- Контекст: cron очищает просроченные сессии.
- Действия: удаляют только устаревшие строки по условию.
- Результат: таблица не разрастается, активные сессии не затронуты.
Характеристики:
- просто и прозрачно;
- безопасно при точном фильтре;
- риск высок, если фильтр недостаточно узкий.
Когда использовать:
Когда физическое удаление допустимо и данные не нужны для истории.
✅ Вывод:
Любой корректный
DELETE начинается с проверенного WHERE.🔍 2. DELETE ... USING и anti-join delete (PostgreSQL)
Когда условие удаления естественно выражается join-логикой, в PostgreSQL удобно использовать
DELETE ... USING.🟢 Если совсем просто:
USING удаляет строки целевой таблицы на основе связанной таблицы.🎯 Как понять, что этап прошёл успешно:
Удалились только строки, которые соответствуют join-условию.
Назначение:
Писать читаемое удаление по связям без сложных вложенных фильтров.
Простыми словами:
USING — это join для DELETE.Для новичка:
Для anti-join удалений по умолчанию используйте
NOT EXISTS: это NULL-safe паттерн.Пример
DELETE ... USING:DELETE FROM order_items oiUSING orders oWHERE oi.order_id = o.id AND o.status = 'cancelled';Пример anti-join через
NOT EXISTS (orphan cleanup):DELETE FROM order_items oiWHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.id = oi.order_id);Ловушка
NOT IN даже в DELETE:-- Опасно: NULL в подзапросе может сломать логикуDELETE FROM usersWHERE id NOT IN ( SELECT user_id FROM orders);-- Безопасно: anti-join через NOT EXISTSDELETE FROM users uWHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id);✅ Вывод:
USING удобен для join-условий, а для anti-join в DELETE лучше выбирать NOT EXISTS.🔗 3. Каскадное удаление: как работают связи
Удаление часто затрагивает не одну таблицу, а цепочку связанных сущностей.
🟢 Если совсем просто:
При удалении родителя нужно явно понимать, что делать с дочерними строками.
🎯 Как понять, что этап прошёл успешно:
После удаления нет сломанной ссылочной целостности и неожиданных потерь.
Назначение:
Управлять поведением связанных данных при удалении.
Простыми словами:
ON DELETE в FK задаёт судьбу дочерних строк.Для новичка:
Перед
DELETE проверьте DDL: CASCADE, RESTRICT, SET NULL — это разные сценарии.Аналогия:
Если удаляете папку, нужно понимать, что будет с файлами внутри: удалятся, заблокируют удаление или останутся отдельно.
Пример
ON DELETE CASCADE (PostgreSQL):CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE);Пример
ON DELETE RESTRICT:CREATE TABLE payments ( id BIGSERIAL PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE RESTRICT);Практическое правило:
CASCADEудобно для технических/временных данных;- для финансовых и исторических данных чаще используют
RESTRICTили soft delete.
✅ Вывод:
Каскад — полезный инструмент, но его включают только осознанно, по доменной логике.
🔎 Как это происходит на практике:
- Контекст: удаляют пользователя тестовой среды.
- Действия:
CASCADEочищает зависимые тестовые записи автоматически. - Результат: база остаётся консистентной без ручной чистки.
Характеристики:
- упрощает cleanup связанных таблиц;
- снижает риск orphan rows;
- при ошибке фильтра может удалить слишком много данных.
Когда использовать:
Когда доменная модель допускает физическое удаление зависимых данных.
✅ Вывод:
Перед удалением родителя всегда проверяйте
ON DELETE стратегию у всех зависимостей.♻️ 4. Soft delete: логическое удаление вместо физического
Во многих бизнес-сценариях данные нельзя удалять физически сразу: нужна история, аудит, восстановление.
🟢 Если совсем просто:
Soft delete — это
UPDATE, который помечает запись удалённой.🎯 Как понять, что этап прошёл успешно:
Данные скрыты из активной выборки, но доступны для аудита/восстановления.
Назначение:
Сохранить историю и при этом исключить запись из активной работы.
Простыми словами:
Вместо
DELETE ставим deleted_at (или is_deleted = true).Для новичка:
После внедрения soft delete все рабочие
SELECT должны учитывать фильтр deleted_at IS NULL.Системный контракт soft delete:
Чтобы «удалённое» не всплывало случайно, нужен единый вход к активным данным:
VIEWдля активных записей;- default scope на уровне приложения;
- CTE/политика доступа (например, RLS) для критичных контуров.
Практичный паттерн (view для активных):
CREATE VIEW users_active ASSELECT *FROM usersWHERE deleted_at IS NULL;Аналогия:
Это как архивировать документ: он не участвует в текущей работе, но не исчезает.
Пример soft delete:
UPDATE usersSET deleted_at = NOW(), updated_at = NOW()WHERE id = 42 AND deleted_at IS NULL;Пример активной выборки:
SELECT u.id, u.emailFROM users uWHERE u.deleted_at IS NULL;Purge-этап (физическая очистка позже):
DELETE FROM usersWHERE deleted_at < NOW() - INTERVAL '90 days';Soft delete vs purge (retention/GDPR):
- soft delete: скрыли из продукта, но оставили для аудита/восстановления;
- purge: физически удалили по срокам хранения или юридическим требованиям.
Purge чаще делают батчами, чтобы не держать долгие блокировки.
✅ Вывод:
Soft delete снижает риск потери данных и лучше подходит для бизнес-критичных сущностей.
🔎 Как это происходит на практике:
- Контекст: пользователь «удаляет» аккаунт в интерфейсе.
- Действия: запись помечают через
deleted_at, а не удаляют сразу. - Результат: фронт не видит запись, но аудит и support могут её восстановить.
Характеристики:
- безопаснее hard delete;
- требует дисциплины в фильтрах
SELECT; - со временем требует purge-процесса.
Когда использовать:
Когда нужна история изменений или политика восстановления.
✅ Вывод:
Для большинства OLTP-доменов soft delete — дефолт, hard delete — исключение.
🔐 5. Безопасность операций удаления
Удаление должно быть операцией с контролем, а не «просто запросом».
🟢 Если совсем просто:
Безопасный
DELETE = превью набора + транзакция + post-check.🎯 Как понять, что этап прошёл успешно:
Результат удаления воспроизводим, проверяем и не ломает прод.
Назначение:
Снизить риск необратимой потери данных.
Простыми словами:
Сначала считаем и проверяем, потом удаляем.
Для новичка:
Если ожидаете маленький объём, удаляйте батчами через CTE, а не «одним махом».
Если ожидаете «примерно 100 строк», всё равно ставьте ограничитель через batch/CTE: так легче рано поймать ошибочно широкий фильтр.
Безопасный шаблон:
BEGIN; SELECT COUNT(*) AS target_countFROM sessionsWHERE expires_at < NOW() - INTERVAL '30 days'; WITH batch AS ( SELECT id FROM sessions WHERE expires_at < NOW() - INTERVAL '30 days' ORDER BY id LIMIT 5000)DELETE FROM sessions sUSING batch bWHERE s.id = b.id; COMMIT;Цикл для больших объёмов:
Повторяем batched delete, пока очередной запуск не удалит
0 строк.Нагрузка и обслуживание (PostgreSQL, must-know):
Большие
Практика: удаляем батчами, в окно низкой нагрузки и следим за
DELETE создают dead tuples и нагрузку на I/O, могут раздувать индексы.Практика: удаляем батчами, в окно низкой нагрузки и следим за
autovacuum/vacuum и блоатом.Production-паттерн: архивирование вместо удаления
BEGIN; INSERT INTO sessions_archiveSELECT *FROM sessionsWHERE expires_at < NOW() - INTERVAL '30 days'; DELETE FROM sessionsWHERE expires_at < NOW() - INTERVAL '30 days'; COMMIT;✅ Вывод:
Безопасность удаления — это процесс, а не один SQL-оператор.
🆚 Сравнение стратегий удаления
| Подход | Что делает | Когда подходит | Риск |
|---|---|---|---|
Hard delete (DELETE) | Физически удаляет строку | Технические, временные данные | Потеря данных при ошибке фильтра |
| Cascade delete | Удаляет зависимые строки автоматически | Явно согласованные связи | Массовая потеря при неверном parent delete |
Soft delete (deleted_at) | Логически скрывает запись | Бизнес-сущности, аудит | Нужна дисциплина фильтров и purge |
✅ Вывод:
Стратегия удаления выбирается по доменной ценности данных и требованиям к истории.
🧠 Must-Know (запомнить)
- Никогда не запускайте
DELETEбез проверенногоWHERE. - Перед
DELETEделайтеSELECT/COUNTс тем же условием. - В
DELETEтоже избегайтеNOT INиз-за NULL-trap, для anti-join используйтеNOT EXISTS. - Для join-удалений в PostgreSQL используйте
DELETE ... USING. - Проверяйте
FKиON DELETEповедение до удаления родителя. ON DELETE CASCADEприменяйте только осознанно.- Для бизнес-важных сущностей чаще выбирайте soft delete.
- Soft delete — системный контракт: закрепляйте активный доступ через view/scope/policy.
- Планируйте purge-процесс для старых soft-deleted данных.
- Для больших объёмов удаляйте батчами и повторяйте до
0 rows. - Большие удаления: батчи + окно низкой нагрузки + контроль vacuum/bloat.
- Критичные удаления выполняйте в транзакции.
- Если нужна трассируемость, используйте
DELETE ... RETURNING(PostgreSQL). - Когда hard delete нельзя, используйте паттерн archive -> delete.
✅ Вывод:
Надёжное удаление строится на стратегии, проверке и управляемом исполнении.
❌ Частые мифы
❌ Миф:
DELETE сработал быстро, значит всё безопасно.
✅ Как правильно: скорость не гарантирует корректность; важен точный фильтр и проверка набора.
📎 Почему это важно: быстрый неверный DELETE может уничтожить прод-данные.❌ Миф:
CASCADE всегда удобно и правильно.
✅ Как правильно: CASCADE включают только там, где домен допускает физическое удаление зависимостей.
📎 Почему это важно: иначе можно удалить критичные связанные данные.❌ Миф: Soft delete — это «лишняя сложность».
✅ Как правильно: soft delete часто необходим для аудита, отката и историчности.
📎 Почему это важно: без истории труднее расследовать инциденты и восстанавливать данные.
❌ Миф: Если удаление в транзакции, можно не делать превью.
✅ Как правильно: транзакция не заменяет проверку фильтра перед удалением.
📎 Почему это важно: rollback может быть дорогим и не всегда доступным в production-сценариях.
🎤 Часто спрашивают на собеседованиях
❓ Вопрос: Почему
DELETE без WHERE — критичная ошибка?
✅ Ответ: потому что запрос удалит все строки таблицы.❓ Вопрос: В чём разница hard delete и soft delete?
✅ Ответ: hard delete физически удаляет строку, soft delete только помечает её удалённой.
❓ Вопрос: Когда уместен
ON DELETE CASCADE?
✅ Ответ: когда доменная логика разрешает автоматическое удаление зависимых данных.❓ Вопрос: Что делать перед массовым удалением?
✅ Ответ: проверить объём через
SELECT/COUNT, оценить связи и запускать удаление батчами.❓ Вопрос: Зачем
DELETE ... RETURNING в PostgreSQL?
✅ Ответ: чтобы сразу получить список удалённых строк для аудита и контроля.❓ Вопрос: Почему
NOT IN опасен в DELETE с подзапросом?
✅ Ответ: NULL в подзапросе может сломать логику; для anti-join безопаснее NOT EXISTS.❓ Вопрос: Какие изменения нужны в запросах после внедрения soft delete?
✅ Ответ: все активные выборки должны фильтровать
deleted_at IS NULL.❓ Вопрос: Как безопасно удалять большие объёмы?
✅ Ответ: батчами через CTE/лимит и повторять цикл до
0 rows.🚨 Типичные ошибки
- Запускать
DELETE, не проверив фильтр черезSELECT. - Удалять родителя и не понимать последствия
ON DELETE. - Использовать
CASCADEв критичных доменах без анализа рисков. - Писать anti-join удаление через
NOT INи ловить NULL-trap. - Внедрить soft delete, но забыть фильтр в активных
SELECT. - Не закрепить soft delete как системный контракт (view/scope/policy).
- Не планировать purge и накапливать «мертвые» данные бесконечно.
- Удалять огромный объём одним statement в пиковое окно нагрузки.
- Не проверять число реально удалённых строк после выполнения.
✅ Вывод:
Большинство инцидентов с удалением предсказуемы и предотвращаемы.
✅ Best Practices
- Всегда начинайте с
SELECT-превью целевого набора. - Для hard delete используйте максимально точный
WHERE. - Для join-удалений используйте
DELETE ... USING, для anti-join —NOT EXISTS. - Критичные удаления оборачивайте в транзакцию.
- Для больших объёмов применяйте batched delete.
- Большие удаления планируйте с учётом
vacuum/блоата и I/O-нагрузки. - Перед удалением проверяйте политику
FK(CASCADE/RESTRICT/SET NULL). - Для бизнес-сущностей по умолчанию рассматривайте soft delete и единый active-scope.
- Добавляйте retention + purge процесс для soft-deleted данных.
- Если удалять нельзя напрямую, применяйте паттерн archive -> delete.
- Логируйте и мониторьте число удалённых строк.
- Используйте
RETURNING, когда нужен аудит результата в одном запросе.
✅ Вывод:
Production-ready удаление — это баланс между чисткой данных, безопасностью и требованиями аудита.
🏁 Заключение
DELETE — мощная команда, которая требует инженерной дисциплины.Если вы правильно выбираете стратегию (hard/cascade/soft) и контролируете выполнение, удаление становится безопасным инструментом.
✅ Вывод:
Надёжный
DELETE = точный WHERE + осознанная стратегия связей + безопасный процесс запуска.