Pull to refresh

Comments 50

А если забыть о дополнительном предикате для deleted_at, то это может иметь серьёзные последствия

Ну, возможно вы не знали, но это не только к полю deleted_at относится. Вообще любая ошибка в запросе может приводить к неожиданным результатам.

Утеря внешних ключей

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

Например, в вашем примере хорошей практикой будет всегда запрашивать накладные только по неудалённым клиентам.

Усложняется отсечение данных

Тут вообще какая-то надуманная проблема

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

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

почти всегда удаление данных также имеет не относящиеся к данным побочные эффекты. Например, были сделаны вызовы к внешним системам, чтобы архивировать записи там, были удалены объекты из блоб-хранилищ или отключены серверы.

Здесь «почти всегда» означает «почти всегда в практике автора». Видимо, практика, задачи и нужды всех остальных разработчиков для автора не стоят рассмотрения.

Вообще, статья была бы даже хороша, если бы автор сформулировал бы главный тезис как «бывают кейсы, когда мягкое удаление не нужно», и рассмотрел бы, что конкретно это за кейсы, и как понять, что именно в данном случае мягкое удаление нужно/не нужно.
Но в такой безаппеляционной постановке вопроса не могу никому рекомендовать статью.

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

Ну, в этом случае вы делаете дополнительную работу.МНОГО дополнительной работы. Функциональность внешних ключей в базе сделана разработчиками, которые на этом собаку съели и протестирована на тысячах приложений во всех возможных (и чато неожиданных ) комбинациях. Вы уверены, что у вас есть ресурсы чтобы достичь сравнимой надежности и производительности ?

Ну и в дополнение к тому, что я написал выше:
«Выключение» сущности, которая больше не нужна, и физическое удаление записи о ней — это две разных по сути операции. И выбирать среди них в первую очередь надо не потому, как удобнее писать запросы, а по тому, какая из них действительно нужна.

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

Ещё раз — мягкое удаление нужно не для того, чтобы подстелить соломки на случай инцидента, а для того, чтобы можно было продолжать работать с удалёнными записями, если это нужно.

Ещё хотел бы добавить, что обычно инструкция UPDATE быстрее чем DELETE. Особенно если есть внешние ограничения.

Хорошая практика, которая тоже отмечена в статье - это именно физическое удаление через какое время, чтобы в таблице не было много "мертвых" строк

Попросили как то настроить бекапы - несколько интернет магазинов завязанных на одну базу + внутренняя CRM. Крутилось всё на базе Laravel - раз в сутки приходил bareos, и делал выборку записей delete_at not in null, update_at/created_at > datetime предыдущего бекапа. После создания sql дампа удалял данные помеченые delete_at.

Такой себе инкрементальный бекап на минималках.

ЗЫ ИМХО - поле полезное delete_at, ибо научен горьким опытом безвозвратной потери данных от прямого запроса DELETE.

Если не справляются с большой структурой - добро пожаловать в кровавый энтерпрайз, тут легко не бывает.

В Laravel кстати появился трейт Prunable - можно гибко настроить автоматическое или ручное удаление записей, которые были ранее "мягко" удалены, как раз для очистки БД от ненужных более записей.

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

Надо для начала разобраться, зачем вообще удалять записи. Какой вы закладываете в это смысл с точки зрения предметной области. Сама по себе потеря актуальности не является безусловной причиной для удаления.

Присоединяюсь к недоумению, зачем вы собираетесь удалять выпущенные накладные. Это вообще законно? ;-)

Бухгалтеры для решения аналогичных проблем много-много лет назад придумали сторнирование.

Например, у вас таблица с временными данными. И эта таблица весит уже с десяток терабайт. 90% записей - мусор. Имхо, хороший повод почистить табличку. При этом нужно учесть, что ошибка в скрипте очистки - это падение прода и ректальная кара от бизнеса. Соответственно, перед удалением скопировать, посмотреть, что все ок, выждать театральную паузу и грохнуть табличку бэкап - вполне себе решение.

Да, таблицы надо периодически чистить по разным критериям. Но обсуждаемая в статье чистка по удалению родительской записи это совершенно ортогональный вопрос. Скажем, старые накладные возможно надо удалять при закрытии бухгалтерами квартала, а не при закрытии покупателем на которого они ссылаются аккаунта.

таблица с временными данными.  И эта таблица весит уже с десяток терабайт. 90% записей - мусор.

Так все таки временные данные или мусор? Или они стали мусором после использования? Значит процесс их использующий должен за собой почистить мусор, удалить все временные данные, если уже не нужны, а не копить терабайты, пока алерты не полетят что место кончается. А значит много заранее должен быть реализован и 100500 раз протестирован скрипт очистки.

Соответственно, перед удалением скопировать, посмотреть, что все ок, выждать театральную паузу и грохнуть табличку

Для этого используют тестовую БД, а не театральную паузу на проде. И следуют поговорке "Семь раз на тесте один раз на проде" )))

Так все таки временные данные или мусор?

Временные данные, которые потеряли со временем актуальность и стали мусором.

А значит много заранее должен быть реализован и 100500 раз протестирован скрипт очистки.

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

Значит процесс их использующий должен за собой почистить мусор, удалить все временные данные, если уже не нужны,

Было сильно не очевидно, что данные уже не актуальны. Но, по факту, выработали критерии и сделали такой скрипт. Да, сопровождение, прошляпало. Все прошляпали. Ничего криминального не произошло. Данные не утекли, сервис не упал. Просто нашли место, где можно ресурсы сильно сэкономить.

Для этого используют тестовую БД, а не театральную паузу на проде.

Для этого использовали:

1) Тестовую БД

2) Театральную паузу на проде.

Удаление данных - вещь не обратимая. Поэтому лучше лишний раз перебдеть.

И следуют поговорке "Семь раз на тесте один раз на проде" )))

И следовали поговорке )

А если использовать поток событий, то можно спокойно удалять из БД состояния.

Если что - предыдущее состояние можно будет восстановить используя сохраненный поток.

Ивент сорсинг? Несовместимо с GDPR и ему подобными. Разве что костылями похлеще традиционного софт делита.

Несовместимо с GDPR и ему подобными

ого! это неожиданно ...

эдак получается и блокчейн несовместим с GDPR ?

Если в нём сохранить персональную информацию, то несомненно не совместим. Но, слава Богу, такое там стараются не сохранять.

Много раз пользовались восстановлением отдельных записей в своей практике. Очевидно, что подход и необходимость хранения удаленных записей зависит от задачи. Применяю и подход с полями-пометками и подход с дополнительными таблицами. Предпочитаю на каждую "важную" оригинальную таблицу делать таблицу-копию для удаленных записей с дополнительными полями. Вариант ужимать все в какой-нибудь строку/массив байтов применяли (правда не для хранения удаленных записей), и, если честно, не очень понравилось. Слишком много доп телодвижений нужно для разбора в обратном порядке, но под специфическую задачу, почему бы и нет.

UFO just landed and posted this here

Ну понятие "важность" оно относительное. Систему с хранением истории всех изменений тоже используем на отельных таблицах, но вся "важность" заключается лишь в том, что пользователям захотелось вести "расследования" и находить "крайнего", но на производственные процессы и надежность это никак не повлияло, чисто психологический эффект. )

Не нужно ни мягкое ни жесткое, ни удаление, ни обновление. Нужен CQRS / ES и забыть навсегда про всю эту срань со всякими deleted_at и updated_by :)))

Шутка, конечно, но во всякой шутке, как известно... :)

А чем плохи исторические таблицы в случаях, если есть вероятность необходимости восстановления? С датой начала действия записи, датой окончания. По моему такой подход более гибкий - можно понять, сколько времени запись уже не активна, когда можно без больших рисков архивировать/удалять её?

Тем, что приходится объяснять пользователям разницу между исправлением ошибки (опечатка при занесении фамилии оператором ) и изменением данных (смена фамилии) и постоянно контролировать корректоность использования.

Во-первых, логика мягкого удаления растекается на все части кода.

Логика может и не "растекаться", если вы создадите представление, например

create view v_active_customer as

SELECT * FROM customer WHERE deleted_at IS NULL;

и в дальнейшем использовать это представление в запросах на выборку вместо таблицы customer.

и добавить триггеры, которые будут обновлять в связанных таблицах такие же deleted_at.

UPD. Хотя нет, так потеряется информация о тех связанных сущностях, которые были удалены отдельно...

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

А вот это очень странная претензия. Неужели автор не понимает, что для того и придумали "мягкое удаление" чтобы ни сам объект, ни ссылающиеся на него объекты не были физически удалены из таблиц.

Вот было бы веселье, если бы клиента удалили "мягко", а все его накладные удалили бы "жёстко". В таком случае при восстановлении ошибочно удалённого клиента командой update, мы были бы неприятно удивлены тем, что клиента мы восстановили, а его накладные куда-то безвозвратно пропали :-(

Не знаю как вы пришли к такому выводу, очевидно имеется ввиду мягкое удаление накладных.

Я знаете чего подумал? А зачем вообще в явном виде "мягко" удалять сущность, зависимую только от родительской сущности? Ведь если родительская сущность удалена "мягко", то по сути в БД уже имеется информация о том, что все сущности, зависящие только от мягко удалённой родительской, могут считаться мягко удалёнными, а в случае восстановления родительской сущности, они автоматом считаются восстановленными. То есть зависимая сущность пользуется столбцом deleted_at родительской сущности (с помощью операции соединения по ключевому столбцу).

Нормальное решение, единственное - трудозатратная, появиться необходимость join'а всех родительских таблиц по цепочке.

Можно создать представление как вы написали выше, это легко, но могут быть проблемы если используете ORM. (зависить от ORM)

Раз зашла речь про накладные, то вспомним 1С:
накладная же не просто так висит, чтобы мы знали кто чего зачем покупал, под нее подвязана таблица взаиморасчетов и остатков на складе. И если мы "мягко" удалим неактуального контрагента, то при удалении его накладных уедет итоговый остаток по складу. Так что надо будет без автомата (на уровне СУБД) принимать решение, мы считаем что поступлений товара не было, или формируем некую сводную запись приходе товара "с божьей помощью" и подставляем ее в качестве основания для всех приходов вместо мягко удаленных накладных. (правда как тогда возрождать их не понятно становится.)

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

Увидел название публикации в потоках и приуныл : "опять про зубы"(( Очень рад, что ошибся.

То, что попало в информационную систему, должно в ней оставаться на все время ее работы. Полностью удалять данные можно только если можно считать, что их как будто вообще никогда не было. Создали новость с неправильным текстом, не опубликовали, можно полностью удалить. Создали тестового пользователя, заказов на него не создавали, тоже можно удалить.

Вот только есть всякие GDPR. И пользователи желающие странного, например удалять что-то.

GDPR требует
1. удалять лишь персональные данные, а не все данные, т.е. иногда можно просто удалить имена, адреса и т.п., но оставить анонимные идентификаторы, чтобы сохранить целостность;
2. эти данные должны потерять свою значимость для контролера данных, т.е. у него больше нет легальных причин их хранить (см. 17.1(b): "..., and where there is no other legal ground for the processing", 17.3(b): "for compliance with a legal obligation which requires processing by Union or Member State law to which the controller is subject or for the performance of a task carried out in the public interest or in the exercise of official authority vested in the controller")

Так что, если скажем данные все ещё нужны для отчётности перед надзорными органами (бухгалтерия и проч), и данные были собраны законно (в частности, пользователь был уведомлен об этом и дал согласие), то у вас нет обязанности бежать удалять эти данные по первому запросу, ибо у вас есть legal grounds.

А если было 2 пользователя, с кучей заказов, потом оказалось, что это 1 человек и надо объединить, как правильно поступить?

Тут надо объединить. То есть в соответствии с бизнес-требованиями можно считать, что 2 пользователей никогда не было, а всегда был только 1.

Добавить в таблицу пользователей поле «AKA» где хранить ссылку на альтер-эго (организовав кольцевой список), и оставить всё остальное как есть :)

Есть вариант из коробки, просто в скобках после фио писать примечание. )

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

Тут выше уже высказывались по поводу накладных и прочего, попробую обобщить:


Так называемая "утеря внешних ключей" — это не недостаток мягкого удаления, а то ради чего мягкое удаление и делается.


Учётная запись пользователя деактивирована? Все созданные и изменённые им объекты продолжают на него ссылаться, и это не ошибка.


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


Тип/класс/шаблон/схема документа удалён и больше не ходит в СЭД? Архивные документы продолжают ссылаться на него, и это снова не ошибка.

Можно же внешние ключи сделать, можно добавить поле deleted, которое будет = 0 если объект не удален, а для удаления в это поле надо записать id объекта и тогда можно внешний ключ по двум колонкам сделать.

create table my_users (
	id bigserial,
	login text,
	deleted bigint default 0,
	primary key(id, deleted)
);

create table my_invoices (
	id bigserial,
	amount numeric,
	user_id bigint,
	user_deleted bigint,
	foreign key(user_id, user_deleted) references my_users (id, deleted) on update cascade on delete cascade
);


insert into my_users (login) values
('ivan'),
('sergey'),
('peter')
;

insert into my_invoices (amount, user_id, user_deleted) values
(100, 1, 0),
(200, 1, 0),
(500, 2, 0),
(400, 2, 0),
(700, 3, 0)
;

Запрос на получение накладных

nm7=> select * from my_invoices where user_deleted = 0;
 id | amount | user_id | user_deleted 
----+--------+---------+--------------
  3 |    500 |       2 |            0
  4 |    400 |       2 |            0
  5 |    700 |       3 |            0
  1 |    100 |       1 |            0
  2 |    200 |       1 |            0
(5 rows)

"Удаляем" пользователя ivan

nm7=> update my_users set deleted = id where login='ivan';
UPDATE 1
nm7=>

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

nm7=> select * from my_invoices where user_deleted = 0;
 id | amount | user_id | user_deleted 
----+--------+---------+--------------
  3 |    500 |       2 |            0
  4 |    400 |       2 |            0
  5 |    700 |       3 |            0
(3 rows)

Однако при желании можем и все накладные посмотреть.

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

Если автор не знает, это не значит, что это не использовалось.

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

Сначала мягкое удаление было реализовано похожим способом is_deleted=true (не дата, а bool). Но учитывая, что во всех запросах нужно было об этом помнить, как упомянуто в статье, со временем перешли на другую схему. У объектов уже был атрибут статус и вместо отдельного is_deleted появился новый статус status=STATUS_DELETED. Как правило все запросы и так учитывают статус STATUS_APPROVED поэтому удаленные записи не попадают в выборку.

И да после внедрения мягкого удаления оно применялось и было восстановлено довольно много записей.

Еще раз задумался о всех проблемах мягкого удаления, они действительно есть)

Лучше использовать дату удаления, а не статус. Проще реализовать в УИ просмотр/исправление неактивных объектов (Например, при формировании годового отчета нашли ошибку в наименовании уже неактивного контрагента ).

Насколько я знаю, ни разу за десять с лишним лет ни в одной из этих компаний мягкое удаление не использовалось для восстановления данны

В своём опыте многократно встречал восстановление данных, удалённых мягким способом, и на разных проектах.

UFO just landed and posted this here

Часто так и делают, на другом сервере полностью аналогичная база, только чисто архивная, туда переносятся записи из основной после определенного "срока годности" данных по расписанию. И рабочая база не раздута "удаленными мягко" записями (а их реально может быть даже больше чем рабочих данных при длительной работе), что благотворно влияет на производительность и храним все данные. К тому же архив можно располагать на более дешевых носителях (HDD), в то время как рабочая база крутится на SSD.

UFO just landed and posted this here
А ещё данные, на архивное хранение, можно переносить партициями.

Я вот тоже не особо понимаю, что мешает иметь рядом табличку (копию БД с той же структурой?) с именем "<table_name>_deleted".

По идее в мастер данных, а список клиентов несомненно таким является, нужно поле Актуальность.
В принципе поле дата удаления вполне подходит под эту роль

Sign up to leave a comment.

Articles