Совет дня №25
Иногда мы хотим удалить что-то из базы, но нельзя. Причины разные:
-
высокая связность сущностей. Если это пользователь, на него завязаны публикации, комментарии, фотографии, упоминания, заказы, обращение в поддержку и так далее. Каскадный 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 недостаточно, чтобы понять: почему данные
удалены. В идеале мы бы хотели знать время события, пользователя, кто это сделал
и минимальный комментарий. Выходит, к каждой таблице нужно добавить еще два-три
поля?
И наконец: таблица, где ничего не удаляется, становится помойкой. Нельзя понять, почему удалили данные и что с ними делать. Пользователь удалился сам? Его забанили? За что? Он на модерации? Кто кого ждет?
Данные о платежах не были обработаны? Почему? Когда планируется снова их обработать? Что делать, если опять не получится? Десятки вопросов, и никто ничего не знает.
Решение будет изложено в следующем совете.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter