Nous avons une exigence dans le projet de stocker toutes les révisions (Historique des modifications) pour les entités dans la base de données. Actuellement, nous avons 2 propositions conçues pour cela:
par exemple pour l'entité "Employé"
Conception 1:
-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
-- Holds the Employee Revisions in Xml. The RevisionXML will contain
-- all data of that particular EmployeeId
"EmployeeHistories (EmployeeId, DateModified, RevisionXML)"
Conception 2:
-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
-- In this approach we have basically duplicated all the fields on Employees
-- in the EmployeeHistories and storing the revision data.
"EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName,
LastName, DepartmentId, .., ..)"
Y a-t-il une autre façon de faire cela?
Le problème avec le "Design 1" est que nous devons analyser XML à chaque fois que vous avez besoin d'accéder à des données. Cela ralentira le processus et ajoutera également des limitations telles que nous ne pouvons pas ajouter de jointures sur les champs de données de révision.
Et le problème avec le "Design 2" est que nous devons dupliquer chaque champ sur toutes les entités (nous avons environ 70 à 80 entités pour lesquelles nous voulons maintenir des révisions).
la source
Réponses:
la source
SELECT * FROM EmployeeHistory WHERE LastName = 'Doe'
résultat dans une analyse complète de la table . Ce n'est pas la meilleure idée pour mettre à l'échelle une application.Je pense que la question clé à poser ici est «Qui / Qu'est-ce qui va utiliser l'histoire»?
S'il s'agit principalement de rapports / d'historique lisible par l'homme, nous avons mis en œuvre ce schéma dans le passé ...
Créez une table appelée 'AuditTrail' ou quelque chose qui a les champs suivants ...
Vous pouvez ensuite ajouter une colonne «LastUpdatedByUserID» à toutes vos tables qui doit être définie à chaque fois que vous effectuez une mise à jour / insertion sur la table.
Vous pouvez ensuite ajouter un déclencheur à chaque table pour intercepter toute insertion / mise à jour qui se produit et créer une entrée dans cette table pour chaque champ modifié. Étant donné que la table est également fournie avec le 'LastUpdateByUserID' pour chaque mise à jour / insertion, vous pouvez accéder à cette valeur dans le déclencheur et l'utiliser lors de l'ajout à la table d'audit.
Nous utilisons le champ RecordID pour stocker la valeur du champ clé de la table en cours de mise à jour. S'il s'agit d'une clé combinée, nous faisons simplement une concaténation de chaînes avec un '~' entre les champs.
Je suis sûr que ce système peut avoir des inconvénients - pour les bases de données fortement mises à jour, les performances peuvent être affectées, mais pour mon application Web, nous obtenons beaucoup plus de lectures que d'écritures et il semble fonctionner plutôt bien. Nous avons même écrit un petit utilitaire VB.NET pour écrire automatiquement les déclencheurs en fonction des définitions de table.
Juste une pensée!
la source
sysname
peut être un type de données plus approprié pour les noms de table et de colonne.L' article History Tables du blog Database Programmer pourrait être utile - couvre certains des points soulevés ici et discute du stockage des deltas.
Éditer
Dans l' essai History Tables , l'auteur ( Kenneth Downs ) recommande de conserver un tableau historique d'au moins sept colonnes:
Les colonnes qui ne changent jamais ou dont l'historique n'est pas requis ne doivent pas être suivies dans la table d'historique pour éviter les ballonnements. Le stockage du delta pour les valeurs numériques peut faciliter les requêtes ultérieures, même s'il peut être dérivé des anciennes et des nouvelles valeurs.
La table d'historique doit être sécurisée, les utilisateurs non-système ne pouvant pas insérer, mettre à jour ou supprimer des lignes. Seule une purge périodique doit être prise en charge pour réduire la taille globale (et si cela est autorisé par le cas d'utilisation).
la source
Nous avons mis en place une solution très similaire à la solution suggérée par Chris Roberts, et cela fonctionne plutôt bien pour nous.
La seule différence est que nous ne stockons que la nouvelle valeur. L'ancienne valeur est après tout stockée dans la ligne d'historique précédente
Disons que vous avez une table avec 20 colonnes. De cette façon, vous n'avez qu'à stocker la colonne exacte qui a changé au lieu d'avoir à stocker la ligne entière.
la source
Évitez la conception 1; ce n'est pas très pratique une fois que vous aurez besoin, par exemple, de revenir aux anciennes versions des enregistrements - automatiquement ou "manuellement" à l'aide de la console des administrateurs.
Je ne vois pas vraiment les inconvénients de Design 2. Je pense que le second, la table History devrait contenir toutes les colonnes présentes dans la première, la table Records. Par exemple, dans mysql, vous pouvez facilement créer une table avec la même structure qu'une autre table (
create table X like Y
). Et, lorsque vous êtes sur le point de modifier la structure de la table Records dans votre base de données en direct, vous devezalter table
quand même utiliser des commandes - et il n'y a pas de gros effort à exécuter ces commandes également pour votre table Historique.Remarques
RevisionId
colonne ajoutée ;ModifiedBy
- l'utilisateur qui a créé une révision particulière. Vous pouvez également avoir un champDeletedBy
pour suivre qui a supprimé une révision particulière.DateModified
devrait signifier - soit cela signifie où cette révision particulière a été créée, ou cela signifiera quand cette révision particulière a été remplacée par une autre. La première nécessite que le champ soit dans la table Records, et semble être plus intuitive à première vue; la deuxième solution semble cependant plus pratique pour les enregistrements supprimés (date à laquelle cette révision particulière a été supprimée). Si vous optez pour la première solution, vous aurez probablement besoin d'un deuxième champDateDeleted
(seulement si vous en avez besoin bien sûr). Cela dépend de vous et de ce que vous voulez réellement enregistrer.Les opérations dans Design 2 sont très simples:
ModifierSi vous optez pour Design 2, toutes les commandes SQL nécessaires pour le faire seront très très simples, ainsi que la maintenance! Peut-être que ce sera beaucoup plus facile si vous utilisez les colonnes auxiliaires (
RevisionId
,DateModified
) également dans la table Records - pour garder les deux tables exactement dans la même structure (sauf pour les clés uniques)! Cela permettra des commandes SQL simples, qui seront tolérantes à tout changement de structure de données:N'oubliez pas d'utiliser les transactions!
En ce qui concerne la mise à l'échelle , cette solution est très efficace, car vous ne transformez aucune donnée à partir de XML dans les deux sens, il suffit de copier des lignes entières de la table - des requêtes très simples, à l'aide d'indices - très efficace!
la source
Si vous devez stocker l'historique, créez une table fantôme avec le même schéma que la table que vous suivez et une colonne «Date de révision» et «Type de révision» (par exemple, «supprimer», «mettre à jour»). Ecrivez (ou générez - voir ci-dessous) un ensemble de déclencheurs pour remplir la table d'audit.
Il est assez simple de créer un outil qui lira le dictionnaire de données système pour une table et générera un script qui crée la table d'ombre et un ensemble de déclencheurs pour la remplir.
N'essayez pas d'utiliser XML pour cela, le stockage XML est beaucoup moins efficace que le stockage de table de base de données natif utilisé par ce type de déclencheur.
la source
Ramesh, j'ai été impliqué dans le développement de système basé sur la première approche.
Il s'est avéré que le stockage des révisions sous forme de XML conduit à une énorme croissance de la base de données et ralentit considérablement les choses.
Mon approche serait d'avoir une table par entité:
où IsActive est un signe de la dernière version
Si vous souhaitez associer des informations supplémentaires à des révisions, vous pouvez créer une table séparée contenant ces informations et la lier à des tables d'entités à l'aide de la relation PK \ FK.
De cette façon, vous pouvez stocker toutes les versions des employés dans une table. Avantages de cette approche:
Notez que vous devez autoriser la clé primaire à ne pas être unique.
la source
La façon dont j'ai vu cela fait dans le passé est d'avoir
Vous ne «mettez jamais à jour» sur cette table (sauf pour changer la validité de isCurrent), insérez simplement de nouvelles lignes. Pour tout EmployeeId donné, une seule ligne peut avoir isCurrent == 1.
La complexité de la maintenance peut être masquée par des vues et des déclencheurs "au lieu de" (dans oracle, je présume des choses similaires dans d'autres SGBDR), vous pouvez même accéder à des vues matérialisées si les tables sont trop grandes et ne peuvent pas être gérées par des index) .
Cette méthode est correcte, mais vous pouvez vous retrouver avec des requêtes complexes.
Personnellement, j'aime beaucoup votre façon de faire Design 2, c'est ainsi que je l'ai fait dans le passé également. C'est simple à comprendre, simple à mettre en œuvre et simple à entretenir.
Cela crée également très peu de frais généraux pour la base de données et l'application, en particulier lors de l'exécution de requêtes de lecture, ce que vous ferez probablement 99% du temps.
Il serait également assez facile d'automatiser la création des tables d'historique et des déclencheurs à maintenir (en supposant que cela se fasse via des déclencheurs).
la source
Les révisions des données sont un aspect du concept de « temps de validité » d'une base de données temporelle. De nombreuses recherches ont été menées à ce sujet, et de nombreux modèles et lignes directrices ont émergé. J'ai écrit une longue réponse avec un tas de références à cette question pour les personnes intéressées.
la source
Je vais partager avec vous ma conception et elle est différente de vos deux conceptions en ce qu'elle nécessite une table pour chaque type d'entité. J'ai trouvé que la meilleure façon de décrire toute conception de base de données est via ERD, voici la mienne:
Dans cet exemple, nous avons une entité nommée employé . La table utilisateur contient les enregistrements de vos utilisateurs et entity et entity_revision sont deux tables qui contiennent l'historique des révisions pour tous les types d'entités que vous aurez dans votre système. Voici comment fonctionne cette conception:
Les deux champs de entity_id et revision_id
Chaque entité de votre système aura son propre identifiant d'entité. Votre entité peut subir des révisions, mais son entity_id restera le même. Vous devez conserver cet identifiant d'entité dans la table de vos employés (en tant que clé étrangère). Vous devez également stocker le type de votre entité dans la table des entités (par exemple «employé»). Maintenant, comme pour le revision_id, comme son nom l'indique, il garde une trace de vos révisions d'entité. Le meilleur moyen que j'ai trouvé pour cela est d'utiliser le employee_id comme votre revision_id. Cela signifie que vous aurez des identifiants de révision en double pour différents types d'entités, mais ce n'est pas un plaisir pour moi (je ne suis pas sûr de votre cas). La seule remarque importante à faire est que la combinaison de entity_id et de revision_id doit être unique.
Il existe également un champ d' état dans la table entity_revision qui indique l'état de la révision. Il peut avoir l' un des trois états:
latest
,obsolete
oudeleted
(ne pas se fier à la date de révisions vous aide beaucoup pour booster vos requêtes).Une dernière remarque sur revision_id, je n'ai pas créé de clé étrangère connectant employee_id à revision_id car nous ne voulons pas modifier la table entity_revision pour chaque type d'entité que nous pourrions ajouter à l'avenir.
INSERTION
Pour chaque employé que vous souhaitez insérer dans la base de données, vous ajouterez également un enregistrement à entity et entity_revision . Ces deux derniers enregistrements vous aideront à savoir par qui et quand un enregistrement a été inséré dans la base de données.
METTRE À JOUR
Chaque mise à jour d'un enregistrement d'employé existant sera implémentée sous forme de deux insertions, une dans la table des employés et une dans entity_revision. Le second vous aidera à savoir par qui et quand le dossier a été mis à jour.
EFFACEMENT
Pour supprimer un employé, un enregistrement est inséré dans entity_revision indiquant la suppression et terminée.
Comme vous pouvez le voir dans cette conception, aucune donnée n'est jamais modifiée ou supprimée de la base de données et, plus important encore, chaque type d'entité ne nécessite qu'une seule table. Personnellement, je trouve cette conception très flexible et facile à utiliser. Mais je ne suis pas sûr de vous car vos besoins peuvent être différents.
[METTRE À JOUR]
Ayant pris en charge les partitions dans les nouvelles versions de MySQL, je pense que ma conception est également dotée de l'une des meilleures performances. On peut partitionner la
entity
table en utilisant letype
champ tout en partitionnant enentity_revision
utilisant sonstate
champ. Cela stimuleraSELECT
de loin les requêtes tout en gardant la conception simple et propre.la source
Si effectivement une piste d'audit est tout ce dont vous avez besoin, je pencherais vers la solution de table d'audit (complète avec des copies dénormalisées de la colonne importante sur d'autres tables, par exemple
UserName
). Gardez à l'esprit, cependant, que l'expérience amère indique qu'une seule table d'audit sera un énorme goulot d'étranglement sur la route; cela vaut probablement la peine de créer des tables d'audit individuelles pour toutes vos tables auditées.Si vous avez besoin de suivre les versions historiques (et / ou futures) réelles, la solution standard consiste à suivre la même entité avec plusieurs lignes en utilisant une combinaison de valeurs de début, de fin et de durée. Vous pouvez utiliser une vue pour faciliter l'accès aux valeurs actuelles. Si c'est l'approche que vous adoptez, vous pouvez rencontrer des problèmes si vos données versionnées font référence à des données mutables mais non versionnées.
la source
Si vous souhaitez effectuer le premier, vous pouvez également utiliser XML pour la table Employés. La plupart des bases de données les plus récentes vous permettent d'interroger des champs XML, ce n'est donc pas toujours un problème. Et il peut être plus simple d'avoir un moyen d'accéder aux données des employés, qu'il s'agisse de la dernière version ou d'une version antérieure.
J'essaierais cependant la deuxième approche. Vous pouvez simplifier cela en n'ayant qu'une seule table Employés avec un champ DateModified. EmployeeId + DateModified serait la clé primaire et vous pouvez stocker une nouvelle révision en ajoutant simplement une ligne. De cette façon, l'archivage des anciennes versions et la restauration des versions à partir des archives sont également plus faciles.
Une autre façon de faire cela pourrait être le modèle de datavault de Dan Linstedt. J'ai fait un projet pour le bureau de statistique néerlandais qui a utilisé ce modèle et cela fonctionne assez bien. Mais je ne pense pas que ce soit directement utile pour une utilisation quotidienne des bases de données. Vous aurez peut-être des idées en lisant ses articles.
la source
Que diriez-vous:
Vous créez la clé primaire (EmployeeId, DateModified), et pour obtenir les enregistrements "actuels", il vous suffit de sélectionner MAX (DateModified) pour chaque employeeid. Le stockage d'un IsCurrent est une très mauvaise idée, car tout d'abord, il peut être calculé, et deuxièmement, il est beaucoup trop facile pour les données de se désynchroniser.
Vous pouvez également créer une vue qui répertorie uniquement les derniers enregistrements et l'utiliser principalement lorsque vous travaillez dans votre application. La bonne chose à propos de cette approche est que vous n'avez pas de doublons de données et que vous n'avez pas à collecter des données à partir de deux endroits différents (actuels dans Employees et archivés dans EmployeesHistory) pour obtenir tout l'historique ou la restauration, etc.) .
la source
Si vous souhaitez vous fier aux données d'historique (pour des raisons de rapport), vous devez utiliser une structure comme celle-ci:
Ou solution globale pour l'application:
Vous pouvez également enregistrer vos révisions au format XML, vous n'avez alors qu'un seul enregistrement pour une révision. Ce sera ressemble à:
la source
Nous avons eu des exigences similaires, et ce que nous avons constaté, c'est que souvent, l'utilisateur veut simplement voir ce qui a été changé, pas nécessairement annuler les changements.
Je ne sais pas quel est votre cas d'utilisation, mais nous avons créé une table d'audit qui est automatiquement mise à jour avec les modifications apportées à une entité commerciale, y compris le nom convivial de toutes les références et énumérations de clés étrangères.
Chaque fois que l'utilisateur enregistre ses modifications, nous rechargeons l'ancien objet, exécutons une comparaison, enregistrons les modifications et enregistrons l'entité (tout est effectué dans une seule transaction de base de données en cas de problème).
Cela semble très bien fonctionner pour nos utilisateurs et nous évite le casse-tête d'avoir une table d'audit complètement séparée avec les mêmes champs que notre entité commerciale.
la source
Il semble que vous souhaitiez suivre les modifications d’entités spécifiques au fil du temps, par exemple ID 3, «bob», «123 main street», puis un autre ID 3, «bob» «234 elm st», etc. pour sortir un historique des révisions montrant chaque adresse à laquelle "bob" a été.
La meilleure façon de faire est d'avoir un champ "est en cours" sur chaque enregistrement, et (probablement) un horodatage ou FK à une table de date / heure.
Les insertions doivent ensuite définir le "est en cours" et également annuler le "est en cours" sur l'enregistrement précédent "est en cours". Les requêtes doivent spécifier le "est en cours", sauf si vous voulez tout l'historique.
Il y a d'autres ajustements à cela s'il s'agit d'un très grand tableau ou si un grand nombre de révisions est attendu, mais c'est une approche assez standard.
la source