Gibt es eine MySQL-Option / -Funktion, um den Verlauf von Änderungen an Datensätzen zu verfolgen?

121

Ich wurde gefragt, ob ich die Änderungen an den Datensätzen in einer MySQL-Datenbank verfolgen kann. Wenn also ein Feld geändert wurde, ist das alte gegen das neue verfügbar und das Datum, an dem dies stattgefunden hat. Gibt es eine Funktion oder eine übliche Technik, um dies zu tun?

Wenn ja, dachte ich darüber nach, so etwas zu tun. Erstellen Sie eine Tabelle mit dem Namen changes. Es würde die gleichen Felder wie die Mastertabelle enthalten , jedoch mit dem Präfix alt und neu, jedoch nur für die Felder, die tatsächlich geändert wurden, und ein TIMESTAMPdafür. Es würde mit einem indiziert werden ID. Auf diese Weise kann ein SELECTBericht ausgeführt werden, der den Verlauf jedes Datensatzes anzeigt. Ist das eine gute Methode? Vielen Dank!

Edward
quelle

Antworten:

83

Es ist subtil.

Wenn die Geschäftsanforderung lautet "Ich möchte die Änderungen an den Daten prüfen - wer hat was und wann getan?", Können Sie normalerweise Prüftabellen verwenden (gemäß dem von Keethanjan veröffentlichten Auslösebeispiel). Ich bin kein großer Fan von Triggern, aber es hat den großen Vorteil, dass es relativ einfach zu implementieren ist - Ihr vorhandener Code muss nichts über die Trigger und das Prüfungsmaterial wissen.

Wenn die Geschäftsanforderung lautet "Zeigen Sie mir, wie der Status der Daten zu einem bestimmten Zeitpunkt in der Vergangenheit war", bedeutet dies, dass der Aspekt der zeitlichen Änderung in Ihre Lösung eingegangen ist. Sie können den Status der Datenbank zwar nur anhand von Prüftabellen rekonstruieren, dies ist jedoch schwierig und fehleranfällig, und für jede komplizierte Datenbanklogik wird sie unhandlich. Wenn das Unternehmen beispielsweise wissen möchte, "die Adressen der Briefe zu finden, die wir an Kunden hätten senden sollen, die am ersten Tag des Monats ausstehende, unbezahlte Rechnungen hatten", müssen Sie wahrscheinlich ein halbes Dutzend Audittabellen durchsuchen.

Stattdessen können Sie das Konzept der zeitlichen Änderung in Ihr Schemadesign einbinden (dies ist die zweite Option, die Keethanjan vorschlägt). Dies ist eine Änderung an Ihrer Anwendung, definitiv auf der Ebene der Geschäftslogik und der Persistenz. Sie ist also nicht trivial.

Wenn Sie beispielsweise eine Tabelle wie diese haben:

CUSTOMER
---------
CUSTOMER_ID PK
CUSTOMER_NAME
CUSTOMER_ADDRESS

und Sie wollten den Überblick behalten, Sie würden es wie folgt ändern:

CUSTOMER
------------
CUSTOMER_ID            PK
CUSTOMER_VALID_FROM    PK
CUSTOMER_VALID_UNTIL   PK
CUSTOMER_STATUS
CUSTOMER_USER
CUSTOMER_NAME
CUSTOMER_ADDRESS

Jedes Mal, wenn Sie einen Kundendatensatz ändern möchten, anstatt den Datensatz zu aktualisieren, setzen Sie VALID_UNTIL für den aktuellen Datensatz auf NOW () und fügen einen neuen Datensatz mit einem VALID_FROM (jetzt) ​​und einem Nullwert VALID_UNTIL ein. Sie setzen den Status "CUSTOMER_USER" auf die Login-ID des aktuellen Benutzers (falls Sie diese behalten müssen). Wenn der Kunde gelöscht werden muss, verwenden Sie das Flag CUSTOMER_STATUS, um dies anzuzeigen. Sie dürfen niemals Datensätze aus dieser Tabelle löschen.

Auf diese Weise können Sie immer den Status der Kundentabelle für ein bestimmtes Datum ermitteln - wie lautete die Adresse? Haben sie den Namen geändert? Durch Verbinden mit anderen Tabellen mit ähnlichen Daten von valid_from und valid_until können Sie das gesamte Bild historisch rekonstruieren. Um den aktuellen Status zu ermitteln, suchen Sie nach Datensätzen mit dem Datum VALID_UNTIL null.

Es ist unhandlich (genau genommen benötigen Sie das valid_from nicht, aber es erleichtert die Abfragen ein wenig). Dies erschwert Ihr Design und Ihren Datenbankzugriff. Aber es macht den Wiederaufbau der Welt viel einfacher.

Neville Kuyt
quelle
Aber es würde doppelte Daten für die Felder hinzufügen, die nicht aktualisiert werden? Wie geht das?
Itzmukeshy7
Beim zweiten Ansatz tritt bei der Berichterstellung ein Problem auf, wenn ein Kundendatensatz über einen bestimmten Zeitraum bearbeitet wird. Es ist schwierig zu erkennen, ob ein bestimmter Eintrag demselben Kunden gehört oder sich von einem anderen unterscheidet.
Akshay Joshi
Bester Vorschlag, den ich bisher zu diesem Problem gesehen habe
Worthy7
Oh, und als Antwort auf die Kommentare, wie wäre es, wenn Sie für alles, was sich nicht geändert hat, einfach null speichern? Die neueste Version enthält also alle aktuellen Daten. Wenn der Name jedoch vor 5 Tagen "Bob" war, haben Sie nur eine Zeile, name = bob und sind bis vor 5 Tagen gültig.
Worthy7
2
Die Kombination von customer_id und den Daten ist der Primärschlüssel, sodass sie garantiert eindeutig sind.
Neville Kuyt
185

Hier ist eine einfache Möglichkeit, dies zu tun:

Erstellen Sie zunächst eine Verlaufstabelle für jede Datentabelle, die Sie verfolgen möchten (Beispielabfrage unten). Diese Tabelle enthält einen Eintrag für jede Abfrage zum Einfügen, Aktualisieren und Löschen, die für jede Zeile in der Datentabelle ausgeführt wird.

Die Struktur der Verlaufstabelle entspricht der Datentabelle, die sie verfolgt, mit Ausnahme von drei zusätzlichen Spalten: einer Spalte zum Speichern der aufgetretenen Operation (nennen wir sie "Aktion"), dem Datum und der Uhrzeit der Operation sowie einer Spalte um eine Sequenznummer ('Revision') zu speichern, die pro Operation inkrementiert und nach der Primärschlüsselspalte der Datentabelle gruppiert wird.

Zu diesem Sequenzierungsverhalten wird ein zweispaltiger (zusammengesetzter) Index für die Primärschlüsselspalte und die Revisionsspalte erstellt. Beachten Sie, dass Sie die Sequenzierung nur auf diese Weise durchführen können, wenn die von der Verlaufstabelle verwendete Engine MyISAM ist ( siehe 'MyISAM-Hinweise' auf dieser Seite).

Die Verlaufstabelle ist ziemlich einfach zu erstellen. Ersetzen Sie in der unten stehenden ALTER TABLE-Abfrage (und in den darunter liegenden Trigger-Abfragen) 'primary_key_column' durch den tatsächlichen Namen dieser Spalte in Ihrer Datentabelle.

CREATE TABLE MyDB.data_history LIKE MyDB.data;

ALTER TABLE MyDB.data_history MODIFY COLUMN primary_key_column int(11) NOT NULL, 
   DROP PRIMARY KEY, ENGINE = MyISAM, ADD action VARCHAR(8) DEFAULT 'insert' FIRST, 
   ADD revision INT(6) NOT NULL AUTO_INCREMENT AFTER action,
   ADD dt_datetime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER revision,
   ADD PRIMARY KEY (primary_key_column, revision);

Und dann erstellen Sie die Auslöser:

DROP TRIGGER IF EXISTS MyDB.data__ai;
DROP TRIGGER IF EXISTS MyDB.data__au;
DROP TRIGGER IF EXISTS MyDB.data__bd;

CREATE TRIGGER MyDB.data__ai AFTER INSERT ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'insert', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__au AFTER UPDATE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'update', NULL, NOW(), d.*
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column;

CREATE TRIGGER MyDB.data__bd BEFORE DELETE ON MyDB.data FOR EACH ROW
    INSERT INTO MyDB.data_history SELECT 'delete', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = OLD.primary_key_column;

Und du bist fertig. Jetzt werden alle Einfügungen, Aktualisierungen und Löschungen in 'MyDb.data' in 'MyDb.data_history' aufgezeichnet, sodass Sie eine Verlaufstabelle wie diese erhalten (abzüglich der erfundenen Spalte 'data_columns').

ID    revision   action    data columns..
1     1         'insert'   ....          initial entry for row where ID = 1
1     2         'update'   ....          changes made to row where ID = 1
2     1         'insert'   ....          initial entry, ID = 2
3     1         'insert'   ....          initial entry, ID = 3 
1     3         'update'   ....          more changes made to row where ID = 1
3     2         'update'   ....          changes made to row where ID = 3
2     2         'delete'   ....          deletion of row where ID = 2 

Um die Änderungen für eine bestimmte Spalte oder Spalten von Aktualisierung zu Aktualisierung anzuzeigen, müssen Sie die Verlaufstabelle in den Primärschlüssel- und Sequenzspalten mit sich selbst verknüpfen. Sie können zu diesem Zweck eine Ansicht erstellen, zum Beispiel:

CREATE VIEW data_history_changes AS 
   SELECT t2.dt_datetime, t2.action, t1.primary_key_column as 'row id', 
   IF(t1.a_column = t2.a_column, t1.a_column, CONCAT(t1.a_column, " to ", t2.a_column)) as a_column
   FROM MyDB.data_history as t1 INNER join MyDB.data_history as t2 on t1.primary_key_column = t2.primary_key_column 
   WHERE (t1.revision = 1 AND t2.revision = 1) OR t2.revision = t1.revision+1
   ORDER BY t1.primary_key_column ASC, t2.revision ASC

Edit: Oh wow, Leute mögen meine Geschichtstabelle vor 6 Jahren: P.

Ich würde annehmen, dass meine Implementierung immer noch summt und größer und unhandlicher wird. Ich habe Ansichten und eine hübsche Benutzeroberfläche geschrieben, um den Verlauf in dieser Datenbank zu betrachten, aber ich glaube nicht, dass sie jemals viel verwendet wurde. So geht es.

Um einige Kommentare in keiner bestimmten Reihenfolge anzusprechen:

  • Ich habe meine eigene Implementierung in PHP durchgeführt, die etwas komplizierter war, und einige der in Kommentaren beschriebenen Probleme vermieden (Indizes wurden signifikant übertragen. Wenn Sie eindeutige Indizes in die Verlaufstabelle übertragen, werden die Dinge kaputt gehen. Es gibt Lösungen für dies in den Kommentaren). Diesem Beitrag auf den Brief zu folgen, könnte ein Abenteuer sein, abhängig davon, wie etabliert Ihre Datenbank ist.

  • Wenn die Beziehung zwischen dem Primärschlüssel und der Revisionsspalte nicht korrekt zu sein scheint, bedeutet dies normalerweise, dass der zusammengesetzte Schlüssel irgendwie verzerrt ist. In einigen seltenen Fällen hatte ich dies und war für die Sache ratlos.

  • Ich fand diese Lösung ziemlich performant und verwendete dabei Trigger. Außerdem ist MyISAM bei Einfügungen schnell, was alles ist, was die Trigger tun. Sie können dies durch intelligente Indizierung (oder fehlende ...) weiter verbessern. Das Einfügen einer einzelnen Zeile in eine MyISAM-Tabelle mit einem Primärschlüssel sollte kein Vorgang sein, den Sie wirklich optimieren müssen, es sei denn, Sie haben an anderer Stelle erhebliche Probleme. Während der gesamten Zeit, in der ich die MySQL-Datenbank ausführte, in der sich diese Implementierung der Verlaufstabelle befand, war dies nie die Ursache für eines der (vielen) Leistungsprobleme, die auftraten.

  • Wenn Sie wiederholt Einfügungen erhalten, überprüfen Sie Ihre Softwareschicht auf Abfragen vom Typ INSERT IGNORE. Hrmm, ich kann mich jetzt nicht erinnern, aber ich denke, es gibt Probleme mit diesem Schema und Transaktionen, die letztendlich fehlschlagen, nachdem mehrere DML-Aktionen ausgeführt wurden. Zumindest etwas, das man beachten sollte.

  • Es ist wichtig, dass die Felder in der Verlaufstabelle und der Datentabelle übereinstimmen. Oder vielmehr, dass Ihre Datentabelle nicht MEHR Spalten als die Verlaufstabelle enthält. Andernfalls schlagen Einfügen / Aktualisieren / Löschen von Abfragen in der Datentabelle fehl, wenn die Einfügungen in die Verlaufstabellen Spalten in die Abfrage einfügen, die nicht vorhanden sind (aufgrund von d. * In den Triggerabfragen), und der Trigger fehlschlägt. Es wäre fantastisch, wenn MySQL so etwas wie Schema-Trigger hätte, bei denen Sie die Verlaufstabelle ändern könnten, wenn Spalten zur Datentabelle hinzugefügt würden. Hat MySQL das jetzt? Ich reagiere heutzutage: P.

vorübergehender Verschluss
quelle
3
Ich mag diese Lösung wirklich. Wenn Ihre Haupttabelle jedoch keinen Primärschlüssel hat oder Sie nicht wissen, was der Primärschlüssel ist, ist dies etwas schwierig.
Benjamin Eckstein
1
Ich bin kürzlich auf ein Problem mit dieser Lösung für ein Projekt gestoßen, weil alle Indizes aus der Originaltabelle in die Verlaufstabelle kopiert wurden (aufgrund der Funktionsweise von CREATE TABLE ... LIKE ....). Das Vorhandensein eindeutiger Indizes in der Verlaufstabelle kann dazu führen, dass die INSERT-Abfrage im AFTER UPDATE-Trigger gesperrt wird. Sie müssen daher entfernt werden. In dem PHP-Skript, das ich habe, frage ich nach eindeutigen Indizes für neu erstellte Verlaufstabellen (mit "SHOW INDEX FROM data_table WHERE Key_name! = 'PRIMARY' und Non_unique = 0") und entferne sie dann.
vorübergehende Schließung
3
Hier werden jedes Mal wiederholte Daten in die Sicherungstabelle eingefügt. Wenn wir 10 Felder in einer Tabelle haben und 2 aktualisiert haben, fügen wir wiederholte Daten für die restlichen 8 Felder hinzu. Wie kann man daraus überwinden?
Itzmukeshy7
6
Sie können ein versehentliches Übertragen der verschiedenen Indizes vermeiden, indem Sie die Anweisung create table inCREATE TABLE MyDB.data_history as select * from MyDB.data limit 0;
Eric Hayes
4
@transientclosure Wie würden Sie vorschlagen, andere Felder in den Verlauf aufzunehmen, die nicht Teil der ursprünglichen Abfrage waren? Ich möchte beispielsweise verfolgen, wer diese Änderungen vornimmt. Zum Einfügen hat es bereits ein ownerFeld, und zum Aktualisieren könnte ich ein updatedbyFeld hinzufügen , aber zum Löschen bin ich mir nicht sicher, wie ich das über die Trigger tun könnte. Das Aktualisieren der data_historyZeile mit der Benutzer-ID in fühlt sich schmutzig an: P
Horse
16

Sie können Trigger erstellen, um dies zu lösen. Hier ist ein Tutorial dazu (archivierter Link).

Das Festlegen von Einschränkungen und Regeln in der Datenbank ist besser als das Schreiben von speziellem Code, um dieselbe Aufgabe zu erledigen, da dadurch verhindert wird, dass ein anderer Entwickler eine andere Abfrage schreibt, die den gesamten speziellen Code umgeht und Ihre Datenbank möglicherweise schlecht datenintegriert.

Lange Zeit habe ich Informationen mit einem Skript in eine andere Tabelle kopiert, da MySQL zu diesem Zeitpunkt keine Trigger unterstützte. Ich habe jetzt festgestellt, dass dieser Auslöser effektiver ist, um alles im Auge zu behalten.

Dieser Trigger kopiert einen alten Wert in eine Verlaufstabelle, wenn er geändert wird, wenn jemand eine Zeile bearbeitet. Editor IDund last modwerden jedes Mal in der Originaltabelle gespeichert, wenn jemand diese Zeile bearbeitet; Die Zeit entspricht dem Zeitpunkt, zu dem die aktuelle Form geändert wurde.

DROP TRIGGER IF EXISTS history_trigger $$

CREATE TRIGGER history_trigger
BEFORE UPDATE ON clients
    FOR EACH ROW
    BEGIN
        IF OLD.first_name != NEW.first_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'first_name',
                        NEW.first_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

        IF OLD.last_name != NEW.last_name
        THEN
                INSERT INTO history_clients
                    (
                        client_id    ,
                        col          ,
                        value        ,
                        user_id      ,
                        edit_time
                    )
                    VALUES
                    (
                        NEW.client_id,
                        'last_name',
                        NEW.last_name,
                        NEW.editor_id,
                        NEW.last_mod
                    );
        END IF;

    END;
$$

Eine andere Lösung wäre, ein Revisionsfeld beizubehalten und dieses Feld beim Speichern zu aktualisieren. Sie können entscheiden, dass das Maximum die neueste Version ist oder dass 0 die letzte Zeile ist. Das liegt an dir.

Keethanjan
quelle
8

So haben wir es gelöst

Eine Benutzertabelle sah so aus

Users
-------------------------------------------------
id | name | address | phone | email | created_on | updated_on

Und die Geschäftsanforderungen änderten sich und wir mussten alle vorherigen Adressen und Telefonnummern überprüfen, die ein Benutzer jemals hatte. Das neue Schema sieht so aus

Users (the data that won't change over time)
-------------
id | name

UserData (the data that can change over time and needs to be tracked)
-------------------------------------------------
id | id_user | revision | city | address | phone | email | created_on
 1 |   1     |    0     | NY   | lake st | 9809  | @long | 2015-10-24 10:24:20
 2 |   1     |    2     | Tokyo| lake st | 9809  | @long | 2015-10-24 10:24:20
 3 |   1     |    3     | Sdny | lake st | 9809  | @long | 2015-10-24 10:24:20
 4 |   2     |    0     | Ankr | lake st | 9809  | @long | 2015-10-24 10:24:20
 5 |   2     |    1     | Lond | lake st | 9809  | @long | 2015-10-24 10:24:20

Um die aktuelle Adresse eines Benutzers zu finden, suchen wir nach UserData mit der Revision DESC und LIMIT 1

Um die Adresse eines Benutzers zwischen einem bestimmten Zeitraum zu erhalten, können wir created_on bewteen (Datum1, Datum 2) verwenden.

Zenex
quelle
Es ist die Lösung, die ich haben möchte, aber ich möchte wissen, wie Sie id_user mit Trigger in diese Tabelle einfügen können.
Thecassion
1
Was das geschah revision=1von dem id_user=1? Zuerst hatte ich gedacht, dass Ihre Zählung war, 0,2,3,...aber dann sah ich, dass für id_user=2die Revisionszählung0,1, ...
Pathros
1
Sie brauchen nicht idund id_userSpalten- . Just use a group ID of ID` (Benutzer-ID) und revision.
Gajus
6

MariaDB unterstützt die Systemversionierung seit 10.3. Dies ist die Standard-SQL-Funktion, die genau das tut, was Sie möchten: Sie speichert den Verlauf von Tabellendatensätzen und ermöglicht den Zugriff darauf über SELECT Abfragen. MariaDB ist eine offene Entwicklungsgabel von MySQL. Weitere Informationen zur Systemversionierung finden Sie über diesen Link:

https://mariadb.com/kb/en/library/system-versioned-tables/

midenok
quelle
Beachten Sie über den obigen Link Folgendes: "mysqldump liest keine historischen Zeilen aus versionierten Tabellen, sodass historische Daten nicht gesichert werden. Eine Wiederherstellung der Zeitstempel wäre auch nicht möglich, da sie nicht durch ein Einfügen / definiert werden können Ein Benutzer."
Daniel
4

Warum nicht einfach Bin-Protokolldateien verwenden? Wenn die Replikation auf dem MySQL-Server festgelegt ist und das Binlog-Dateiformat auf ROW festgelegt ist, können alle Änderungen erfasst werden.

Eine gute Python-Bibliothek namens noplay kann verwendet werden. Mehr Infos hier .

Ouroboros
quelle
2
Binlog kann auch dann verwendet werden, wenn Sie keine Replikation haben / benötigen. Binlog hat viele nützliche Anwendungsfälle. Die Replikation ist wahrscheinlich der häufigste Anwendungsfall, kann jedoch auch für Sicherungen und den Überwachungsverlauf genutzt werden, wie hier erwähnt.
Webaholik
3

Nur meine 2 Cent. Ich würde eine Lösung erstellen, die genau aufzeichnet, was sich geändert hat, sehr ähnlich der Lösung von transient.

Meine Änderungstabelle wäre einfach:

DateTime | WhoChanged | TableName | Action | ID |FieldName | OldValue

1) Wenn eine ganze Zeile in der Haupttabelle geändert wird, werden viele Einträge in diese Tabelle aufgenommen, ABER das ist sehr unwahrscheinlich, also kein großes Problem (die Leute ändern normalerweise nur eine Sache) 2) OldVaue (und NewValue, wenn Sie wollen) müssen eine Art epischen "anytype" sein, da es sich um beliebige Daten handeln kann. Möglicherweise gibt es eine Möglichkeit, dies mit RAW-Typen zu tun oder einfach JSON-Strings zum Ein- und Auskonvertieren zu verwenden.

Minimale Datennutzung, speichert alles, was Sie benötigen, und kann für alle Tabellen gleichzeitig verwendet werden. Ich recherchiere das gerade selbst, aber das könnte der Weg sein, den ich gehe.

Für Erstellen und Löschen nur die Zeilen-ID, keine Felder erforderlich. Beim Löschen wäre ein Flag in der Haupttabelle (aktiv?) Gut.

Worthy7
quelle
0

Der direkte Weg, dies zu tun, besteht darin, Trigger für Tabellen zu erstellen. Legen Sie einige Bedingungen oder Zuordnungsmethoden fest. Wenn ein Update oder Löschen erfolgt, wird es automatisch in die Änderungstabelle eingefügt.

Aber der größte Teil ist, wenn wir viele Spalten und viele Tabellen haben. Wir müssen den Namen jeder Spalte jeder Tabelle eingeben. Offensichtlich ist es Zeitverschwendung.

Um dies besser zu handhaben, können wir einige Prozeduren oder Funktionen erstellen, um den Namen der Spalten abzurufen.

Wir können auch einfach das Tool des dritten Teils verwenden, um dies zu tun. Hier schreibe ich ein Java-Programm MySQL Tracker

Goforu
quelle
Wie kann ich Ihren MySQL-Tracker verwenden?
Webchun
1
1. Stellen Sie sicher, dass in jeder Tabelle eine ID-Spalte als Primärschlüssel vorhanden ist. 2. Kopieren Sie die Java-Datei nach local (oder IDE). 3. Importieren Sie Bibliotheken und bearbeiten Sie die statischen Variablen aus Zeile 9-15 entsprechend Ihrer Datenbankkonfiguration und -struktur. 4. Analysieren Sie die Java-Datei und führen Sie sie aus. 5. Kopieren Sie das Konsolenprotokoll und führen Sie es als MySQL-Befehle aus
goforu
create table like tableIch denke, repliziert alle Spalten leicht
Jonathan