Czy istnieje opcja/funkcja MySQL do śledzenia historii zmian w rekordach?

Zostałem zapytany, czy mogę śledzić zmiany rekordów w bazie danych MySQL. Więc gdy pole zostało zmienione, stare vs nowe jest dostępne i data, która miała miejsce. Czy istnieje jakaś cecha lub wspólna technika, aby to zrobić?

Jeśli tak, to myślałem o zrobieniu czegoś takiego. Utwórz tabelę o nazwie changes. Będzie zawierać te same pola co Tabela master , ale poprzedzone starymi i nowymi, ale tylko dla tych pól, które zostały faktycznie zmienione i a TIMESTAMP za to. Będzie indeksowane ID. W ten sposób można uruchomić raport SELECT, aby pokazać historię każdego rekordu. Czy to dobra metoda? Dzięki!
Author: Stephen Kennedy, 2012-09-24

8 answers

To subtelne.

Jeśli wymogiem biznesowym jest " chcę przeprowadzić audyt zmian w danych-kto co i kiedy zrobił ? ", zazwyczaj można używać tabel audytu (jak na przykład wyzwalacza, który opublikował Keethanjan). Nie jestem wielkim fanem wyzwalaczy, ale ma tę wielką zaletę, że jest stosunkowo bezbolesny w implementacji - Twój istniejący kod nie musi wiedzieć o wyzwalaczach i audytach.

Jeśli wymogiem biznesowym jest " Pokaż mi, jaki był stan danych w danym dniu w past", oznacza to, że aspekt zmian w czasie wszedł do rozwiązania. Podczas gdy można, po prostu, odtworzyć stan bazy danych, patrząc na tabele audytu, jest to trudne i podatne na błędy, a dla każdej skomplikowanej logiki bazy danych staje się nieporęczne. Na przykład, jeśli firma chce wiedzieć "znaleźć adresy listów, które powinniśmy byli wysłać do klientów, którzy mieli zaległe, niezapłacone faktury w pierwszym dniu miesiąca", prawdopodobnie trzeba włożyć pół tuzina audytu stoły.

Zamiast tego, możesz upiec koncepcję zmiany w czasie w swoim projekcie schematu (jest to druga opcja, którą proponuje Keethanjan). Jest to zmiana w Twojej aplikacji, zdecydowanie na poziomie logiki biznesowej i trwałości, więc nie jest to trywialne.

Na przykład, jeśli masz tabelę taką jak Ta:

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

I chciałeś śledzić z czasem, zmieniłeś to w następujący sposób:

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

Za każdym razem, gdy chcesz zmienić rekord klienta, zamiast aktualizować rekord, ustawiasz VALID_UNTIL w bieżącym rekordzie NA NOW () I wstawiasz nowy rekord z valid_from (now) I null VALID_UNTIL. Ustawiasz status" CUSTOMER_USER " NA ID logowania bieżącego użytkownika (jeśli chcesz go zachować). Jeśli klient musi zostać usunięty, Użyj flagi CUSTOMER_STATUS, aby to zaznaczyć - nigdy nie możesz usunąć rekordów z tej tabeli.

W ten sposób zawsze możesz dowiedzieć się, jaki był status tabeli klienta dla danej daty - jaki był adres? Zmienili nazwisko? Łącząc się z innymi tabelami o podobnych datach valid_from i valid_until, możesz odtworzyć cały obraz historycznie. Aby znaleźć bieżący status, wyszukuje się rekordy z null valid_until date.

Jest nieporęczny(ściśle mówiąc, nie potrzebujesz valid_from, ale to sprawia, że zapytania są nieco łatwiejsze). Komplikuje to twój projekt i dostęp do bazy danych. Ale to znacznie ułatwia rekonstrukcję świata.

 90
Author: Neville Kuyt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-10 11:24:28

Oto prosty sposób na to:

Najpierw Utwórz tabelę historii dla każdej tabeli danych, którą chcesz śledzić(przykładowe zapytanie poniżej). Ta tabela będzie zawierać wpis dla każdego zapytania insert, update I delete wykonanego w każdym wierszu tabeli danych.

Struktura tabeli historii będzie taka sama jak tabeli danych, którą śledzi, z wyjątkiem trzech dodatkowych kolumn: kolumny do przechowywania operacji, która miała miejsce( nazwijmy ją "akcją"), daty i czasu operacji, i kolumnę do przechowywania numeru sekwencyjnego ('revision'), który zwiększa się na operację i jest pogrupowany według kolumny klucza głównego tabeli danych.

Aby wykonać takie sekwencjonowanie, tworzony jest dwukolumnowy (złożony) indeks na kolumnie klucza głównego i kolumnie rewizji. Zauważ, że sekwencjonowanie w ten sposób można wykonać tylko wtedy, gdy silnikiem używanym przez tabelę historii jest MyISAM (Zobacz "notatki MyISAM" na tej stronie)

Tabela historii jest dość łatwa do utworzenia. W ALTER Zapytanie tabeli poniżej (i w zapytaniach wyzwalających poniżej) zastąp "primary_key_column" rzeczywistą nazwą tej kolumny w tabeli danych.

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);

A następnie tworzysz wyzwalacze:

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;
I jesteś skończony. Teraz wszystkie wstawia, aktualizuje i usuwa w ' MyDb.dane "zostaną zapisane w" MyDb.data_history', dając Ci tabelę historii taką jak ta (minus wymyślona kolumna '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 

Aby wyświetlić zmiany dla danej kolumny lub kolumn z update do aktualizacja, musisz dołączyć tabelę historii do siebie w kolumnach klucza podstawowego i sekwencji. Możesz utworzyć w tym celu widok, na przykład:

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

Edytuj: O rany, ludzie lubią mój stół historyczny sprzed 6 lat: p

Moja implementacja wciąż nuci, robi się coraz większa i bardziej nieporęczna, zakładam. Napisałem widoki i całkiem ładny interfejs, aby spojrzeć na historię w tej bazie danych, ale nie sądzę, że kiedykolwiek był używany. Tak to wygląda.

Na adres niektóre komentarze w żadnej konkretnej kolejności:

  • Zrobiłem własną implementację w PHP, która była trochę bardziej zaangażowana i uniknąłem niektórych problemów opisanych w komentarzach (o przeniesieniu indeksów, znacząco. Jeśli przeniesiesz unikalne indeksy do tabeli historii, wszystko się zepsuje. Są na to rozwiązania w komentarzach). Podążanie za tym postem do listu może być przygodą, w zależności od tego, jak powstała twoja baza danych.

  • Jeśli związek między kluczem głównym a kolumną rewizji wydaje się być wyłączony zazwyczaj oznacza to, że klucz złożony jest jakoś borowany. W kilku rzadkich przypadkach miałem to się stało i byłem w stratę do przyczyny.

  • Okazało się, że to rozwiązanie jest dość wydajne, używając wyzwalaczy, jak to robi. Ponadto MyISAM jest szybki w wstawkach, co jest wszystkie wyzwalacze zrobić. Możesz to jeszcze poprawić dzięki inteligentnemu indeksowaniu (lub braku...). Wstawianie pojedynczego wiersza do tabeli MyISAM za pomocą klucza podstawowego nie powinno być operacja trzeba zoptymalizować, naprawdę, chyba że masz poważne problemy dzieje się gdzie indziej. Przez cały czas pracy z bazą danych MySQL Ta implementacja tabeli historii była włączona, nigdy nie była przyczyną żadnego z (wielu) problemów z wydajnością, które pojawiły się.

  • Jeśli otrzymujesz powtarzające się wstawki, sprawdź w warstwie oprogramowania, czy nie ma zapytań typu INSERT IGNORE. Hrmm, nie pamiętam teraz, ale myślę, że są problemy z tym schematem i transakcjami, które ostatecznie fail po uruchomieniu wielu akcji DML. Przynajmniej coś, o czym warto wiedzieć.

  • Ważne jest, aby pola w tabeli historii i tabeli danych były zgodne. Albo raczej, że Twoja Tabela danych nie ma więcej kolumn niż tabela historii. W przeciwnym razie zapytania insert/update / del w tabeli danych nie powiodą się, gdy wstawki do tabel historii umieszczą w zapytaniu kolumny, które nie istnieją (z powodu d.* w zapytaniach wyzwalających), a WYZWALACZ się nie powiedzie. t byłoby super gdyby MySQL miał coś w rodzaju scheme-triggers, gdzie można zmienić tabelę historii, jeśli kolumny zostały dodane do tabeli danych. Czy MySQL ma to teraz? Ostatnio reaguję: P

 199
Author: transient closure,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-23 08:11:32

Możesz stworzyć wyzwalacze, aby to rozwiązać. Oto samouczek (zarchiwizowany link).

Ustawianie ograniczeń i reguł w bazie danych jest lepsze niż pisanie specjalny kod do obsługi tego samego zadania, ponieważ uniemożliwi to kolejne programisty od napisania innego zapytania, które omija wszystkie specjalny kod i może opuścić bazę danych ze słabą integralnością danych.

Przez długi czas kopiowałem info do innej tabeli za pomocą skryptu od MySQL nie obsługiwał wtedy wyzwalaczy. Znalazłem to. WYZWALACZ, aby być bardziej skutecznym w śledzeniu wszystkiego.

Ten WYZWALACZ skopiuje starą wartość do tabeli historii, jeśli zostanie zmieniona kiedy ktoś edytuje wiersz. Editor ID i last mod są przechowywane w oryginalna tabela za każdym razem, gdy ktoś edytuje ten wiersz; czas odpowiada do czasu zmiany jej obecnej formy.

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;
$$

Innym rozwiązaniem byłoby zachowanie pola rewizji i zaktualizowanie tego pola w save. Możesz zdecydować, że max to najnowsza wersja, lub że 0 to najnowszy wiersz. To zależy od Ciebie.

 16
Author: Keethanjan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-26 16:42:32

Oto Jak to rozwiązaliśmy

Tabela użytkowników wyglądała tak

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

I wymagania biznesowe zmieniły się i byliśmy w potrzebie, aby sprawdzić wszystkie poprzednie adresy i numery telefonów, które użytkownik kiedykolwiek miał. nowy schemat wygląda tak

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

Aby znaleźć aktualny adres dowolnego użytkownika, wyszukujemy dane Użytkownika z revision DESC i LIMIT 1

Aby uzyskać adres użytkownika w określonym czasie możemy użyć created_on bewteen (date1 , date 2)

 9
Author: Zenex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-08-11 01:37:38

MariaDB obsługuje wersjonowanie systemu od wersji 10.3, która jest standardową funkcją SQL, która robi dokładnie to, co chcesz: przechowuje historię rekordów tabeli i zapewnia dostęp do niej za pomocą zapytań SELECT. MariaDB jest open-development fork MySQL. Więcej informacji na temat wersji systemu można znaleźć pod tym linkiem:

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

 7
Author: midenok,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-06 09:10:49

Tylko moje 2 centy. Stworzyłbym rozwiązanie, które dokładnie rejestruje to, co się zmieniło, bardzo podobne do rozwiązania transiena.

Mój ChangesTable byłby prosty:

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

1) gdy cały wiersz zostanie zmieniony w głównej tabeli, wiele wpisów trafi do tej tabeli, ale jest to bardzo mało prawdopodobne, więc nie jest to duży problem (ludzie zwykle zmieniają tylko jedną rzecz) 2) OldVaue (i NewValue jeśli chcesz) muszą być jakimś epickim "anytype", ponieważ mogą to być dowolne dane, mogą można to zrobić z typami surowymi lub po prostu za pomocą ciągów JSON do konwersji w I Na Zewnątrz.

Minimalne zużycie danych, przechowuje wszystko, czego potrzebujesz i może być używany dla wszystkich tabel jednocześnie. Właśnie to badam, ale to może skończyć się moją drogą.

Do tworzenia i usuwania, tylko identyfikator wiersza, bez pól. Po usunięciu flagi na głównej tabeli (aktywna?) byłoby dobrze.

 4
Author: Worthy7,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-15 07:46:53

Dlaczego po prostu nie użyć bin log files? Jeśli replikacja jest ustawiona na serwerze Mysql, a format pliku binlog jest ustawiony na ROW, wtedy wszystkie zmiany mogą zostać przechwycone.

Można użyć dobrej biblioteki Pythona o nazwie noplay. Więcej informacji TUTAJ .

 4
Author: Ouroboros,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-19 13:40:21

Bezpośrednim sposobem na to jest tworzenie wyzwalaczy na tabelach. Ustaw kilka warunków lub metod mapowania. Gdy nastąpi aktualizacja lub usunięcie, automatycznie wstawia się do tabeli "Zmień".

Ale najważniejsze jest to, co jeśli mamy dużo kolumn i dużo tabeli. Musimy wpisać nazwę każdej kolumny każdej tabeli. Oczywiście, to strata czasu.

Aby radzić sobie z tym lepiej, możemy utworzyć kilka procedur lub funkcji do pobierania nazw kolumn.

Możemy również użyć Narzędzie 3rd-część po prostu to zrobić. Tutaj piszę program java Mysql Tracker

 0
Author: goforu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-04 05:26:40