Иногда мы хотим удалить что-то из базы, но нельзя. Причины разные:

  • высокая связность сущностей. Если это пользователь, на него завязаны публикации, комментарии, фотографии, упоминания, заказы, обращение в поддержку и так далее. Каскадный DELETE по всей базе – так себе решение.

  • В финансах и телекомах есть требования регулятора. Данные не должны удаляться бесследно.

  • Пользователь может передумать. Скажем, удалил письмо по ошибке, нажал Undo и оно вернулось в ящик.

  • При удалении данных не должна страдать статистика. Например, даже если пользователь удалился из маркетплейса, в отчете продаж должны быть его покупки.

Словом, есть много случаев, когда DELETE вам не поможет.

Часто поступают так: раз удалять нельзя, добавим флажок is_deleted и поставим ему истину. То есть вместо

delete from users where id = 42

выполним

update users set is_deleted = true
where id = 42

Когда-то давно я тоже выступал за такой подход, у меня даже есть заметка в блоге. Что ж, я заблуждался. Мне довелось поработать в проекте, где в каждой таблице был флаг is_deleted, и это был ад.

Если коротко – база, из которой ничего не удаляют, становится квартирой, из которой ничего не выбрасывают. Поживите неделю, складывая мусор в шкафы, и вы поймете.

Во-первых, обращаясь к любой таблице, нужно не забыть условие where not is_deleted. Редко, но разработчики его теряли, и удаленные строки шли на фронт. Вы, конечно, скажете: в моей ORM можно задать фильтры по умолчанию. Знаю, я так делал в Django: при обращении к модели условие добавится само. Но не все работают с ORM.

Можно создать вьюху users_active, которая выбирает всех не удаленных пользователей. Но это новая сущность, плюс понадобится много таких вьюх.

Во-вторых, удаленные записи болтаются в таблице, участвуют в сканировании, обходе по индексу и так далее. Они банально замедляют операции с записями: пусть ненамного, но все же. Возможно, вам понадобится строить частичные индексы с выражением where not is_deleted, чтобы отсечь удаленные записи.

В-третьих, если встречаемся уникальность, удаленная запись не позволит вставить новую. Скажем, если пользователь с почтой test@foobar.com отмечен удаленным и почта уникальна, вы не вставите другого пользователя с такой почтой. Кроме почты, уникальными могут быть другие поля, например номер транзакции или чека.

Технически это можно исправить: создать уникальный индекс с условием:

create unique index idx_user_email on users
using btree (email) where not is_deleted

В этом случае уникальность проверяется только для не удаленных пользователей. Вроде бы всё хорошо? Тогда вопрос: что вы будете делать с удаленным пользователем? При попытке снять флаг вас ждет конфликт, потому что почта уже занята.

В-четвертых, флага is_deleted недостаточно, чтобы понять: почему данные удалены. В идеале мы бы хотели знать время события, пользователя, кто это сделал и минимальный комментарий. Выходит, к каждой таблице нужно добавить еще два-три поля?

И наконец: таблица, где ничего не удаляется, становится помойкой. Нельзя понять, почему удалили данные и что с ними делать. Пользователь удалился сам? Его забанили? За что? Он на модерации? Кто кого ждет?

Данные о платежах не были обработаны? Почему? Когда планируется снова их обработать? Что делать, если опять не получится? Десятки вопросов, и никто ничего не знает.

Решение будет изложено в следующем совете.