Im Projekt müssen alle Revisionen (Änderungsverlauf) für die Entitäten in der Datenbank gespeichert werden. Derzeit haben wir 2 Vorschläge dafür:
zB für "Mitarbeiter"
Design 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)"
Design 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, .., ..)"
Gibt es eine andere Möglichkeit, dies zu tun?
Das Problem mit dem "Design 1" ist, dass wir XML jedes Mal analysieren müssen, wenn Sie auf Daten zugreifen müssen. Dies verlangsamt den Prozess und fügt einige Einschränkungen hinzu, da wir keine Verknüpfungen zu den Revisionsdatenfeldern hinzufügen können.
Und das Problem mit dem "Design 2" ist, dass wir jedes Feld auf allen Entitäten duplizieren müssen (wir haben ungefähr 70-80 Entitäten, für die wir Revisionen beibehalten möchten).
sql
database
database-design
versioning
Ramesh Soni
quelle
quelle
Antworten:
quelle
SELECT * FROM EmployeeHistory WHERE LastName = 'Doe'
Ergebnis in einem vollständigen Tabellenscan aus . Nicht die beste Idee, eine Anwendung zu skalieren.Ich denke, die Schlüsselfrage, die hier gestellt werden muss, ist: Wer / Was wird die Geschichte verwenden?
Wenn es hauptsächlich um Berichterstattung / lesbare Historie geht, haben wir dieses Schema in der Vergangenheit implementiert ...
Erstellen Sie eine Tabelle mit dem Namen 'AuditTrail' oder eine Tabelle mit den folgenden Feldern ...
Sie können dann allen Ihren Tabellen eine Spalte 'LastUpdatedByUserID' hinzufügen, die jedes Mal festgelegt werden sollte, wenn Sie die Tabelle aktualisieren / einfügen.
Sie können dann jeder Tabelle einen Trigger hinzufügen, um alle Einfügungen / Aktualisierungen abzufangen und für jedes geänderte Feld einen Eintrag in dieser Tabelle zu erstellen. Da die Tabelle auch mit der 'LastUpdateByUserID' für jede Aktualisierung / Einfügung geliefert wird, können Sie auf diesen Wert im Trigger zugreifen und ihn beim Hinzufügen zur Überwachungstabelle verwenden.
Wir verwenden das RecordID-Feld, um den Wert des Schlüsselfelds der zu aktualisierenden Tabelle zu speichern. Wenn es sich um einen kombinierten Schlüssel handelt, führen wir einfach eine Zeichenfolgenverkettung mit einem '~' zwischen den Feldern durch.
Ich bin mir sicher, dass dieses System Nachteile haben kann - bei stark aktualisierten Datenbanken kann die Leistung beeinträchtigt werden, aber bei meiner Web-App erhalten wir viel mehr Lese- als Schreibvorgänge und es scheint ziemlich gut zu funktionieren. Wir haben sogar ein kleines VB.NET-Dienstprogramm geschrieben, um die Trigger basierend auf den Tabellendefinitionen automatisch zu schreiben.
Nur ein Gedanke!
quelle
sysname
möglicherweise ist ein geeigneterer Datentyp für die Tabellen- und Spaltennamen.Die Geschichte Tabellen Artikel in der Datenbank - Programmierer Blog nützlich sein könnten - deckt einige der Punkte hier angehoben und diskutiert die Speicherung von Deltas.
Bearbeiten
Im Aufsatz " Geschichtstabellen" empfiehlt der Autor ( Kenneth Downs ), eine Geschichtstabelle mit mindestens sieben Spalten zu führen:
Spalten, die sich nie ändern oder deren Verlauf nicht erforderlich ist, sollten nicht in der Verlaufstabelle nachverfolgt werden, um ein Aufblähen zu vermeiden. Das Speichern des Deltas für numerische Werte kann nachfolgende Abfragen erleichtern, obwohl es aus den alten und neuen Werten abgeleitet werden kann.
Die Verlaufstabelle muss sicher sein, und Nicht-Systembenutzer dürfen keine Zeilen einfügen, aktualisieren oder löschen. Es sollte nur eine regelmäßige Spülung unterstützt werden, um die Gesamtgröße zu verringern (und wenn dies im Anwendungsfall zulässig ist).
quelle
Wir haben eine Lösung implementiert, die der von Chris Roberts vorgeschlagenen sehr ähnlich ist und die für uns ziemlich gut funktioniert.
Der einzige Unterschied besteht darin, dass wir nur den neuen Wert speichern. Der alte Wert wird immerhin in der vorherigen Verlaufszeile gespeichert
Nehmen wir an, Sie haben eine Tabelle mit 20 Spalten. Auf diese Weise müssen Sie nur die genaue Spalte speichern, die sich geändert hat, anstatt die gesamte Zeile zu speichern.
quelle
Vermeiden Sie Design 1; Es ist nicht sehr praktisch, wenn Sie beispielsweise ein Rollback auf alte Versionen der Datensätze durchführen müssen - entweder automatisch oder "manuell" über die Administratorkonsole.
Ich sehe keine wirklichen Nachteile von Design 2. Ich denke, die zweite Tabelle "Verlauf" sollte alle Spalten enthalten, die in der ersten Tabelle "Datensätze" vorhanden sind. Zum Beispiel können Sie in MySQL einfach eine Tabelle mit derselben Struktur wie eine andere Tabelle (
create table X like Y
) erstellen . Und wenn Sie sich jetzt ändern Struktur der Datensätze Tabelle in Ihrer Live - Datenbank sind, haben Sie zu verwendenalter table
Befehle sowieso - und es gibt keine große Mühe , diese Befehle auch für Ihre History - Tabelle in Laufen.Anmerkungen
RevisionId
Spalte.ModifiedBy
- den Benutzer, der eine bestimmte Revision erstellt hat. Möglicherweise möchten Sie auch ein Feld haben, umDeletedBy
zu verfolgen, wer eine bestimmte Revision gelöscht hat.DateModified
bedeuten soll - entweder bedeutet dies, wo diese bestimmte Revision erstellt wurde, oder es bedeutet, wann diese bestimmte Revision durch eine andere ersetzt wurde. Ersteres erfordert, dass sich das Feld in der Tabelle "Datensätze" befindet, und scheint auf den ersten Blick intuitiver zu sein. Die zweite Lösung scheint jedoch für gelöschte Datensätze praktischer zu sein (Datum, an dem diese bestimmte Revision gelöscht wurde). Wenn Sie sich für die erste Lösung entscheiden, benötigen Sie wahrscheinlich ein zweites FeldDateDeleted
(natürlich nur, wenn Sie es benötigen). Kommt auf dich an und was du eigentlich aufnehmen willst.Operationen in Design 2 sind sehr trivial:
ÄndernWenn Sie sich für Design 2 entscheiden, sind alle dafür erforderlichen SQL-Befehle sehr einfach und wartungsfreundlich! Vielleicht ist es viel einfacher, wenn Sie die Hilfsspalten (
RevisionId
,DateModified
) auch in der Records-Tabelle verwenden - um beide Tabellen in genau derselben Struktur zu halten (mit Ausnahme eindeutiger Schlüssel)! Dies ermöglicht einfache SQL-Befehle, die gegenüber Änderungen der Datenstruktur tolerant sind:Vergessen Sie nicht, Transaktionen zu verwenden!
Die Skalierung ist sehr effizient, da Sie keine Daten aus XML hin und her transformieren, sondern nur ganze Tabellenzeilen kopieren - sehr einfache Abfragen, Indizes verwenden - sehr effizient!
quelle
Wenn Sie den Verlauf speichern müssen, erstellen Sie eine Schattentabelle mit demselben Schema wie die Tabelle, die Sie verfolgen, und einer Spalte mit dem Überarbeitungsdatum und dem Überarbeitungstyp (z. B. "Löschen", "Aktualisieren"). Schreiben Sie eine Reihe von Triggern (oder generieren Sie sie - siehe unten), um die Prüftabelle zu füllen.
Es ist ziemlich einfach, ein Tool zu erstellen, das das Systemdatenwörterbuch für eine Tabelle liest und ein Skript generiert, das die Schattentabelle und eine Reihe von Triggern zum Auffüllen erstellt.
Versuchen Sie nicht, XML dafür zu verwenden. Der XML-Speicher ist viel weniger effizient als der native Datenbanktabellenspeicher, den diese Art von Trigger verwendet.
quelle
Ramesh, ich war an der Entwicklung eines Systems beteiligt, das auf dem ersten Ansatz basiert.
Es stellte sich heraus, dass das Speichern von Revisionen als XML zu einem enormen Datenbankwachstum führt und die Dinge erheblich verlangsamt.
Mein Ansatz wäre, eine Tabelle pro Entität zu haben:
Dabei ist IsActive ein Zeichen für die neueste Version
Wenn Sie Revisionen zusätzliche Informationen zuordnen möchten, können Sie eine separate Tabelle mit diesen Informationen erstellen und diese mithilfe der PK \ FK-Beziehung mit Entitätstabellen verknüpfen.
Auf diese Weise können Sie alle Versionen von Mitarbeitern in einer Tabelle speichern. Vorteile dieses Ansatzes:
Beachten Sie, dass Sie zulassen sollten, dass der Primärschlüssel nicht eindeutig ist.
quelle
Die Art und Weise, wie ich das in der Vergangenheit gesehen habe, ist
Sie "aktualisieren" diese Tabelle nie (außer um die Gültigkeit von isCurrent zu ändern), fügen Sie einfach neue Zeilen ein. Für eine bestimmte EmployeeId kann nur 1 Zeile isCurrent == 1 haben.
Die Komplexität der Verwaltung kann durch Ansichten und "anstelle von" Triggern verborgen werden (in Oracle nehme ich ähnliche Dinge an wie in anderen RDBMS). Sie können sogar zu materialisierten Ansichten wechseln, wenn die Tabellen zu groß sind und nicht von Indizes verarbeitet werden können. .
Diese Methode ist in Ordnung, aber Sie können mit einigen komplexen Abfragen enden.
Persönlich mag ich Ihre Design 2-Methode ziemlich gern, so wie ich es auch in der Vergangenheit gemacht habe. Es ist einfach zu verstehen, einfach zu implementieren und einfach zu warten.
Es verursacht auch sehr wenig Overhead für die Datenbank und die Anwendung, insbesondere wenn Leseabfragen ausgeführt werden, was wahrscheinlich in 99% der Fälle der Fall ist.
Es wäre auch recht einfach, die Erstellung der zu verwaltenden Verlaufstabellen und Trigger automatisch durchzuführen (vorausgesetzt, dies würde über Trigger erfolgen).
quelle
Revisionen von Daten sind ein Aspekt des " Gültigkeitszeit " -Konzepts einer zeitlichen Datenbank. Es wurde viel geforscht, und es sind viele Muster und Richtlinien entstanden. Ich habe eine lange Antwort mit einer Reihe von Verweisen auf diese Frage für Interessierte geschrieben.
quelle
Ich werde mein Design mit Ihnen teilen und es unterscheidet sich von Ihren beiden Designs darin, dass es eine Tabelle pro Entitätstyp erfordert. Ich habe festgestellt, dass der beste Weg, ein Datenbankdesign zu beschreiben, die ERD ist. Hier ist meine:
In diesem Beispiel haben wir eine Entität namens Mitarbeiter . Die Benutzertabelle enthält die Datensätze Ihrer Benutzer, und entity und entity_revision sind zwei Tabellen, die den Revisionsverlauf für alle Entitätstypen enthalten, die in Ihrem System vorhanden sind. So funktioniert dieses Design:
Die beiden Felder entity_id und revision_id
Jede Entität in Ihrem System verfügt über eine eigene eindeutige Entitäts-ID. Ihre Entität wird möglicherweise überarbeitet, aber ihre entity_id bleibt gleich. Sie müssen diese Entitäts-ID in Ihrer Mitarbeitertabelle behalten (als Fremdschlüssel). Sie sollten auch den Typ Ihrer Entität in der Entitätstabelle speichern (z. B. 'Mitarbeiter'). Wie der Name schon sagt, verfolgt die revision_id die Entitätsrevisionen. Der beste Weg, den ich dafür gefunden habe, ist, die employee_id als Ihre revision_id zu verwenden. Dies bedeutet, dass Sie doppelte Revisions-IDs für verschiedene Arten von Entitäten haben, aber das ist für mich kein Vergnügen (ich bin mir über Ihren Fall nicht sicher). Der einzige wichtige Hinweis ist, dass die Kombination von entity_id und revision_id eindeutig sein sollte.
Es gibt auch einen Zustand Feld innerhalb entity_revision Tabelle , die den Zustand der Revision angezeigt. Es kann einen der drei Zustände haben :
latest
,obsolete
oderdeleted
( wenn Sie sich nicht auf das Datum der Überarbeitung verlassen, können Sie Ihre Abfragen erheblich verbessern).Ein letzter Hinweis zu revision_id: Ich habe keinen Fremdschlüssel erstellt, der employee_id mit revision_id verbindet, da wir die Tabelle entity_revision nicht für jeden Entitätstyp ändern möchten, den wir möglicherweise in Zukunft hinzufügen.
EINSATZ
Für jeden Mitarbeiter , den Sie in die Datenbank einfügen möchten, fügen Sie der Entität und der Entitätsrevision einen Datensatz hinzu . Mithilfe dieser beiden letzten Datensätze können Sie verfolgen, von wem und wann ein Datensatz in die Datenbank eingefügt wurde.
AKTUALISIEREN
Jede Aktualisierung für einen vorhandenen Mitarbeiterdatensatz wird als zwei Einfügungen implementiert, eine in der Mitarbeitertabelle und eine in entity_revision. Der zweite hilft Ihnen zu wissen, von wem und wann der Datensatz aktualisiert wurde.
STREICHUNG
Zum Löschen eines Mitarbeiters wird in entity_revision ein Datensatz eingefügt, der das Löschen angibt und abgeschlossen ist.
Wie Sie in diesem Entwurf sehen können, werden niemals Daten geändert oder aus der Datenbank entfernt, und was noch wichtiger ist, jeder Entitätstyp erfordert nur eine Tabelle. Persönlich finde ich dieses Design sehr flexibel und einfach zu bearbeiten. Aber ich bin mir nicht sicher, da Ihre Bedürfnisse unterschiedlich sein könnten.
[AKTUALISIEREN]
Nachdem ich Partitionen in den neuen MySQL-Versionen unterstützt habe, glaube ich, dass mein Design auch eine der besten Leistungen bietet. Man kann eine
entity
Tabelle mit einemtype
Feld partitionierenentity_revision
, während man eine Partition mit seinemstate
Feld erstellt. Dies wird dieSELECT
Abfragen bei weitem beschleunigen und gleichzeitig das Design einfach und sauber halten.quelle
Wenn Sie in der Tat nur einen Prüfpfad benötigen, würde ich mich auf die Prüftabellenlösung stützen (einschließlich denormalisierter Kopien der wichtigen Spalte in anderen Tabellen, z
UserName
. B. ). Beachten Sie jedoch, dass diese bittere Erfahrung darauf hindeutet, dass eine einzelne Prüftabelle später einen großen Engpass darstellen wird. Es lohnt sich wahrscheinlich, individuelle Prüftabellen für alle geprüften Tabellen zu erstellen.Wenn Sie die tatsächlichen historischen (und / oder zukünftigen) Versionen verfolgen müssen, besteht die Standardlösung darin, dieselbe Entität mit mehreren Zeilen unter Verwendung einer Kombination aus Start-, End- und Dauerwerten zu verfolgen. Sie können eine Ansicht verwenden, um den Zugriff auf aktuelle Werte zu vereinfachen. Wenn Sie diesen Ansatz wählen, können Probleme auftreten, wenn Ihre versionierten Daten auf veränderbare, aber nicht versionierte Daten verweisen.
quelle
Wenn Sie den ersten ausführen möchten, möchten Sie möglicherweise auch XML für die Employees-Tabelle verwenden. In den meisten neueren Datenbanken können Sie XML-Felder abfragen, sodass dies nicht immer ein Problem darstellt. Und es ist möglicherweise einfacher, auf eine Weise auf Mitarbeiterdaten zuzugreifen, unabhängig davon, ob es sich um die neueste oder eine frühere Version handelt.
Ich würde jedoch den zweiten Ansatz versuchen. Sie können dies vereinfachen, indem Sie nur eine Employees-Tabelle mit einem DateModified-Feld haben. Die EmployeeId + DateModified ist der Primärschlüssel, und Sie können eine neue Revision speichern, indem Sie einfach eine Zeile hinzufügen. Auf diese Weise ist es auch einfacher, ältere Versionen zu archivieren und Versionen aus dem Archiv wiederherzustellen.
Ein anderer Weg, dies zu tun, könnte das Datavault-Modell von Dan Linstedt sein. Ich habe ein Projekt für das niederländische Statistikamt durchgeführt, das dieses Modell verwendet hat, und es funktioniert recht gut. Aber ich denke nicht, dass es direkt für die tägliche Datenbanknutzung nützlich ist. Sie könnten jedoch einige Ideen bekommen, wenn Sie seine Papiere lesen.
quelle
Wie wäre es mit:
Sie erstellen den Primärschlüssel (EmployeeId, DateModified) und wählen zum Abrufen der "aktuellen" Datensätze einfach MAX (DateModified) für jede Mitarbeiter-ID aus. Das Speichern eines IsCurrent ist eine sehr schlechte Idee, da es erstens berechnet werden kann und zweitens viel zu einfach ist, dass Daten nicht mehr synchron sind.
Sie können auch eine Ansicht erstellen, in der nur die neuesten Datensätze aufgelistet sind, und diese meistens während der Arbeit in Ihrer App verwenden. Das Schöne an diesem Ansatz ist, dass Sie keine doppelten Daten haben und keine Daten von zwei verschiedenen Orten sammeln müssen (aktuell in Employees und archiviert in EmployeesHistory), um den gesamten Verlauf oder Rollback usw. abzurufen. .
quelle
Wenn Sie sich (aus Berichtsgründen) auf Verlaufsdaten verlassen möchten, sollten Sie eine Struktur wie die folgende verwenden:
Oder globale Lösung für die Anwendung:
Sie können Ihre Revisionen auch in XML speichern, dann haben Sie nur einen Datensatz für eine Revision. Das wird so aussehen:
quelle
Wir haben ähnliche Anforderungen hatte, und was wir fanden war , dass oft der Benutzer will nur sehen , was sich geändert hat, nicht unbedingt rollen alle Änderungen zurück.
Ich bin nicht sicher, was Ihr Anwendungsfall ist, aber wir haben eine Tabelle erstellt und geprüft, die automatisch mit Änderungen an einer Geschäftseinheit aktualisiert wird, einschließlich des Anzeigenamens aller Fremdschlüsselreferenzen und -aufzählungen.
Immer wenn der Benutzer seine Änderungen speichert, laden wir das alte Objekt neu, führen einen Vergleich durch, zeichnen die Änderungen auf und speichern die Entität (alle werden in einer einzigen Datenbanktransaktion durchgeführt, falls es Probleme gibt).
Dies scheint für unsere Benutzer sehr gut zu funktionieren und erspart uns die Kopfschmerzen, eine vollständig separate Prüftabelle mit denselben Feldern wie unsere Geschäftseinheit zu haben.
quelle
Es hört sich so an, als ob Sie Änderungen an bestimmten Entitäten im Laufe der Zeit verfolgen möchten, z. B. ID 3, "Bob", "123 Main Street", dann eine andere ID 3, "Bob", "234 Elm St" usw., die im Wesentlichen in der Lage sind einen Revisionsverlauf herauszuholen, der zeigt, an welcher Adresse "bob" war.
Der beste Weg, dies zu tun, besteht darin, in jedem Datensatz ein Feld "Ist aktuell" und (wahrscheinlich) einen Zeitstempel oder eine FK für eine Datums- / Zeittabelle zu haben.
Die Einfügungen müssen dann das "Ist aktuell" setzen und auch das "Ist aktuell" im vorherigen "Ist aktuell" -Datensatz deaktivieren. Abfragen müssen "Ist aktuell" angeben, es sei denn, Sie möchten den gesamten Verlauf.
Es gibt weitere Verbesserungen, wenn es sich um eine sehr große Tabelle handelt oder eine große Anzahl von Revisionen erwartet wird, aber dies ist ein ziemlich Standardansatz.
quelle