Обработка удаленных пользователей - отдельная или та же таблица?

19

Сценарий состоит в том, что у меня расширяется набор пользователей, и со временем пользователи отменят свои учетные записи, которые мы в настоящее время помечаем как «удаленные» (с флагом) в той же таблице.

Если пользователи с одним и тем же адресом электронной почты (как пользователи входят в систему) желают создать новую учетную запись, они могут зарегистрироваться снова, но создается новая учетная запись. (У нас есть уникальные идентификаторы для каждой учетной записи, поэтому адреса электронной почты могут дублироваться среди живых и удаленных).

Что я заметил, так это то, что во всей нашей системе мы постоянно запрашиваем таблицу пользователей, проверяя, что пользователь не удален, тогда как я думаю, что нам вообще не нужно это делать ... ! [Разъяснение1: под «постоянными запросами» я имел в виду, что у нас есть запросы, которые выглядят так: «... ОТ пользователей, ГДЕ isdeleted =" 0 "И ...". Например, нам может потребоваться получить всех пользователей, зарегистрированных для всех собраний на определенную дату, поэтому в запросе THAT у нас также есть пользователи FROM WHERE isdeleted = "0" - это проясняет мою точку зрения?]

(1) continue keeping deleted users in the 'main' users table
(2) keep deleted users in a separate table (mostly required for historical
    book-keeping)

Каковы плюсы и минусы любого подхода?

Алан Битс
источник
По каким причинам вы держите пользователей?
Кепла
2
Это называется soft-delete. См. Также Удаление записей базы данных unpermenantley (soft-delete)
Sjoerd
@keppla - он упоминает, что: "исторический бухгалтерский учет".
ChrisF
@ChrisF: меня интересовала сфера охвата: хочет ли он вести книги только пользователей, или есть еще какие-то данные (например, комментарии, платежи и т. Д.)
keppla
Это может помочь перестать думать о них , как удаляются (не верно) , и начать думать о своем счете отмененных (что является правдой).
Майк Шеррилл 'Cat Recall'

Ответы:

13

(1) продолжать хранить удаленных пользователей в «основной» таблице пользователей

  • Плюсы: более простые запросы во всех случаях
  • Минусы: может ухудшить производительность с течением времени, если есть большое количество пользователей

(2) хранить удаленных пользователей в отдельной таблице (в основном требуется для исторического учета)

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

  • Плюсы: более простое обслуживание для таблицы активных пользователей, стабильная производительность
  • Минусы: нужны разные запросы к таблице истории; однако, поскольку большая часть приложения не заинтересована в этом, этот отрицательный эффект, вероятно, ограничен
Петер Тёрёк
источник
11
Таблица разделов (в IsDeleted) устранит проблемы с производительностью при использовании одной таблицы.
Ян
1
@Ian, если каждый запрос не снабжен IsDeleted в качестве критерия запроса (что, похоже, не входит в исходный вопрос), разбиение может даже привести к снижению производительности.
Адриан Шум
1
@Adrian, я предполагал, что наиболее распространенные запросы будут во время входа в систему и что только ни один удаленный пользователь не сможет войти в систему.
Ян
1
Используйте индексированное представление для isdeleted, если оно становится проблемой производительности, и вы хотите получить выгоду от одной таблицы.
Джеффо
10

Я настоятельно рекомендую использовать ту же таблицу. Основная причина - целостность данных. Скорее всего, будет много таблиц со связями в зависимости от пользователей. Когда пользователь удаляется, вы не хотите оставлять эти записи сиротами.
Наличие осиротевших записей усложняет соблюдение ограничений и затрудняет поиск исторической информации. Другое поведение, которое нужно учитывать, когда пользователь предоставляет использованную электронную почту, если вы хотите, чтобы он восстановил все свои старые записи. Это будет работать автоматически с помощью мягкого удаления. Что касается его кодирования, например, в моем текущем приложении на c # linq, предложение where Удалено = 0 автоматически добавляется в конец всех запросов.

Андрей
источник
7

«Что я заметил, так это то, что по всей нашей системе мы постоянно запрашиваем таблицу пользователей, проверяя, не удален ли пользователь»

Это дает мне неприятный запах дизайна. Вы должны скрыть такую ​​логику. Например, вы должны UserServiceпредоставить метод isValidUser(userId)для использования «по всей вашей системе», а не делать что-то вроде:

msgstr "получить запись пользователя, проверить, помечен ли пользователь как удалённый".

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

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

Вещи для рассмотрения включают в себя:

  • Как долго должна удаляться удаленная запись?
  • Какова доля удаленных записей?
  • Будет ли проблема ссылочной целостности (например, пользователь ссылается из другой таблицы), если вы фактически удалите ее из таблицы?
  • Вы рассматриваете возможность повторного открытия пользователя?

Обычно я бы взял комбинированный путь:

  1. Отметить запись как удаленную (чтобы сохранить ее для функциональных требований, таких как повторное открытие кондиционера или проверка недавно закрытого кондиционера).
  2. По истечении заданного периода времени переместите удаленную запись в архивную таблицу (для целей бухгалтерского учета).
  3. Очистите его после некоторого предопределенного периода архива.
Адриан Шум
источник
1
[Разъяснение1: под «постоянными запросами» я имел в виду, что у нас есть запросы, которые выглядят так: «... ОТ пользователей, ГДЕ isdeleted =" 0 "И ...". Например, нам может потребоваться извлечь всех пользователей, зарегистрированных для всех собраний на определенную дату, поэтому в запросе TH у нас также есть пользователи FROM WHERE isdeleted = "0" - это проясняет мою точку зрения?] @Adrian
Alan Beats
Да намного понятнее. :) Если я делаю это, я бы предпочел сделать это как изменение статуса пользователя, а не как физическое / логическое удаление. Хотя объем кода не уменьшится ("and isDeleted = '0'" vs "и" state <> 'TERMINATED' "), но все будет выглядеть намного разумнее, и также нормально иметь другое пользовательское состояние. Периодическая очистка пользователей TERMINATED также может быть выполнена, как было предложено в моем предыдущем ответе)
Адриан Шум
5

Чтобы правильно ответить на этот вопрос, сначала нужно решить: что означает «удалить» в контексте этой системы / приложения?

Чтобы ответить на этот вопрос, вам нужно ответить еще на один вопрос: почему удаляются записи?

Существует ряд веских причин, по которым пользователю может потребоваться удалить данные. Обычно я нахожу, что существует только одна причина (для каждой таблицы), почему удаление может быть необходимым. Вот некоторые примеры:

  • Освободить дисковое пространство;
  • Жесткое удаление требуется в соответствии с политикой хранения / конфиденциальности;
  • Поврежденные / безнадежно некорректные данные, их легче удалить и восстановить, чем восстановить.
  • Большинство строк будут удалены, например, журнал таблицы ограничены X записей / дней.

Есть также несколько очень плохих причин для жесткого удаления (подробнее об этом позже):

  • Чтобы исправить небольшую ошибку. Это обычно подчеркивает лень разработчика и враждебный пользовательский интерфейс.
  • Чтобы «аннулировать» транзакцию (например, счет, который никогда не должен был быть выставлен).
  • Потому что ты можешь .

Вы спросите, почему это так важно? Что не так с хорошим оле DELETE?

  • В любой системе, даже удаленно привязанной к деньгам, жесткое удаление нарушает всевозможные бухгалтерские ожидания, даже если перемещено в таблицу архива / надгробной плиты. Правильный способ справиться с этим - задним числом .
  • Архивные таблицы имеют тенденцию отклоняться от действующей схемы. Если вы забудете хотя бы об одном добавленном столбце или каскаде, вы просто навсегда потеряете эти данные.
  • Жесткое удаление может быть очень дорогой операцией, особенно с каскадами . Многие люди не понимают, что каскадирование более чем одного уровня (или, в некоторых случаях, любое каскадирование, в зависимости от СУБД) приведет к операциям на уровне записей вместо операций установки.
  • Повторное частое жесткое удаление ускоряет процесс фрагментации индекса.

Итак, мягкое удаление лучше, верно? Нет, не совсем:

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

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

Уди Дахан написал об этом в « Не удалять - просто не делай» . Существует всегда какой - то задачи, сделки, активность , или (мой предпочтительный термин) событие , которое на самом деле представляет собой «Удалить». Это нормально, если впоследствии вы захотите денормализовать в таблицу «текущее состояние» для повышения производительности, но сделайте это после того, как вы завершили транзакционную модель, а не раньше.

В этом случае у вас есть «пользователи». Пользователи по сути являются клиентами. Клиенты имеют деловые отношения с вами. Эти отношения не просто исчезают, потому что они отменили свою учетную запись. Что на самом деле происходит:

  • Клиент создает аккаунт
  • Клиент отменяет аккаунт
  • Клиент обновляет аккаунт
  • Клиент отменяет аккаунт
  • ...

В каждом случае это один и тот же клиент и, возможно, одна и та же учетная запись (т. Е. Каждое продление учетной записи является новым соглашением об обслуживании). Так почему вы удаляете строки? Это очень легко моделировать:

+-----------+       +-------------+       +-----------------+
| Account   | --->* | Agreement   | --->* | AgreementStatus |
+-----------+       +-------------+       +----------------+
| Id        |       | Id          |       | AgreementId     |
| Name      |       | AccountId   |       | EffectiveDate   |
| Email     |       | ...         |       | StatusCode      |
+-----------+       +-------------+       +-----------------+

Вот и все. Это все, что нужно сделать. Вам никогда не нужно ничего удалять. Выше приведен довольно распространенный дизайн, который обеспечивает хорошую степень гибкости, но его можно немного упростить; Вы можете решить, что вам не нужен уровень «Соглашение», и просто «Account» перейти к таблице «AccountStatus».

Если в вашем приложении часто требуется получить список активных соглашений / учетных записей, то это (немного) хитрый запрос, но для этого нужны представления:

CREATE VIEW ActiveAgreements AS
SELECT agg.Id, agg.AccountId, acc.Name, acc.Email, s.EffectiveDate, ...
FROM AgreementStatus s
INNER JOIN Agreement agg
    ON agg.Id = s.AgreementId
INNER JOIN Account acc
    ON acc.Id = agg.AccountId
WHERE s.StatusCode = 'ACTIVE'
AND NOT EXISTS
(
    SELECT 1
    FROM AgreementStatus so
    WHERE so.AgreementId = s.AgreementId
    AND so.EffectiveDate > s.EffectiveDate
)

И вы сделали. Теперь у вас есть кое-что со всеми преимуществами программных удалений, но ни с одним из недостатков:

  • Потерянные записи не являются проблемой, потому что все записи видны всегда; вы просто выбираете из другого ракурса, когда это необходимо.
  • «Удаление» - это обычно невероятно дешевая операция - просто вставка одной строки в таблицу событий.
  • Там никогда не бывает шанс потери истории, когда - либо , независимо от того , насколько сильно вы завинчивать.
  • Вы по-прежнему можете жестко удалить учетную запись, если вам нужно (например, по соображениям конфиденциальности), и быть уверенным в том, что удаление будет происходить чисто и не будет мешать любой другой части приложения / базы данных.

Единственная проблема, которую нужно решить, это проблема производительности. Во многих случаях это на самом деле не является проблемой из-за включенного кластерного индекса AgreementStatus (AgreementId, EffectiveDate)- там очень мало запросов ввода-вывода. Но если это когда-либо является проблемой, есть способы решить это, используя триггеры, индексированные / материализованные представления, события уровня приложения и т. Д.

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

Aaronaught
источник
1

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

Итак, я рекомендую отдельные таблицы истории.

Джесси С. Слайсер
источник
Конечно, без каскадных сдвигов истории, у вас точно такая же проблема?
Гленатрон
Нет в ваших таблицах активных записей, нет.
Джесси С. Слайсер
Так что же происходит с дочерними записями, которые FK покидают пользовательскую таблицу после того, как пользователь был добавлен в таблицу истории?
Гленатрон
Ваш триггер (или бизнес-логика) также отправит дочерние записи в соответствующие таблицы истории. Дело в том, что вы не можете физически удалить родительскую запись (для перехода в историю), если база данных не сообщит вам, что вы нарушили RI. Таким образом, вы вынуждены создать его. Удаленный флаг не вызывает каскадное мягкое удаление.
Джесси С. Слайсер
3
Зависит от того, что действительно означает ваше мягкое удаление. Если это просто способ их деактивировать, нет необходимости корректировать записи, относящиеся к деактивированной учетной записи. Похоже, просто данные для меня. И да, мне приходится иметь дело с этим и в системе, которую я не проектировал. Это не значит, что тебе должно это нравиться.
Джеффо
1

Разбить стол на две части было бы самой глупой вещью.

Вот два очень простых шага, которые я бы порекомендовал:

  1. Переименуйте таблицу «users» в «allusers».
  2. Создайте представление под названием «пользователи» как «выберите * из всех пользователей, где удалено = ложь».

PS Извините за несколько месяцев задержки ответа!

Майк Накис
источник
0

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

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

ChrisF
источник
0

Вы не упоминаете СУБД в использовании. Если у вас Oracle с соответствующей лицензией, вы можете рассмотреть возможность разделения таблицы пользователей на два раздела: активные и удаленные пользователи.

mczajk
источник
Затем вы должны перемещать строки из одного раздела в другой при удалении пользователей, что, безусловно, не означает, что разделы предназначены для использования.
Петер Тёрёк
@ Петер: А? Вы можете разбить на любые критерии, которые вы хотите, в том числе удаленный флаг.
Аарона
@ Аронот, хорошо, я неправильно это сформулировал. СУБД может выполнить эту работу за вас, но это все еще дополнительная работа (поскольку строка должна физически перемещаться из одного места в другое, возможно, в другой файл), и это может ухудшить физическое распределение данных.
Петер Тёрёк