Gestion des utilisateurs supprimés - table distincte ou même?

19

Le scénario est que j'ai un ensemble croissant d'utilisateurs, et au fil du temps, les utilisateurs annuleront leurs comptes que nous marquons actuellement comme «supprimés» (avec un indicateur) dans le même tableau.

Si les utilisateurs avec la même adresse e-mail (c'est ainsi que les utilisateurs se connectent) souhaitent créer un nouveau compte, ils peuvent se réinscrire, mais un NOUVEAU compte est créé. (Nous avons des identifiants uniques pour chaque compte, donc les adresses e-mail peuvent être dupliquées parmi celles en direct et supprimées).

Ce que j'ai remarqué, c'est que dans tout notre système, dans le cours normal des choses, nous interrogeons constamment la table des utilisateurs en vérifiant que l'utilisateur n'est pas supprimé, alors que je pense que nous n'avons pas du tout besoin de le faire ... ! [Clarification1: en 'interrogeant constamment', je voulais dire que nous avons des requêtes qui sont comme: '... DEPUIS les utilisateurs OERE isdeleted = "0" ET ...'. Par exemple, nous pouvons avoir besoin de récupérer tous les utilisateurs enregistrés pour toutes les réunions à une date particulière, donc dans CETTE requête, nous avons également des utilisateurs FROM OERE isdeleted = "0" - cela clarifie-t-il mon point?]

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

Quels sont les avantages et les inconvénients de l'une ou l'autre approche?

Alan Beats
la source
Pour quelles raisons gardez-vous les utilisateurs?
keppla
2
C'est ce qu'on appelle l'effacement progressif. Voir aussi Suppression d'enregistrements de base de données unpermenantley (soft-delete)
Sjoerd
@keppla - il mentionne que: "comptabilité historique".
ChrisF
@ChrisF: je m'intéressais à la portée: veut-il garder des livres uniquement des utilisateurs, ou y a-t-il encore des données jointes (commentaires eG, paiements, etc.)
keppla
Il peut être utile d'arrêter de les considérer comme supprimés (ce qui n'est pas vrai) et de commencer à penser à leur compte comme annulé (ce qui est vrai).
Mike Sherrill 'Cat Recall'

Réponses:

13

(1) continuer à conserver les utilisateurs supprimés dans le tableau des utilisateurs «principaux»

  • Avantages: requêtes plus simples dans tous les cas
  • Inconvénients: peut dégrader les performances au fil du temps, si le nombre d'utilisateurs est élevé

(2) conserver les utilisateurs supprimés dans un tableau séparé (principalement requis pour la comptabilité historique)

Vous pouvez par exemple utiliser un déclencheur pour déplacer automatiquement les utilisateurs supprimés vers la table d'historique.

  • Avantages: maintenance plus simple de la table des utilisateurs actifs, performances stables
  • Inconvénients: besoin de différentes requêtes pour la table d'historique; cependant, comme la plupart des applications ne sont pas censées s'y intéresser, cet effet négatif est probablement limité
Péter Török
la source
11
Une table de partition (sur IsDeleted) supprimerait les problèmes de performances liés à l'utilisation d'une seule table.
Ian
1
@Ian, sauf si chaque requête est fournie avec IsDeleted comme critère de requête (ce qui ne semble pas être la question d'origine), le partitionnement peut même entraîner une dégradation des performances.
Adrian Shum
1
@Adrian, je supposais que les requêtes les plus courantes seraient au moment de la connexion et que seuls les utilisateurs supprimés seraient autorisés à se connecter.
Ian
1
Utilisez une vue indexée sur isdeleted si cela devient un problème de performances et que vous souhaitez bénéficier d'une seule table.
JeffO
10

Je recommande fortement d'utiliser le même tableau. La raison principale est l'intégrité des données. Il y aura très probablement de nombreuses tables avec des relations en fonction des utilisateurs. Lorsqu'un utilisateur est supprimé, vous ne souhaitez pas laisser ces enregistrements orphelins.
La possession d'enregistrements orphelins rend à la fois plus difficile l'application des contraintes et rend plus difficile la recherche d'informations historiques. L'autre comportement à considérer si lorsqu'un utilisateur fournit un e-mail utilisé si vous souhaitez qu'il récupère tous ses anciens enregistrements. Cela fonctionnerait automatiquement en utilisant la suppression logicielle. En ce qui concerne le codage, par exemple dans mon application c # linq actuelle, la clause where supprimé = 0 est automatiquement ajoutée à la fin de toutes les requêtes

Andrey
la source
7

"Ce que j'ai remarqué, c'est que dans tout notre système, dans le cours normal des choses, nous interrogeons constamment la table des utilisateurs en vérifiant que l'utilisateur n'est pas supprimé"

Cela me donne une mauvaise odeur de design. Vous devriez cacher une telle logique. Par exemple, vous devriez avoir une UserServiceméthode isValidUser(userId)à utiliser pour "l'ensemble de votre système", au lieu de faire quelque chose comme:

msgstr "obtenir le dossier utilisateur, vérifier si l 'utilisateur est marqué comme supprimé".

Votre façon de stocker l'utilisateur supprimé ne devrait pas affecter la logique métier.

Avec un tel type d'encapsulation, l'argument ci-dessus ne devrait plus affecter l'approche de votre persistance. Ensuite, vous pouvez vous concentrer davantage sur les avantages et les inconvénients liés à la persistance elle-même.

Les choses à considérer incluent:

  • Pendant combien de temps l'enregistrement supprimé doit-il être purgé?
  • Quelle est la proportion d'enregistrements supprimés?
  • Y aura-t-il un problème d'intégrité référentielle (par exemple, l'utilisateur est référé d'une autre table) si vous le supprimez réellement de la table?
  • Envisagez-vous de rouvrir l'utilisateur?

Normalement, je prendrais une voie combinée:

  1. Marquez l'enregistrement comme supprimé (afin de le conserver pour les exigences fonctionnelles, comme la réouverture de la climatisation ou la vérification de la climatisation récemment fermée).
  2. Après une période prédéfinie, déplacez l'enregistrement supprimé vers la table d'archivage (à des fins de comptabilité).
  3. Purgez-le après une période d'archivage prédéfinie.
Adrian Shum
la source
1
[Clarification1: en 'interrogeant constamment', je voulais dire que nous avons des requêtes qui sont comme: '... DEPUIS les utilisateurs OERE isdeleted = "0" ET ...'. Par exemple, nous pouvons avoir besoin de récupérer tous les utilisateurs enregistrés pour toutes les réunions à une date particulière, donc dans CETTE requête, nous avons également des utilisateurs FROM OERE isdeleted = "0" - cela clarifie-t-il mon point de vue?] @Adrian
Alan Beats
Ouais beaucoup plus clair. :) Si je fais cela, je préfère le faire en tant que changement de statut de l'utilisateur, au lieu de le regarder comme une suppression physique / logique. Bien que la quantité de code ne diminue pas ("et isDeleted = '0'" vs 'et "state <>' TERMINATED '"), mais tout semblera beaucoup plus raisonnable, et il est normal d'avoir également un état utilisateur différent. La purge périodique des utilisateurs TERMINÉS peut également être effectuée, comme suggéré dans ma réponse précédente)
Adrian Shum
5

Afin de répondre correctement à cette question, vous devez d'abord décider: Que signifie «supprimer» dans le contexte de ce système / application?

Pour répondre à cette question, vous devez répondre à une autre question: pourquoi les enregistrements sont-ils supprimés?

Il existe plusieurs bonnes raisons pour lesquelles un utilisateur peut avoir besoin de supprimer des données. Habituellement, je trouve qu'il y a exactement une raison (par table) pour laquelle une suppression peut être nécessaire. Quelques exemples sont:

  • Pour récupérer de l'espace disque;
  • Suppression définitive requise conformément à la politique de conservation / confidentialité;
  • Données corrompues / désespérément incorrectes, plus faciles à supprimer et à régénérer qu'à réparer.
  • La majorité des lignes sont supprimées, par exemple une table de journal limitée à X enregistrements / jours.

Il existe également de très mauvaises raisons pour la suppression définitive (plus d'informations sur ces raisons plus tard):

  • Pour corriger une erreur mineure. Cela souligne généralement la paresse des développeurs et une interface utilisateur hostile.
  • Pour «annuler» une transaction (par exemple, une facture qui n'aurait jamais dû être facturée).
  • Parce que vous le pouvez .

Pourquoi, demandez-vous, est-ce vraiment un gros problème? Quel est le problème avec un bon vieux DELETE?

  • Dans n'importe quel système, même à distance lié à l'argent, la suppression matérielle viole toutes sortes d'attentes comptables, même si elle est déplacée vers une table d'archive / pierre tombale. La bonne façon de gérer cela est un événement rétroactif .
  • Les tables d'archivage ont tendance à diverger du schéma en direct. Si vous oubliez même une colonne ou une cascade nouvellement ajoutée, vous venez de perdre définitivement ces données.
  • La suppression matérielle peut être une opération très coûteuse, en particulier avec les cascades . Beaucoup de gens ne se rendent pas compte que la cascade de plusieurs niveaux (ou dans certains cas toute cascade, selon le SGBD) entraînera des opérations au niveau de l'enregistrement au lieu d'opérations définies.
  • La suppression répétée et fréquente accélère le processus de fragmentation de l'index.

Donc, la suppression logicielle est meilleure, non? Non, pas vraiment:

  • La mise en place de cascades devient extrêmement difficile. Vous vous retrouvez presque toujours avec ce qui apparaît au client comme des lignes orphelines.
  • Vous ne pouvez suivre qu'une seule suppression. Que faire si la ligne est supprimée et annulée plusieurs fois?
  • Les performances de lecture en souffrent, bien que cela puisse être quelque peu atténué par le partitionnement, les vues et / ou les index filtrés.
  • Comme indiqué précédemment, il peut en fait être illégal dans certains scénarios / juridictions.

La vérité est que ces deux approches sont fausses. La suppression est incorrecte. Si vous posez réellement cette question, cela signifie que vous modélisez l'état actuel au lieu des transactions. C'est une mauvaise, mauvaise pratique dans le domaine des bases de données.

Udi Dahan a écrit à ce sujet dans Don't Delete - Just Don't . Il y a toujours une sorte de tâche, opération, activité , ou (mon terme préféré) événement qui représente en fait la « suppression ». C'est OK si vous souhaitez par la suite dénormaliser dans une table "état actuel" pour les performances, mais faites-le après avoir cloué le modèle transactionnel, pas avant.

Dans ce cas, vous avez des "utilisateurs". Les utilisateurs sont essentiellement des clients. Les clients ont une relation commerciale avec vous. Cette relation ne disparaît pas simplement dans l'air, car ils ont annulé leur compte. Ce qui se passe vraiment, c'est:

  • Le client crée un compte
  • Le client annule le compte
  • Le client renouvelle son compte
  • Le client annule le compte
  • ...

Dans tous les cas, c'est le même client , et éventuellement le même compte (ie chaque renouvellement de compte est un nouvel accord de service). Alors pourquoi supprimez-vous des lignes? C'est très facile à modéliser:

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

C'est ça. C'est tout ce qu'on peut en dire. Vous n'avez jamais besoin de supprimer quoi que ce soit. Ce qui précède est une conception assez courante qui permet un bon degré de flexibilité mais vous pouvez le simplifier un peu; vous pouvez décider que vous n'avez pas besoin du niveau "Accord" et que "Compte" doit simplement aller dans un tableau "AccountStatus".

Si un besoin fréquent dans votre application est d'obtenir une liste des accords / comptes actifs , il s'agit d'une requête (légèrement) délicate, mais c'est à cela que servent les vues:

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
)

Et tu as fini. Maintenant, vous avez quelque chose avec tous les avantages des suppressions en douceur, mais aucun des inconvénients:

  • Les enregistrements orphelins ne posent aucun problème car tous les enregistrements sont visibles à tout moment; vous choisissez simplement d'une vue différente chaque fois que nécessaire.
  • "Supprimer" est généralement une opération incroyablement bon marché - il suffit d'insérer une ligne dans une table d'événements.
  • Il n'y a jamais aucune chance de perdre une histoire, jamais , peu importe à quel point vous vous trompez.
  • Vous pouvez toujours supprimer définitivement un compte si vous en avez besoin (par exemple pour des raisons de confidentialité), et être à l'aise avec la certitude que la suppression se fera proprement et n'interférera avec aucune autre partie de l'application / base de données.

Le seul problème qui reste à résoudre est celui des performances. Dans de nombreux cas, il s'avère que ce n'est pas un problème en raison de l'index clusterisé AgreementStatus (AgreementId, EffectiveDate)- il y a très peu d'E / S cherchant à s'y produire . Mais s'il s'agit d'un problème, il existe des moyens de le résoudre, en utilisant des déclencheurs, des vues indexées / matérialisées, des événements au niveau de l'application, etc.

Ne vous inquiétez pas trop tôt des performances - il est plus important de bien concevoir, et dans ce cas, «bien» signifie utiliser la base de données comme une base de données est censée être utilisée, en tant que système transactionnel .

Aaronaught
la source
1

Je travaille actuellement avec un système où chaque table a un indicateur Supprimé pour la suppression logicielle. C'est le fléau de toute existence. Il brise totalement l'intégrité relationnelle lorsqu'un utilisateur peut "supprimer" un enregistrement d'une table, mais les enregistrements enfants qui FK vers cette table ne sont pas supprimés en cascade. Fait vraiment pour les données de poubelle après le temps.

Je recommande donc des tables d'historique distinctes.

Jesse C. Slicer
la source
Sûrement sans changements d'histoire en cascade, vous avez exactement le même problème?
glenatron
Pas dans vos tables d'enregistrement actives, non.
Jesse C. Slicer
Alors, qu'arrive-t-il aux enregistrements enfants qui quittent la table utilisateur après que l'utilisateur a été consigné dans la table d'historique?
glénatron
Votre déclencheur (ou logique métier) consignerait également les enregistrements enfants dans leurs tables d'historique respectives. Le fait est que vous ne pouvez pas supprimer physiquement l'enregistrement parent (pour passer à l'historique) sans que la base de données vous indique que vous avez rompu le RI. Vous êtes donc obligé de le concevoir. L'indicateur supprimé ne force pas les suppressions progressives en cascade.
Jesse C. Slicer
3
Cela dépend vraiment de ce que signifie votre suppression logicielle. S'il s'agit simplement d'un moyen de les désactiver, il n'est pas nécessaire d'ajuster les enregistrements liés à un compte désactivé. Il me semble que ce ne sont que des données. Et oui, je dois aussi y faire face dans un système que je n'ai pas conçu. Cela ne signifie pas que vous devez l'aimer.
JeffO
1

Briser la table en deux serait la chose la plus pâle imaginable.

Voici les deux étapes très simples que je recommanderais:

  1. Renommez la table «utilisateurs» en «utilisateurs».
  2. Créez une vue appelée «utilisateurs» en tant que «sélectionnez * parmi tous les utilisateurs où supprimé = faux».

PS Désolé pour le retard de plusieurs mois dans la réponse!

Mike Nakis
la source
0

Si vous aviez récupéré des comptes supprimés lorsque quelqu'un revient avec la même adresse e-mail, j'aurais préféré garder tous les utilisateurs dans le même tableau. Cela rendrait le processus de récupération de compte trivial.

Cependant, lorsque vous créez de nouveaux comptes, il serait probablement plus simple de déplacer les comptes supprimés vers une table distincte. Le système en direct n'a pas besoin de ces informations, ne les exposez donc pas. Comme vous le dites, cela rend les requêtes plus simples et peut-être plus rapides sur des ensembles de données plus volumineux. Un code plus simple est également plus facile à gérer.

ChrisF
la source
0

Vous ne mentionnez pas le SGBD utilisé. Si vous avez Oracle avec la licence appropriée, vous pouvez envisager de partitionner la table des utilisateurs en deux partitions: les utilisateurs actifs et supprimés.

mczajk
la source
Ensuite, vous devez déplacer des lignes d'une partition à une autre lors de la suppression d'utilisateurs, ce qui n'est certainement pas la façon dont les partitions sont destinées à être utilisées.
Péter Török
@ Péter: Hein? Vous pouvez partitionner selon les critères de votre choix, y compris l'indicateur supprimé.
Aaronaught
@Aaronaught, OK, je l'ai mal formulé. Le SGBD peut faire le travail pour vous, mais c'est encore du travail supplémentaire (car la ligne doit être physiquement déplacée d'un emplacement à un autre, éventuellement vers un fichier différent), et cela peut détériorer la distribution physique des données.
Péter Török