W projekcie mamy wymóg przechowywania wszystkich wersji (historii zmian) dla jednostek w bazie danych. Obecnie mamy 2 zaprojektowane propozycje dla tego:
np. dla Podmiotu „Pracownik”
Projekt 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)"
Projekt 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, .., ..)"
Czy jest inny sposób na zrobienie tego?
Problem z "Projektem 1" polega na tym, że musimy analizować XML za każdym razem, gdy potrzebujesz dostępu do danych. Spowolni to proces, a także doda pewne ograniczenia, na przykład nie możemy dodawać złączeń w polach danych wersji.
Problem z „Projektem 2” polega na tym, że musimy zduplikować każde pole we wszystkich obiektach (mamy około 70-80 elementów, dla których chcemy zachować poprawki).
sql
database
database-design
versioning
Ramesh Soni
źródło
źródło
Odpowiedzi:
źródło
SELECT * FROM EmployeeHistory WHERE LastName = 'Doe'
wyniki w pełnym skanie tabeli . Skalowanie aplikacji nie jest najlepszym pomysłem.Myślę, że kluczowe pytanie, które należy tutaj zadać, brzmi: „Kto / co będzie korzystało z historii”?
Jeśli ma to być głównie do raportowania / czytelnej historii, wdrożyliśmy ten schemat w przeszłości ...
Utwórz tabelę o nazwie „AuditTrail” lub coś, co ma następujące pola ...
Następnie możesz dodać kolumnę „LastUpdatedByUserID” do wszystkich swoich tabel, która powinna być ustawiana za każdym razem, gdy wykonujesz aktualizację / wstawianie do tabeli.
Następnie możesz dodać wyzwalacz do każdej tabeli, aby przechwycić wszelkie wstawienia / aktualizacje, które mają miejsce, i utworzyć wpis w tej tabeli dla każdego zmienionego pola. Ponieważ tabela jest również dostarczana z „LastUpdateByUserID” dla każdej aktualizacji / wstawienia, możesz uzyskać dostęp do tej wartości w wyzwalaczu i użyć jej podczas dodawania do tabeli inspekcji.
Używamy pola RecordID do przechowywania wartości pola klucza aktualizowanej tabeli. Jeśli jest to klucz złożony, po prostu wykonujemy konkatenację ciągów znaków z „~” między polami.
Jestem pewien, że ten system może mieć wady - w przypadku mocno zaktualizowanych baz danych wydajność może zostać obniżona, ale w przypadku mojej aplikacji internetowej otrzymujemy znacznie więcej odczytów niż zapisów i wydaje się, że działa całkiem dobrze. Napisaliśmy nawet małe narzędzie VB.NET, które automatycznie zapisuje wyzwalacze na podstawie definicji tabeli.
Tylko myśl!
źródło
sysname
może być bardziej odpowiednim typem danych dla nazw tabel i kolumn.Artykuł History Tables w programie Database Programmer blogu może być przydatna - obejmuje niektóre z kwestii poruszonych tu i omawia przechowywanie delt.
Edytować
W eseju History Tables autor ( Kenneth Downs ) zaleca prowadzenie tabeli historii składającej się z co najmniej siedmiu kolumn:
Kolumny, które nigdy się nie zmieniają lub których historia nie jest wymagana, nie powinny być śledzone w tabeli historii, aby uniknąć nadużyć. Przechowywanie delty dla wartości liczbowych może ułatwić późniejsze zapytania, nawet jeśli można je wyprowadzić ze starych i nowych wartości.
Tabela historii musi być bezpieczna, a użytkownicy niesystemowi nie mogą wstawiać, aktualizować ani usuwać wierszy. Należy obsługiwać tylko okresowe czyszczenie, aby zmniejszyć całkowity rozmiar (i jeśli jest to dozwolone w przypadku użycia).
źródło
Wdrożyliśmy rozwiązanie bardzo podobne do rozwiązania sugerowanego przez Chrisa Robertsa, które działa dla nas całkiem dobrze.
Jedyna różnica polega na tym, że przechowujemy tylko nową wartość. Stara wartość jest przecież przechowywana w poprzednim wierszu historii
Powiedzmy, że masz tabelę z 20 kolumnami. W ten sposób musisz tylko zapisać dokładnie zmienioną kolumnę, zamiast zapisywać cały wiersz.
źródło
Unikaj projektu 1; nie jest to zbyt przydatne, gdy trzeba będzie na przykład przywrócić stare wersje rekordów - automatycznie lub „ręcznie” za pomocą konsoli administratora.
Naprawdę nie widzę wad Projektu 2. Myślę, że druga, Tabela historii, powinna zawierać wszystkie kolumny obecne w pierwszej, tabeli Rekordy. Np. W mysql możesz łatwo stworzyć tabelę o takiej samej strukturze jak inna table (
create table X like Y
). A kiedy masz zamiar zmienić strukturę tabeli Records w aktywnej bazie danych, i tak musisz użyćalter table
poleceń - i nie ma dużego wysiłku, aby uruchomić te polecenia również dla tabeli historii.Uwagi
RevisionId
kolumną;ModifiedBy
- użytkownik, który utworzył daną rewizję. Możesz także chcieć mieć poleDeletedBy
do śledzenia, kto usunął daną wersję.DateModified
powinno oznaczać - albo oznacza to, gdzie ta konkretna wersja została utworzona, albo będzie oznaczać, kiedy ta konkretna wersja została zastąpiona inną. Pierwsza z nich wymaga, aby pole znajdowało się w tabeli Rekordy i na pierwszy rzut oka wydaje się być bardziej intuicyjna; drugie rozwiązanie wydaje się jednak być bardziej praktyczne w przypadku usuniętych rekordów (data usunięcia tej konkretnej zmiany). Jeśli zdecydujesz się na pierwsze rozwiązanie, prawdopodobnie będziesz potrzebować drugiego polaDateDeleted
(tylko jeśli potrzebujesz go oczywiście). Zależy od Ciebie i tego, co chcesz nagrać.Operacje w Projekcie 2 są bardzo trywialne:
ModyfikowaćJeśli wybierzesz Projekt 2, wszystkie polecenia SQL potrzebne do tego będą bardzo łatwe, a także konserwacja! Być może będzie o wiele łatwiej, jeśli użyjesz kolumn pomocniczych (
RevisionId
,DateModified
) również w tabeli Rekordy - aby utrzymać obie tabele na dokładnie tej samej strukturze (z wyjątkiem unikalnych kluczy)! Pozwoli to na proste polecenia SQL, które będą odporne na wszelkie zmiany struktury danych:Nie zapomnij skorzystać z transakcji!
Jeśli chodzi o skalowanie , to rozwiązanie jest bardzo wydajne, ponieważ nie transformujesz żadnych danych z XML-a tam iz powrotem, tylko kopiujesz całe wiersze tabeli - bardzo proste zapytania, przy użyciu indeksów - bardzo wydajne!
źródło
Jeśli musisz przechowywać historię, utwórz tabelę-cień z tym samym schematem co tabela, którą śledzisz, oraz kolumną „Data wersji” i „Typ zmiany” (np. „Usuń”, „aktualizuj”). Napisz (lub wygeneruj - patrz poniżej) zestaw wyzwalaczy do wypełnienia tabeli audytu.
Stworzenie narzędzia, które odczyta systemowy słownik danych dla tabeli i wygeneruje skrypt, który utworzy tabelę-cień i zestaw wyzwalaczy do jej zapełnienia, jest dość proste.
Nie próbuj używać do tego XML, przechowywanie XML jest dużo mniej wydajne niż natywny magazyn tabel bazy danych, z którego korzysta ten typ wyzwalacza.
źródło
Ramesh, byłem zaangażowany w tworzenie systemu opartego na pierwszym podejściu.
Okazało się, że przechowywanie wersji w formacie XML prowadzi do ogromnego wzrostu bazy danych i znacznego spowolnienia działania.
Moje podejście polegałoby na stworzeniu jednej tabeli na jednostkę:
gdzie IsActive to znak najnowszej wersji
Jeśli chcesz powiązać dodatkowe informacje z wersjami, możesz utworzyć oddzielną tabelę zawierającą te informacje i połączyć ją z tabelami encji za pomocą relacji PK \ FK.
W ten sposób możesz przechowywać wszystkie wersje pracowników w jednej tabeli. Zalety tego podejścia:
Pamiętaj, że powinieneś pozwolić, aby klucz podstawowy był nieunikalny.
źródło
Sposób, w jaki widziałem to w przeszłości, jest
Nigdy nie „aktualizujesz” tej tabeli (z wyjątkiem zmiany poprawności isCurrent), po prostu wstaw nowe wiersze. Dla danego EmployeeId tylko 1 wiersz może mieć isCurrent == 1.
Złożoność utrzymania tego może być ukryta przez widoki i wyzwalacze "zamiast" (w Oracle zakładam podobne rzeczy w innych RDBMS), możesz nawet przejść do zmaterializowanych widoków, jeśli tabele są zbyt duże i nie mogą być obsługiwane przez indeksy) .
Ta metoda jest w porządku, ale możesz skończyć z kilkoma złożonymi zapytaniami.
Osobiście bardzo podoba mi się sposób, w jaki robisz to w Design 2, tak samo jak robiłem to w przeszłości. Jest łatwy do zrozumienia, prosty we wdrożeniu i prosty w utrzymaniu.
Tworzy również bardzo niewielkie obciążenie dla bazy danych i aplikacji, zwłaszcza podczas wykonywania zapytań odczytu, co prawdopodobnie będziesz robić w 99% przypadków.
Dość łatwo byłoby również zautomatyzować tworzenie tabel historii i wyzwalaczy do utrzymania (zakładając, że byłoby to zrobione przez wyzwalacze).
źródło
Weryfikacja danych jest jednym z aspektów koncepcji czasowej bazy danych o okresie ważności . Przeprowadzono wiele badań i pojawiło się wiele wzorców i wytycznych. Napisałem obszerną odpowiedź z wieloma odniesieniami do tego pytania dla zainteresowanych.
źródło
Podzielę się z wami moim projektem, który różni się od obu projektów tym, że wymaga jednej tabeli na każdy typ jednostki. Najlepszym sposobem opisania projektu bazy danych jest użycie ERD, oto moje:
W tym przykładzie mamy jednostkę o nazwie pracownik . user table przechowuje rekordy użytkowników, a entity i entity_revision to dwie tabele, które przechowują historię wersji dla wszystkich typów encji, które będziesz mieć w systemie. Oto jak działa ten projekt:
Dwa pola entity_id i revision_id
Każda jednostka w twoim systemie będzie miała swój własny unikalny identyfikator. Twój obiekt może przejść przez zmiany, ale jego entity_id pozostanie taki sam. Musisz zachować ten identyfikator jednostki w tabeli pracowników (jako klucz obcy). W tabeli encji należy również zapisać typ swojego podmiotu (np. „Pracownik”). Teraz, jeśli chodzi o revision_id, jak wskazuje jego nazwa, śledzi on wersje encji. Najlepszym sposobem, jaki znalazłem, jest użycie atrybutu Employer_id jako revision_id. Oznacza to, że będziesz mieć zduplikowane identyfikatory wersji dla różnych typów podmiotów, ale nie jest to dla mnie gratką (nie jestem pewien co do Twojego przypadku). Jedyną ważną uwagą jest to, że kombinacja entity_id i revision_id powinna być unikalna.
W tabeli entity_revision znajduje się również pole stanu, które wskazuje stan wersji. To może mieć jeden z trzech stanów: , lub
latest
obsolete
deleted
(nie opierając się na bieżąco korekt pomaga wiele, aby zwiększyć zapytań).Ostatnia uwaga na temat revision_id: nie utworzyłem klucza obcego łączącego Employer_id z revision_id, ponieważ nie chcemy zmieniać tabeli entity_revision dla każdego typu jednostki, którą moglibyśmy dodać w przyszłości.
WPROWADZENIE
Dla każdego pracownika , którego chcesz wstawić do bazy danych, dodasz również rekord do encji i rewizji encji . Te dwa ostatnie rekordy ułatwiają śledzenie przez kogo i kiedy rekord został wstawiony do bazy danych.
AKTUALIZACJA
Każda aktualizacja istniejącego rekordu pracownika zostanie zaimplementowana jako dwie wstawki, jedna w tabeli pracowników i jedna w rewizji encji. Drugi pomoże ci dowiedzieć się, przez kogo i kiedy rekord został zaktualizowany.
USUNIĘCIE
W celu usunięcia pracownika do rewizji entity_revision wstawiany jest rekord określający usunięcie i wykonane.
Jak widać w tym projekcie, żadne dane nigdy nie są zmieniane ani usuwane z bazy danych, a co ważniejsze, każdy typ jednostki wymaga tylko jednej tabeli. Osobiście uważam ten projekt za naprawdę elastyczny i łatwy w użyciu. Ale nie jestem pewien co do Ciebie, ponieważ Twoje potrzeby mogą być inne.
[AKTUALIZACJA]
Mając obsługiwane partycje w nowych wersjach MySQL, uważam, że mój projekt ma również jedną z najlepszych wydajności. Można podzielić
entity
tabelę za pomocątype
pola, podczas gdy partycjonowaćentity_revision
za pomocą jegostate
pola.SELECT
Znacznie zwiększy to liczbę zapytań, zachowując prostotę i przejrzystość projektu.źródło
Jeśli rzeczywiście potrzebujesz ścieżki audytu, pochyliłbym się w stronę rozwiązania tabeli audytów (wraz ze zdenormalizowanymi kopiami ważnej kolumny w innych tabelach, np.
UserName
.). Pamiętaj jednak, że gorzkie doświadczenie wskazuje, że pojedyncza tabela audytu będzie ogromnym wąskim gardłem na drodze; tworzenie indywidualnych tabel audytu dla wszystkich kontrolowanych tabel prawdopodobnie jest warte wysiłku.Jeśli chcesz śledzić rzeczywiste wersje historyczne (i / lub przyszłe), standardowym rozwiązaniem jest śledzenie tej samej jednostki z wieloma wierszami przy użyciu kombinacji wartości początkowej, końcowej i czasu trwania. Możesz użyć widoku, aby ułatwić dostęp do bieżących wartości. Jeśli podejmiesz takie podejście, możesz napotkać problemy, jeśli dane wersjonowane odwołują się do danych podlegających zmianom, ale bez wersji.
źródło
Jeśli chcesz zrobić pierwszą, możesz użyć XML również dla tabeli Pracownicy. Większość nowszych baz danych umożliwia wykonywanie zapytań w pola XML, więc nie zawsze jest to problem. I może być prostszy sposób na dostęp do danych pracowników, niezależnie od tego, czy jest to najnowsza, czy wcześniejsza wersja.
Spróbowałbym jednak drugiego podejścia. Można to uprościć, mając tylko jedną tabelę Employees z polem DateModified. EmployeeId + DateModified byłby kluczem podstawowym i można zapisać nową wersję, po prostu dodając wiersz. W ten sposób archiwizowanie starszych wersji i przywracanie wersji z archiwum jest również łatwiejsze.
Innym sposobem na zrobienie tego może być model przechowalni danych autorstwa Dana Linstedta. Zrobiłem projekt dla holenderskiego biura statystycznego, który korzystał z tego modelu i działa całkiem dobrze. Ale nie sądzę, by było to bezpośrednio przydatne w codziennym użytkowaniu bazy danych. Możesz jednak mieć kilka pomysłów, czytając jego artykuły.
źródło
Co powiesz na:
Tworzysz klucz podstawowy (EmployeeId, DateModified) i aby uzyskać „bieżące” rekordy, po prostu wybierz MAX (DateModified) dla każdego identyfikatora pracownika. Przechowywanie IsCurrent jest bardzo złym pomysłem, ponieważ po pierwsze można go obliczyć, a po drugie, dane są zbyt łatwe do utraty synchronizacji.
Możesz także utworzyć widok zawierający tylko najnowsze rekordy i głównie używać go podczas pracy w aplikacji. Zaletą tego podejścia jest to, że nie masz duplikatów danych i nie musisz zbierać danych z dwóch różnych miejsc (aktualnych w Employees i zarchiwizowanych w EmployeesHistory), aby uzyskać całą historię lub wycofać itp.) .
źródło
Jeśli chcesz polegać na danych historycznych (do celów raportowania), powinieneś użyć struktury podobnej do tej:
Lub globalne rozwiązanie do aplikacji:
Możesz zapisać swoje wersje również w formacie XML, wtedy masz tylko jeden rekord dla jednej wersji. Będzie to wyglądać następująco:
źródło
Mieliśmy podobne wymagania i odkryliśmy, że często użytkownik chce tylko zobaczyć co zostało zmienione, niekoniecznie cofając jakiekolwiek zmiany.
Nie jestem pewien, jaki jest twój przypadek użycia, ale zrobiliśmy to, aby utworzyć i sprawdzić tabelę, która jest automatycznie aktualizowana o zmiany w jednostce biznesowej, w tym przyjazną nazwę wszelkich odniesień do kluczy obcych i wyliczeń.
Za każdym razem, gdy użytkownik zapisuje swoje zmiany, przeładowujemy stary obiekt, uruchamiamy porównanie, zapisujemy zmiany i zapisujemy jednostkę (wszystko odbywa się w ramach jednej transakcji w bazie danych na wypadek problemów).
Wydaje się, że działa to bardzo dobrze dla naszych użytkowników i oszczędza nam bólu głowy związanego z posiadaniem całkowicie oddzielnej tabeli kontroli z tymi samymi polami, co nasza jednostka biznesowa.
źródło
Wygląda na to, że chcesz śledzić zmiany w określonych obiektach w czasie, np. ID 3, „bob”, „123 main street”, a następnie inny ID 3, „bob” „234 elm st” itd., Zasadniczo będąc w stanie zwymiotować historię zmian pokazującą każdy adres, pod którym był „bob”.
Najlepszym sposobem na to jest posiadanie pola „is current” w każdym rekordzie i (prawdopodobnie) znacznika czasu lub FK do tabeli daty / czasu.
Wstawki muszą wtedy ustawić „jest aktualny”, a także wyłączyć ustawienie „jest aktualny” na poprzednim rekordzie „jest aktualny”. Zapytania muszą określać „jest aktualne”, chyba że chcesz mieć całą historię.
Istnieją dalsze poprawki, jeśli jest to bardzo duża tabela lub spodziewana jest duża liczba poprawek, ale jest to dość standardowe podejście.
źródło