SQL

БЛОК 6. CRUD — 19. DELETE

📚 23 вопросовПройти тест →
Лекция

БЛОК 6. CRUD — 19. DELETE

SQL

19. DELETE

🧭 Введение: почему удаление данных опаснее, чем кажется

DELETE выглядит простой командой, но именно она чаще всего приводит к необратимым ошибкам.
Один неверный фильтр может удалить рабочие данные, которые потом трудно восстановить.
В этой лекции разберём ключевые production-паттерны:
  • DELETE с точным WHERE;
  • DELETE ... USING и anti-join удаление;
  • каскадное удаление и поведение внешних ключей;
  • soft delete как безопасную альтернативу физическому удалению;
  • правила безопасного выполнения операций удаления.
💡 Совет: Перед удалением всегда запускайте SELECT с тем же фильтром, чтобы видеть целевой набор строк.
Вывод: Надёжный DELETE — это контроль границ удаления, а не «быстрый cleanup».

⚠️ Проблема -> решение

Типичная проблема:
  1. удаляют без проверенного WHERE;
  2. не учитывают связи по FK;
  3. смешивают физическое удаление и бизнес-требование хранить историю.
Последствия:
  1. потеря данных и инциденты;
  2. ошибки целостности при связанных таблицах;
  3. конфликт с аудитом/аналитикой.
Решение:
  1. сначала превью через SELECT;
  2. явно понимать стратегию ON DELETE у связей;
  3. для большинства бизнес-сущностей использовать 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: Чтобы «удалённое» не всплывало случайно, нужен единый вход к активным данным:
  1. VIEW для активных записей;
  2. default scope на уровне приложения;
  3. 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 + осознанная стратегия связей + безопасный процесс запуска.
🎯

Проверьте знания

Закрепите материал — пройдите тест по теме «БЛОК 6. CRUD — 19. DELETE»

Пройти тест →