Те, кто знаком хотя бы с парой разных окружений баз данных продакшена, скорее всего, знают паттерн «мягкого удаления» (soft deletion): вместо удаления данных напрямую конструкцией DELETE таблицы получают дополнительную временную метку deleted_at и удаление выполняется конструкцией обновления:

UPDATE foo SET deleted_at = now() WHERE id = $1;

Мягкое удаление необходимо для того, чтобы удаление выполнялось безопаснее и было обратимым. После того, как запись удалили «жёстким» DELETE, теоретически её всё равно можно восстановить, углубившись в слой хранения, но нужно сказать, что вернуть её очень сложно. В теории, при мягком удалении достаточно снова присвоить deleted_at значение NULL, и на этом всё:

-- и волшебным образом всё вернулось на место!!
UPDATE foo SET deleted_at = NULL WHERE id = $1;

Недостатки: утечка кода


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

SELECT *
FROM customer
WHERE id = @id
    AND deleted_at IS NULL;

А если забыть о дополнительном предикате для deleted_at, то это может иметь серьёзные последствия: случайно будут возвращены данные, которые больше не должны быть видимы.

Некоторые ORM или ORM-плагины упрощают ситуацию, автоматически добавляя дополнительный оператор deleted_at в каждый запрос (см., например, acts_as_paranoid), но только то, что записи оказываются скрытыми, не улучшает положение. Если конструкция выполнит запрос к базе данных напрямую, то, скорее всего, о deleted_at забудут, потому что обычно этот оператор добавляет ORM.

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


Ещё одним последствием мягкого удаления является, по сути, утеря внешних ключей.

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

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

ERROR:  update or delete on table "customer" violates
    foreign key constraint "invoice_customer_id_fkey" on table "invoice"

DETAIL:  Key (id)=(64977e2b-40cc-4261-8879-1c1e6243699b) is still
    referenced from table "invoice".

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

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

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


За последние несколько лет сделаны серьёзные шаги для обеспечения защиты данных потребителей, например, ввод в действие GDPR в Европе. Поэтому в общем случае стоит опасаться неограниченного по времени хранения данных, а по умолчанию большинство мягко удалённых строк будет находиться именно в таком состоянии.

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

Однако те же внешние ключи, которые из-за мягкого удаления стали практически бесполезными, теперь усложняют эту работу, потому что запись нельзя удалить, не гарантировав удаления и всех её зависимостей (ON DELETE CASCADE может делать это автоматически, однако использование каскада достаточно опасно и не рекомендуется для данных с повышенной точностью воспроизведения).

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

WITH team_deleted AS (
    DELETE FROM team
    WHERE (
        team.archived_at IS NOT NULL
        AND team.archived_at < @archived_at_horizon::timestamptz
    )
    RETURNING *
),

--
-- team resources
--
cluster_deleted AS (
    DELETE FROM cluster
    WHERE team_id IN (
        SELECT id FROM team_deleted
    )
    OR (
        archived_at IS NOT NULL
        AND archived_at < @archived_at_horizon::timestamptz
    )
    RETURNING *
),
invoice_deleted AS (
    DELETE FROM invoice
    WHERE team_id IN (
        SELECT id FROM team_deleted
    )
    OR (
        archived_at IS NOT NULL
        AND archived_at < @archived_at_horizon::timestamptz
    )
    RETURNING *
),

--
-- cluster + team resources
--
subscription_deleted AS (
    DELETE FROM subscription
    WHERE cluster_id IN (
        SELECT id FROM cluster_deleted
    ) OR team_id IN (
        SELECT id FROM team_deleted
    )
    RETURNING *
)

SELECT 'cluster', array_agg(id) FROM cluster_deleted
UNION ALL
SELECT 'invoice', array_agg(id) FROM invoice_deleted
UNION ALL
SELECT 'subscription', array_agg(id) FROM subscription_deleted
UNION ALL
SELECT 'team', array_agg(id) FROM team_deleted;

Полная версия запроса в пять раз длиннее и включает в себя 30 полных отдельных таблиц. Здорово, что это работает, но всё слишком уж сложно.

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

Действительно ли работает восстановление?


Повторюсь, теоретически мягкое удаление является препятствием для случайной утери данных. И в качестве последнего аргумента против него я прошу вас реалистично оценить, действительно ли восстановление удалённого когда-то выполняется.

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

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

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

Насколько я знаю, ни разу за десять с лишним лет ни в одной из этих компаний мягкое удаление не использовалось для восстановления данных. [Дополнение: бывший коллега из Stripe только что написал мне, что, по крайней мере, давно мы иногда восстанавливали записи клиентов для пользователей, попавших в реальные неприятности, но это было редко и не приветствовалось. Спасибо, О. Б.!]

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

Пару раз в Heroku важный пользователь случайно удалял приложение и хотел его восстановить. У нас использовалось мягкое удаление, и теоретически другие побочные эффекты удаления невозможно было откатить, но нас убеждали не пробовать, потому что никто раньше этого не делал, а пытаться делать что-то в срочном порядке — самое неподходящее для этого время; что-то совершенно точно пойдёт не так, и пользователь окажется в плохом состоянии. Вместо этого мы создавали новое приложение и помогали пользователю скопировать в него окружение и данные из удалённого приложения. То есть даже в ситуации, когда мягкое удаление теоретически наиболее полезно, мы его не использовали.

Альтернатива: таблица удалённых записей


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

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

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

CREATE TABLE deleted_record (
    id uuid PRIMARY KEY DEFAULT gen_ulid(),
    deleted_at timestamptz NOT NULL default now(),
    original_table varchar(200) NOT NULL,
    original_id uuid NOT NULL,
    data jsonb NOT NULL
);

Тогда удаление будет выглядеть так:

WITH deleted AS (
    DELETE FROM customer
    WHERE id = @id
    RETURNING *
)
INSERT INTO deleted_record
		(original_table, original_id, data)
SELECT 'foo', id, to_jsonb(deleted.*)
FROM deleted
RETURNING *;

Такая система имеет недостаток по сравнению с deleted_at: процесс выбора столбцов в jsonb не так-то легко обратить. Хотя это можно сделать, для этого, скорее всего, пришлось бы создавать запросы для единичных случаев и вмешиваться вручную. Но это вполне может вас устроить — просто вспомните, как часто вам на самом деле приходилось пробовать восстанавливать данные после удаления.

Эта техника решает все перечисленные выше проблемы:

  • В запросы для обычных неудалённых данных больше не нужно добавлять повсюду deleted_at IS NULL.
  • Внешние ключи продолжают работать. Попытка удаления записи без её зависимостей будет являться ошибкой.
  • Жёсткое удаление старых записей по требованию законодательства становится очень простым: DELETE FROM deleted_record WHERE deleted_at < now() - '1 year'::interval.

К удалённым данным становится чуть сложнее добраться, но не сильно, и они всё равно хранятся на случай, если кому-то нужно будет на них взглянуть.

Комментарии (42)


  1. fireSparrow
    20.07.2022 18:03
    +22

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

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

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

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

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

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

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

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

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

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

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

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


    1. oracle_schwerpunkte
      21.07.2022 10:23
      +7

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

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


  1. fireSparrow
    20.07.2022 18:18
    +16

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

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

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


    1. alex1t
      20.07.2022 19:04
      +1

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

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


      1. terantul
        20.07.2022 20:54
        +1

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

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

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

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


        1. censor2005
          21.07.2022 10:02

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


    1. oracle_schwerpunkte
      21.07.2022 10:45
      +2

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


  1. vadimr
    20.07.2022 18:41
    +11

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

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

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


    1. ZloyVampir
      20.07.2022 20:58

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


      1. mentin
        20.07.2022 21:18
        +5

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


      1. InChaos
        21.07.2022 12:02
        +1

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

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

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

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


  1. vassabi
    20.07.2022 18:48
    +1

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

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


    1. YegorP
      20.07.2022 20:04
      +1

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


      1. vassabi
        21.07.2022 12:26
        -1

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

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

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


  1. Emulyator
    20.07.2022 18:52
    +1

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


    1. Nbx
      20.07.2022 19:12
      +2

      Предпочитаю на каждую «важную» оригинальную таблицу делать таблицу-копию для удаленных записей с дополнительными полями.
      Для реально важных — нужно делать дополнительные таблицы аудита с историей всех изменений (добавлений, обновлений и удалений). На основную навесить триггер и при любых изменений сохранять туда, с дополнительной информацией о типе изменения, времени изменения и пользователе который изменения сделал. Если конечно позволяют производительность, частота изменений и объёмы данных. Плюс не забывать о правах на таблицы аудита, чтобы нельзя было данные изменить задним числом.

      Главное автоматизировать создание триггеров и схем таблиц аудита, чтобы при изменениях схемы не надо было вручную всё шерстить.


      1. Emulyator
        20.07.2022 20:14

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


  1. nronnie
    20.07.2022 19:04
    +3

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

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


  1. feogil
    20.07.2022 19:05
    +3

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


    1. oracle_schwerpunkte
      21.07.2022 10:51
      +2

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


  1. user5000
    20.07.2022 19:05
    +7

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

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

    create view v_active_customer as

    SELECT * FROM customer WHERE deleted_at IS NULL;

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


    1. savostin
      20.07.2022 20:59

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

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


  1. user5000
    20.07.2022 19:10
    +4

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

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

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


    1. 0x450x6c
      20.07.2022 19:16

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


      1. user5000
        20.07.2022 19:58
        +1

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


        1. 0x450x6c
          20.07.2022 21:08

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

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


        1. Ndochp
          21.07.2022 00:47
          +1

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


  1. Rumidu
    20.07.2022 21:45
    +8

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


  1. michael_v89
    20.07.2022 22:22
    +3

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


    1. vikarti
      21.07.2022 14:46

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


    1. Emulyator
      21.07.2022 16:04

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


      1. michael_v89
        21.07.2022 17:36

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


  1. mayorovp
    20.07.2022 23:53
    +8

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


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


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


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


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


  1. guryanov
    21.07.2022 00:55
    -1

    Можно же внешние ключи сделать, можно добавить поле 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)
    

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


  1. un1t
    21.07.2022 09:13
    +3

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

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

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

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

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

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


    1. oracle_schwerpunkte
      21.07.2022 11:02

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


  1. Newbilius
    21.07.2022 09:19

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

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


  1. sden77
    21.07.2022 09:42
    +3

    Архив старых записей может быть и неплохой идеей, но не в виде json, а в виде таблицы с той же структурой, что и основная. Чтобы можно было в интерфейсе приложения перейти в раздел "архив" и искать там нужную информацию таким же способом, как и в основной БД


    1. InChaos
      21.07.2022 12:14
      +1

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


      1. sden77
        21.07.2022 12:31
        +1

        Ну собственно и я про то же, не понимаю зачем автор предлагает заворачивать записи в jdon


      1. GlukKazan
        21.07.2022 12:49

        А ещё данные, на архивное хранение, можно переносить партициями.


  1. acsent1
    21.07.2022 15:36

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