Прямой ответ
Мягкое удаление (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.
Рекомендации:
- Не применяйте мягкое удаление ко всем таблицам. Для справочников и технических данных оно добавляет издержки без пользы.
- Проверяйте соединения таблиц. Присоединение к «удалённой» записи возвращает данные, которые пользователь не должен видеть.
- Считайте долю мёртвых строк. Метрика помогает решить, когда архивирование становится необходимым.
- Разделяйте бизнес-удаление и юридическое. Первое скрывает запись от пользователя, второе уничтожает данные физически.
- Планируйте архив заранее. Перенос миллионов строк в отдельную таблицу без подготовленной процедуры — рискованная операция.
Практический сценарий: невозможность зарегистрировать пользователя с адресом удалённого аккаунта
Контекст: после удаления 3 000 аккаунтов пользователи начали получать ошибку о занятом адресе почты при регистрации.
- Диагностика. Удаление выполнялось мягко, а уникальное ограничение
UNIQUE (email)оставалось полным. Строка с адресом физически присутствовала, и новое значение конфликтовало с ней. - Причина. Мягкое удаление вводилось для возможности восстановления, но ограничения и индексы не были адаптированы: они продолжали учитывать все строки.
- Действия. (а) Полное ограничение заменили частичным с условием
WHERE deleted_at IS NULL; (б) рабочие индексы пересоздали как частичные, что уменьшило их размер на 40 процентов при доле мёртвых строк около трети; (в) добавили задачу архивирования строк старше года с переносом в отдельную таблицу; (г) проверили другие таблицы и нашли две с той же проблемой; (д) ввели правило: при добавлении мягкого удаления пересматриваются уникальные ограничения. - Результат. Регистрация с адресами удалённых аккаунтов стала возможной, а доля мёртвых строк в рабочей таблице перестала расти благодаря архивированию.
Вывод: мягкое удаление — не бесплатная возможность, а изменение семантики таблицы. Ограничения, индексы и запросы должны учитывать новую семантику сразу, иначе ошибки проявляются в неожиданных местах.
FAQ
Когда мягкое удаление оправдано? Когда нужны восстановление после ошибки, история изменений или сохранение ссылок из других сущностей. Для технических и временных данных оно избыточно.
Как быть с требованием удалить персональные данные? Мягкое удаление не выполняет требование. Нужен процесс фактического удаления или обезличивания по запросу.
Что лучше: мягкое удаление или архивная таблица? Архивная таблица не замедляет рабочие запросы и сохраняет историю, но требует переноса. Мягкое удаление проще в начале и дороже при росте.
Нужно ли фильтровать в каждом запросе вручную? Инкапсуляция в слое доступа снижает риск, но полный контроль даёт только дисциплина и проверка соединений таблиц на ревью.
Источники: PostgreSQL docs (ограничения, частичные индексы), GDPR ст. 17 — ссылки в шапке.