Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Soft delete: мягкое удаление и его цена

Soft delete: мягкое удаление записей и его реальная цена

Soft delete через deleted_at: преимущества и скрытая цена, уникальные индексы, рост таблиц, конфликт с требованием удаления и альтернативы в виде архива.

Прямой ответ

Мягкое удаление (soft delete) помечает строку как удалённую вместо физического удаления: запись остаётся в таблице, а запросы её отфильтровывают. Это решает задачи аудита и восстановления, но добавляет постоянные издержки во все запросы и создаёт конфликт с требованием полного удаления персональных данных.

Типичная реализация — колонка отметки времени:

ALTER TABLE documents ADD COLUMN deleted_at timestamptz;

CREATE INDEX ON documents (project_id, id)
    WHERE deleted_at IS NULL;

Частичный индекс покрывает только живые строки, поэтому он меньше и быстрее полного; о частичных индексах — в статье про индексы PostgreSQL.

Ключевое преимущество — восстановление после ошибки. Физическое удаление по ошибочному условию требует восстановления из резервной копии; мягкое удаление отменяется одной командой:

UPDATE documents SET deleted_at = NULL
    WHERE project_id = 42 AND deleted_at > now() - interval '1 hour';

Цена проявляется в четырёх местах.

1. Каждый запрос должен фильтровать. Условие WHERE deleted_at IS NULL требуется везде, а его пропуск возвращает удалённые записи.

2. Уникальные ограничения ломаются. Ограничение UNIQUE (email) не позволяет создать нового пользователя с адресом удалённого:

-- работает: уникальность только среди живых строк
CREATE UNIQUE INDEX uq_users_email ON users (email)
    WHERE deleted_at IS NULL;

3. Внешние ключи указывают на «удалённые» строки. Ссылка остаётся валидной, хотя сущность удалена, поэтому проверка живости переносится в код.

4. Таблица растёт. Доля мёртвых строк увеличивается, а планировщик и индексы учитывают весь объём; о разрастании — в статье про VACUUM.

Конфликт с регулированием: право на удаление требует фактического удаления персональных данных, а мягкое удаление их сохраняет. Решение — разделение срока хранения и фактическое удаление по истечении срока.

Альтернативы мягкому удалению

Мягкое удаление — не единственный способ сохранить историю. Три альтернативы решают те же задачи с другими издержками.

Архивная таблица. При удалении строка переносится в таблицу с тем же набором колонок плюс отметка времени переноса. Рабочая таблица остаётся компактной, запросы не требуют фильтрации по признаку удаления, а уникальные ограничения работают без частичных индексов. Цена — необходимость транзакции на перенос и отдельный код для чтения архива.

Temporal-таблицы. История изменений ведётся автоматически, а запрос к состоянию на определённый момент выполняется стандартными средствами. Это покрывает задачу аудита полнее, чем отметка удаления, но не заменяет восстановление после ошибочного удаления массовых данных; о механизме — в статье про temporal-таблицы.

Журнал действий. Если цель — знать, кто и когда удалил запись, достаточно записи в журнале без сохранения самой строки. Журнал занимает меньше места и не влияет на рабочие запросы; о реализации — в статье про audit log.

Выбор зависит от задачи:

Задача Подходящее решение
Отмена ошибочного удаления Мягкое удаление или архив
Аудит изменений Temporal-таблицы
Знать факт удаления Журнал действий
Освободить место в рабочей таблице Архивная таблица

Распространённая ошибка — применять мягкое удаление «для истории», хотя историю даёт журнал или temporal-таблицы, а мягкое удаление при этом добавляет издержки во все запросы.

Второе заблуждение — считать мягкое удаление способом сэкономить место. Оно работает наоборот: строки остаются в таблице, индексы продолжают их учитывать, а планировщик обрабатывает больший объём данных. Место освобождается только архивированием либо физическим удалением по истечении срока хранения.

Практика: применение мягкого удаления с учётом цены

1. Используйте мягкое удаление там, где нужна отмена. Документы, заказы, проекты — да; сессионные и технические данные — нет.

2. Делайте уникальные ограничения частичными. Без WHERE deleted_at IS NULL восстановление удалённой записи или создание новой с тем же значением невозможно.

3. Создавайте частичные индексы для рабочих запросов. Индекс только по живым строкам меньше и не содержит мусора; о выборе — в статье про индексы.

4. Инкапсулируйте фильтрацию. Метод репозитория возвращает живые записи по умолчанию, а доступ к удалённым требует явного намерения.

5. Установите срок хранения удалённых строк. Через заданный период строки переносятся в архив либо удаляются физически; о хранении истории — в статье про temporal-таблицы.

6. Обрабатывайте запрос на удаление персонально. Для персональных данных мягкое удаление не заменяет фактическое удаление; о требованиях — в статье про 152-ФЗ и GDPR.

7. Фиксируйте факт удаления в журнале. Аудит действия важнее сохранения самой строки; о журнале — в статье про audit log.

Рекомендации:

  1. Не применяйте мягкое удаление ко всем таблицам. Для справочников и технических данных оно добавляет издержки без пользы.
  2. Проверяйте соединения таблиц. Присоединение к «удалённой» записи возвращает данные, которые пользователь не должен видеть.
  3. Считайте долю мёртвых строк. Метрика помогает решить, когда архивирование становится необходимым.
  4. Разделяйте бизнес-удаление и юридическое. Первое скрывает запись от пользователя, второе уничтожает данные физически.
  5. Планируйте архив заранее. Перенос миллионов строк в отдельную таблицу без подготовленной процедуры — рискованная операция.

Практический сценарий: невозможность зарегистрировать пользователя с адресом удалённого аккаунта

Контекст: после удаления 3 000 аккаунтов пользователи начали получать ошибку о занятом адресе почты при регистрации.

  1. Диагностика. Удаление выполнялось мягко, а уникальное ограничение UNIQUE (email) оставалось полным. Строка с адресом физически присутствовала, и новое значение конфликтовало с ней.
  2. Причина. Мягкое удаление вводилось для возможности восстановления, но ограничения и индексы не были адаптированы: они продолжали учитывать все строки.
  3. Действия. (а) Полное ограничение заменили частичным с условием WHERE deleted_at IS NULL; (б) рабочие индексы пересоздали как частичные, что уменьшило их размер на 40 процентов при доле мёртвых строк около трети; (в) добавили задачу архивирования строк старше года с переносом в отдельную таблицу; (г) проверили другие таблицы и нашли две с той же проблемой; (д) ввели правило: при добавлении мягкого удаления пересматриваются уникальные ограничения.
  4. Результат. Регистрация с адресами удалённых аккаунтов стала возможной, а доля мёртвых строк в рабочей таблице перестала расти благодаря архивированию.

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

FAQ

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

Как быть с требованием удалить персональные данные? Мягкое удаление не выполняет требование. Нужен процесс фактического удаления или обезличивания по запросу.

Что лучше: мягкое удаление или архивная таблица? Архивная таблица не замедляет рабочие запросы и сохраняет историю, но требует переноса. Мягкое удаление проще в начале и дороже при росте.

Нужно ли фильтровать в каждом запросе вручную? Инкапсуляция в слое доступа снижает риск, но полный контроль даёт только дисциплина и проверка соединений таблиц на ревью.

Источники: PostgreSQL docs (ограничения, частичные индексы), GDPR ст. 17 — ссылки в шапке.