Jarek Tkaczyk

Jarek Tkaczyk www.GoHolidays.pl -
siła napędowa

Temat: historia zmian

MySQL, mała baza: tabela z niewielką ilością rekordów (do kilku tys.) + kilka tabel pomocniczych z maks kilkunastoma pozycjami.

W jaki sposób najlepiej wykonać funkcjonalność historii zmian w bazie, przy czym głównym celem jest wyświetlenie użytkownikowi kolejnych wersji, a nie logi poszczególnych zmian?
Tak by można było porównać kolejne wersje i uwidocznić zmienione dane (parami wersja X i wersja X+1).Jarek Tkaczyk edytował(a) ten post dnia 03.11.10 o godzinie 17:23

Temat: historia zmian

Zajmuję się głównie bazami oracle, ale nie silnik tu jest ważny.
Do tabeli, w której będziesz prowadził historyzację dodajesz 2 pola: status rekordu (1 - aktualny, 0 - nieaktualny) oraz id_poprzedniego_rekordu. Zakładam, że tabela ta ma identyfikator unikalny. Gdy nowy rekord jest tworzony, wówczas ma status = 1, a id_poprzedniego_rekordu puste. Gdy modyfikujesz rekord robisz tak, że w zmienianym rekordzie modyfikujesz tylko status na 0, a nowy, zmieniony zapisujesz ze statusem = 1, id_poprzedniego = identyfikator zmienianego rekordu, reszta pól zgodnie ze zmianami. To daje Ci wskazanie X <-> X+1, a pokazanie co się zmieniło możesz podziałać porównując poszczególne pola.
To taki pomysł na szybko :)

konto usunięte

Temat: historia zmian

Wojciech T.:
Zajmuję się głównie bazami oracle, ale nie silnik tu jest ważny.
Do tabeli, w której będziesz prowadził historyzację dodajesz 2 pola: status rekordu (1 - aktualny, 0 - nieaktualny) oraz id_poprzedniego_rekordu. Zakładam, że tabela ta ma identyfikator unikalny. Gdy nowy rekord jest tworzony, wówczas ma status = 1, a id_poprzedniego_rekordu puste. Gdy modyfikujesz rekord robisz tak, że w zmienianym rekordzie modyfikujesz tylko status na 0, a nowy, zmieniony zapisujesz ze statusem = 1, id_poprzedniego = identyfikator zmienianego rekordu, reszta pól zgodnie ze zmianami. To daje Ci wskazanie X <-> X+1, a pokazanie co się zmieniło możesz podziałać porównując poszczególne pola.
To taki pomysł na szybko :)

Nie lepiej byłoby stworzyć drugą tabelę, w której umieszczałoby się kolejne stare rekordy, a aktualne update`owało w tabeli bazowej ?
Odpytywanie pierwszej tabeli w momencie, gdy przechowywałaby całą historię zajmie chyba dłużej niż wybranie z pierwszej jednego aktualnego rekordu i - na żądanie - wybranie z tabeli `historycznej` archiwalnych danych.

Poza tym żeby pobrać wszystkie historyczne rekordy dla konkretnego parenta to musisz w Twoim rozwiązaniu pobrać wszystkie i przeanalizować wszystkich rodziców licząc od ostatniej zmiany.

Lepiej według mnie zrobić tak:

- tabela bazowa - bez zmian
- tabela historyczna:
* id
* kolumny z danymi z bazowej
* klucz obcy do bazowej
* timestamp ewentualnie
Bartosz Ślepowronski

Bartosz Ślepowronski Problem? Jaki
problem?

Temat: historia zmian

Ćwiczyłem oba rozwiązania i podpisuję się pod metodą Jakuba. Rozwiązanie z trzymaniem historycznych rekordów w tej samej tabeli potrafi szybko zmienić się w koszmar wydajnościowy.

konto usunięte

Temat: historia zmian

Ja też miałem "przyjemność" stosowania obu rozwiązań. Jeżeli trzymać taki śmietnik w bazie to w osobnej tabeli. W ten sposób jest szansa na jakąś postać normalną... Można jakieś więzy założyć, indeks na kluczu może być unikalny. Dane w tabeli historycznej bez więzów, jak leci - może jakiś indeks, żeby ciut szybciej to działało i tyle. Do tego dostęp z dowolnego ORMa jest standardowy, bez własnych przeróbek czasem mocno na siłę. Z drugiej strony - to mysql... :)

Temat: historia zmian

Oczywiście popieram jak najbardziej! Jeśli historia, to w osobnej tabeli. Fakt, mój błąd. Za szybko napisałem.
Jednak tak jak napisał Jarek Tkaczyk, tabela ma być niewielka, więc z wydajnością nie powinno być problemów nawet, gdyby to siedziało w jednej. Nie znam MySql'a, więc mogę się mylić.Wojciech T. edytował(a) ten post dnia 03.11.10 o godzinie 23:08
Jarek Tkaczyk

Jarek Tkaczyk www.GoHolidays.pl -
siła napędowa

Temat: historia zmian

Dzięki za rady, brałem pod uwagę między innymi jedną tabelę i grupowanie rekordów, właśnie ze względu na niewielkie rozmiary bazy, ale jak to w życiu bywa, może się okazać (i oby tak było), że szybko urośnie.

Tak więc myślę jednak o rozwiązaniu z osobną tabelą archiwalną + trigger dodający wpisy przy update tabeli głównej.

Teraz zastanawiam się nad jednym: jako że główna ma kilka kluczy obcych do tabel pomocniczych, czy lepiej pozostawić ten sam układ, czy też w archiwalnej wpisać wszystko statycznie?
Zasadniczo Archiwum nie ulega zmianie, więc raczej wersja nr 2, ale może macie inną sugestię?Jarek Tkaczyk edytował(a) ten post dnia 03.11.10 o godzinie 23:56
Dawid Rokita

Dawid Rokita CTO picAds.pl

Temat: historia zmian

Panowie, nie rozkminiliście wariantu następującego :-)

Stosując wasz sposób w poniższym przykładzie nie wiemy czy kraj_id = 1 powinien być aktualizowany do 2 czy według starej wersji. Nie wiem czy dobrze wyjaśniłem ale mam nadzieję że rozumiecie.

Tabela A: Id, Nowy, PoprzedniId Nazwa, kraj_id
Rekord 1: 1, 0, NULL, jakiś tam rekord 1
Rekord 2: 2, 0, 1 jakiś tam rekord 1

Tabela B: Id, Nowy, PoprzedniId Nazwa,
Rekord 1: 1, 0, NULL, Polska Rzeczpospolita Ludowa
Rekord 2: 2, 0, 1, Republika Polska
Rekord 3: 3, 0, NULL, Anglia

Mój sposób wygląda tak:
Tabela ZMIANY:
data_zdarzenia => no comment
klasa_obiektu => klasa z php co by łatwiej powołać np. wniosek
pola_klucza => pola klucza glownego, po ktorym unikalnie identyfikuję (w każdej klasie mam własność id_fields)klucz => wartość pól wyżej do identyfikacji, np po przecinku wartosc_przed_zmiana => bez komentarza :)
wartosc_po_zmianie => również bez
klasa_modyfikatora => klasa z PHP obiektu modyfikującego np. Pracownik, Userpola_klucza_modyfikatora => pola klucza głównego jak wcześniej
klucz_modyfikatora => jak wcześniej tyle że modyfikatora

Z takiej tabeli odpowiednimi (prostymi) selectami jesteś w stanie odtworzyć stan obiektu w każdej sekundzie życia systemu, ustalać jego cykl życia oraz pociągać do odpowiedzialności odpowiednich użytkowników. :-)

A tu kod do PHP, wytargałem z jakiegoś starego projektu.

Generalnie trzeba dziedziczyć po tym i tylko ustawiać $primary_fields i $not_logged_vars w każdej klasie potomnej.

Trzeba dopisać składanie obiektu do tablicy ...

class baseObject {
public $primary_fields = array('id'); // właściwość obiektu będąca kluczem
public $not_logged_vars = array(); // właściwości nie logowane
public $initialState = array();

public function logujZmiany($userObject = null)
{
if(!is_null($userObject))
{$klasa_modyfikatora = get_class($userObject);}
else {$klasa_modyfikatora = '';}
if(!is_null($userObject) && isset($userObject->id))
{$klucz_modyfikatora = $userObject->id;}
elseif(!is_null($userObject) && isset($userObject->Id))
{$klucz_modyfikatora = $userObject->Id;}
else {$klucz_modyfikatora = '';}
$data_zdarzenia = date('Y-m-d H:i:s');
$pola_klucza = implode('-',$this->primary_fields);
$pieces = array();
foreach($this->primary_fields AS $key => $var)
{
$pieces[] = $this->$var;
}
$klucz = implode('_',$pieces);
foreach($this->initialState AS $var => $content)
{
$l = Validator::removeRN($content);
$r = Validator::removeRN($this->$var);

$wynik = $l!=$r;

if(($l=='0000-00-00' && $r=='') || ($r=='0000-00-00' && $l=='')) {$wynik = false;}
if(((int)$l==0 && $r=='') || ((int)$r=='0' && $l=='')) {$wynik = false;}

if($wynik && !in_array($var,$this->not_logged_vars))
{
$SQL = "
INSERT INTO `lsi_grupa4`.`zmiany` (
`data_zdarzenia` ,
`klasa_obiektu` ,
`pola_klucza` ,
`klucz` ,
`wartosc_przed_zmiana` ,
`wartosc_po_zmianie` ,
`klasa_modyfikatora` ,
`klucz_modyfikatora` ,
`zmieniona_wlasciwosc`
)
VALUES (
'".$data_zdarzenia."',
'".get_class($this)."',
'".$pola_klucza."',
'".$klucz."',
'".$content."',
'".$this->$var."',
'".$klasa_modyfikatora."',
'".$klucz_modyfikatora."',
'".$var."'
);
";
$this->DB->SQL_insert($SQL);
}
}
}
Dawid Rokita edytował(a) ten post dnia 04.11.10 o godzinie 00:42
Jarek Tkaczyk

Jarek Tkaczyk www.GoHolidays.pl -
siła napędowa

Temat: historia zmian

Dawid Rokita:
Panowie, nie rozkminiliście wariantu następującego :-)

Stosując wasz sposób w poniższym przykładzie nie wiemy czy kraj_id = 1 powinien być aktualizowany do 2 czy według starej wersji. Nie wiem czy dobrze wyjaśniłem ale mam nadzieję że rozumiecie.

Cóż Dawid, jeśli dobrze rozumiem twoje intencje w pierwszym zdaniu, to id pozostaje 'stare'.
Niemniej jednak ty wytaczasz działo armatnie tam, gdzie ja chcę ubić muchę!
Nie o to chodzi :)
Patryk Prelewicz

Patryk Prelewicz Head of Technical
Support and Analysis
Department

Temat: historia zmian

Tabela archiwizacyjna tożsama z archiwizowaną + dwa atrybuty dodatkowe (user_dokonujący_zmiany, data_zmiany) oraz trigger realizujący archiwizację przy update rekordu tabeli archiwizowanej. W tabeli archiwum pozdejmuj wszystkie klucze i traktuj to jako "worek". Ewentualnie załóż jakiś index (nawet nieunikalny) pomagający wybierać historię zmian, czy to po dacie zmian, czy po historii danego rekordu.
W MySQLu to chyba najbardziej wydajne rozwiązanie.Patryk Prelewicz edytował(a) ten post dnia 04.11.10 o godzinie 12:00
Arkadiusz Figurny

Arkadiusz Figurny Project Manager,
właściciel firmy,
architekt systemów

Temat: historia zmian

Według mnie w tabeli archiwalnej można zrezygnować częściowo z normalizacji. Dochodzi tutaj zależność pomiędzy modyfikowanymi rekordami w tabeli głównej i w tabelach podrzędnych. Dane zawarte w tabelach archiwalnych muszą być spójne i muszą odzwierciedlać stan w jakiejś chwili w przeszłości. Projektując tabele archiwalne powinno się zwrócić uwagę jakie analizy będziemy chcieli wykonywać i optymalizować tabele pod kontem szybkości odczytu. Z założenia tabele archiwalne przechowują dużo więcej rekordów niż archiwizowane tabele i konieczne jest dokładanie dodatkowych indeksów. Warto się też zastanowić czy interesuje nas historia wszystkich danych czy może istotna jest tylko ich część. Dodatkowo odradzam zdejmowanie kluczy. Jeżeli już jakieś pole odnoszące się do klucza obcego istnieje to lepiej żeby wskazywało ono istniejący rekord niż coś czego już nie ma.Arkadiuszy Figurny edytował(a) ten post dnia 04.11.10 o godzinie 12:56
Dawid Rokita

Dawid Rokita CTO picAds.pl

Temat: historia zmian

Jarek Tkaczyk:
Dawid Rokita:
Panowie, nie rozkminiliście wariantu następującego :-)

Stosując wasz sposób w poniższym przykładzie nie wiemy czy kraj_id = 1 powinien być aktualizowany do 2 czy według starej wersji. Nie wiem czy dobrze wyjaśniłem ale mam nadzieję że rozumiecie.

Cóż Dawid, jeśli dobrze rozumiem twoje intencje w pierwszym zdaniu, to id pozostaje 'stare'.
Niemniej jednak ty wytaczasz działo armatnie tam, gdzie ja chcę ubić muchę!
Nie o to chodzi :)

Zyski:
- nie musisz alterować tabeli archiwum za każdym razem
- tylko jedna dodatkowa tabela
- nie przechowujesz danych nadmiarowych (pól, które nie zmieniły się z wersji na wersję)

Moja armata to rozwiązanie z jednego z moich systemów gdzie jest >300 tabel. Nie wyobrażam sobie robienia do każdej z nich klona z archiwum :-)

Temat: historia zmian

Dawid Rokita:
Jarek Tkaczyk:
Dawid Rokita:
Panowie, nie rozkminiliście wariantu następującego :-)

Stosując wasz sposób w poniższym przykładzie nie wiemy czy kraj_id = 1 powinien być aktualizowany do 2 czy według starej wersji. Nie wiem czy dobrze wyjaśniłem ale mam nadzieję że rozumiecie.

Cóż Dawid, jeśli dobrze rozumiem twoje intencje w pierwszym zdaniu, to id pozostaje 'stare'.
Niemniej jednak ty wytaczasz działo armatnie tam, gdzie ja chcę ubić muchę!
Nie o to chodzi :)

Zyski:
- nie musisz alterować tabeli archiwum za każdym razem
- tylko jedna dodatkowa tabela
- nie przechowujesz danych nadmiarowych (pól, które nie zmieniły się z wersji na wersję)

Moja armata to rozwiązanie z jednego z moich systemów gdzie jest >300 tabel. Nie wyobrażam sobie robienia do każdej z nich klona z archiwum :-)
Przechowywanie archiwum >300 tabel w jednej?
Maciej Niedźwiecki

Maciej Niedźwiecki Born to rails hell

Temat: historia zmian

Wojciech T.:
Dawid Rokita:

Moja armata to rozwiązanie z jednego z moich systemów gdzie jest >300 tabel. Nie wyobrażam sobie robienia do każdej z nich klona z archiwum :-)
Przechowywanie archiwum >300 tabel w jednej?

To jest prosta tabela, która potencjalnie może mieć bardzo dużo rekordów, ale będzie mocniej wykorzystywana raczej dość rzadko.
A zawsze można ją co jakiś czas zbackupować i założyć od nowa, np. raz w roku.

Temat: historia zmian

Maciej Niedźwiecki:
Wojciech T.:
Dawid Rokita:

Moja armata to rozwiązanie z jednego z moich systemów gdzie jest >300 tabel. Nie wyobrażam sobie robienia do każdej z nich klona z archiwum :-)
Przechowywanie archiwum >300 tabel w jednej?

To jest prosta tabela, która potencjalnie może mieć bardzo dużo rekordów, ale będzie mocniej wykorzystywana raczej dość rzadko.
A zawsze można ją co jakiś czas zbackupować i założyć od nowa, np. raz w roku.
Z wymagań Jarka Tkaczyka to nie wynika, a jest wręcz przeciwnie:
"W jaki sposób najlepiej wykonać funkcjonalność historii zmian w bazie, przy czym głównym celem jest wyświetlenie użytkownikowi kolejnych wersji, a nie logi poszczególnych zmian?
Tak by można było porównać kolejne wersje i uwidocznić zmienione dane (parami wersja X i wersja X+1)."

Głównym celem tej tabeli jest wyświetlanie użytkownikowi historii zmian, więc śmiem twierdzić, że tabela będzie dość często odpytywana. Chyba, że nie mam racji i zapytania będą sporadyczne. Wtedy zgoda. Można się pokusić o trzymanie wszystkiego "w jednym worku", lecz w przypadku gdy np. 10 użytkowników naraz zacznie sobie śledzić zmiany na rekordach, to może zrobić się dość nieciekawie :)
Jarek W.

Jarek W. Software Engineer

Temat: historia zmian

Wielkim specem od baz danych nie jestem, dlatego większych specjalistów zapytam: co sądzicie w tym przypadku o temporalnych bazach danych?
Maciej Niedźwiecki

Maciej Niedźwiecki Born to rails hell

Temat: historia zmian

Wojciech T.:
Maciej Niedźwiecki:
Wojciech T.:
Dawid Rokita:

Moja armata to rozwiązanie z jednego z moich systemów gdzie jest >300 tabel. Nie wyobrażam sobie robienia do każdej z nich klona z archiwum :-)
Przechowywanie archiwum >300 tabel w jednej?

To jest prosta tabela, która potencjalnie może mieć bardzo dużo rekordów, ale będzie mocniej wykorzystywana raczej dość rzadko.
A zawsze można ją co jakiś czas zbackupować i założyć od nowa, np. raz w roku.
Z wymagań Jarka Tkaczyka to nie wynika, a jest wręcz przeciwnie:
"W jaki sposób najlepiej wykonać funkcjonalność historii zmian w bazie, przy czym głównym celem jest wyświetlenie użytkownikowi kolejnych wersji, a nie logi poszczególnych zmian?
Tak by można było porównać kolejne wersje i uwidocznić zmienione dane (parami wersja X i wersja X+1)."

Głównym celem tej tabeli jest wyświetlanie użytkownikowi historii zmian, więc śmiem twierdzić, że tabela będzie dość często odpytywana. Chyba, że nie mam racji i zapytania będą sporadyczne. Wtedy zgoda. Można się pokusić o trzymanie wszystkiego "w jednym worku", lecz w przypadku gdy np. 10 użytkowników naraz zacznie sobie śledzić zmiany na rekordach, to może zrobić się dość nieciekawie :)

Zgadza się, mówiłem o rozwiązaniu Dawida dla 300 tabel i faktycznie założeniu, że historia zmian będzie odczytywana sporadycznie.
Jarek Tkaczyk

Jarek Tkaczyk www.GoHolidays.pl -
siła napędowa

Temat: historia zmian

Wojciech T.:
Z wymagań Jarka Tkaczyka to nie wynika, a jest wręcz przeciwnie:
[...]
Głównym celem tej tabeli jest wyświetlanie użytkownikowi historii zmian, więc śmiem twierdzić, że tabela będzie dość często odpytywana. Chyba, że nie mam racji i zapytania będą sporadyczne. Wtedy zgoda. Można się pokusić o trzymanie wszystkiego "w jednym worku", lecz w przypadku gdy np. 10 użytkowników naraz zacznie sobie śledzić zmiany na rekordach, to może zrobić się dość nieciekawie :)

Z moich wymagań zawartych w krótkim zdaniu można różne wnioski wyciągać, więc już piszę konkretniej.
Otóż w rzeczy samej chodzi o małą bazę, stosunkowo niewiele rekordów i sporadyczne przeglądanie historii, przy czym użytkowników jednocześnie odpytujących bazę może być w porywach 2-3.
Zatem główną kwestią, jak chciałem poruszyć, to wygoda pracy na archiwum, wydajność tutaj nie stanowi problemu.

Temat: historia zmian

Jarek Tkaczyk:
Wojciech T.:
Z wymagań Jarka Tkaczyka to nie wynika, a jest wręcz przeciwnie:
[...]
Głównym celem tej tabeli jest wyświetlanie użytkownikowi historii zmian, więc śmiem twierdzić, że tabela będzie dość często odpytywana. Chyba, że nie mam racji i zapytania będą sporadyczne. Wtedy zgoda. Można się pokusić o trzymanie wszystkiego "w jednym worku", lecz w przypadku gdy np. 10 użytkowników naraz zacznie sobie śledzić zmiany na rekordach, to może zrobić się dość nieciekawie :)

Z moich wymagań zawartych w krótkim zdaniu można różne wnioski wyciągać, więc już piszę konkretniej.
Otóż w rzeczy samej chodzi o małą bazę, stosunkowo niewiele rekordów i sporadyczne przeglądanie historii, przy czym użytkowników jednocześnie odpytujących bazę może być w porywach 2-3.
Zatem główną kwestią, jak chciałem poruszyć, to wygoda pracy na archiwum, wydajność tutaj nie stanowi problemu.

No to chyba wygrywa armata :)
Dawid Rokita

Dawid Rokita CTO picAds.pl

Temat: historia zmian

Przy 4 583 325 rekordach zajmuje 915 mega (wraz z jednym indeksem, który jest wykorzystywany). Z ciekawości odpaliłem (mój lap: core duo 2,54 ,3Gb RAM, Vista, MySQL 5.0.67-community) zapytanie jakie jest w warunkach produkcyjnych wykonywane i zebrało dane w 3,6062 sekundy więc raczej nie ma tragedii. Na maszynie produkcyjnej na pewno jest szybciej).

Zawsze można usuwać "zbyt stare informacje" albo zrobić sharding.

Następna dyskusja:

MSSQL 2005 Express - histor...




Wyślij zaproszenie do