Best Practices: projekt aplikacji a rodzaje zbiorów danych



Przetwarzanie danych trwa coraz dłużej i dłużej a dokładanie kolejnych procesorów czy pamięci pomaga tylko doraźnie? Aplikacja rozrasta się bardzo szybko w niezbyt kontrolowany sposób a niewielkie zmiany wymagają modyfikacji w wielu miejscach w aplikacji? Przyspieszenie jednego kawałka aplikacji powoduje spadek wydajności w innym procesie? Analiza kodu jest skomplikowana i wymaga weryfikacji linijka po linijce zadań poszczególnych zmiennych i często natykasz się na hardcoding? 

Jeśli tak, to sprawdź, czy właściwie określiłeś i przetwarzasz różne rodzaje danych

Czy wszystkie dane są takie same?
Projektując bazę danych z reguły zaczynamy od projektu struktury danych. Określamy encje, atrybuty oraz ich powiązania z innymi encjami. Encje w dalszym projekcie przekształcają się w tabele, otrzymują klucze główne, indexy.

W tabelach przechowujemy dane, których używany podczas działania systemu. Dane te odczytujemy, zapisujemy, modyfikujemy, procesujemy. 

Ale czy wszystkie dane w tabelach są takie same? Czy ze wszystkich danych korzystamy w ten sam sposób? Czy potrzebne są nam do tych samych zadań?

Nie. Dane w bazie danych różnią się głównie pod kątem tego, do czego służą. Główne dane wyliczamy na podstawie rejestrowanych w bazie zdarzeń, inne dane służą nam do określenia sposobu działania funkcjonalności, inne są zaś tylko zalogowaniem zmian w systemie.

Po co określać rodzaj danych?
Jednym z kluczowych elementów w procesie tworzenia aplikacji opartej na bazie danych jest właściwe określenie rodzajów zbiorów danych z jakimi mamy do czynienia. Każdy typ danych będzie miał inne zadanie w naszym systemie, inaczej będziemy pobierać takie dane i inną będą miały rolę w przetwarzaniu danych. Konsekwencją niewłaściwego rozpoznania zbiorów danych jest nadmierne skomplikowanie systemu, niewydolność systemu, geometryczny spadek wydajności w czasie, tendencja do tworzenia kodów spaghetti , tendencja do hardcodingu.

Każdy rodzaj tabel będzie miał inne cechy. Niektóre dane będziemy wersjonować inne niekoniecznie, jedne dane będą często modyfikowane inne praktycznie niezmienne, jedne tabele będą odnosiły się do całej aplikacji, zaś inne tylko do określonych funkcjonalności.

Najczęstsze kategorie danych w systemie
1. tabele słownikowe
2. tabele konfiguracyjne / parametryzacyjne aplikacji
3. tabele konfiguracyjne / parametryzacyjne funkcjonalne
4. tabele danych
5. tabele logów archiwizacyjnych
6. tabele logów zdarzeń

W większości przypadków przypisanie tabeli do jednej z tych kategorii nie przysparza większych problemów jednak w niektórych przypadkach może to nie być takie oczywiste. 

Tabele słownikowe
Tabele słownikowe to tabele przechowujące informacje, najczęściej opisowe, do używanych w systemie skrótów czy pojęć. Najczęściej stosowane do takich danych jak waluty, państwa i inne. 

Przykład:

PLN
Polski złoty
AUD
Dolar australijski
EUR
Euro
USD
Dolar amerykański

Dane te najczęściej zmieniają się bardzo rzadko i nie są wersjonowane.

Do pobierania danych słownikowych najlepiej przygotować API – funkcje słownikowe odpowiadające za pobieranie danych.

W systemach przetwarzających duże ilości danych funkcje słownikowe powinny pobierać na początku przetwarzania całą tabelę słownikową i przechowywać ją w pamięci. W bazach Oracle można zastosować do funkcji słownikowych kolekcje globalne. Dzięki temu podczas przetwarzań funkcje słownikowe nie będą musiały sięgać do fizycznych tabel. Zmniejszy to ilość przełączeń między silnikami SQL i PL/SQL oraz ilość odczytów  z dysku. 

Pamiętać należy o określeniu zasad przeładowywania funkcji słownikowych szczególnie w procesach działających bez przerwy, w pętli. Można co jakiś czas odświeżać tablice w pamięci np. w momencie zmiany daty  lub skorzystać z funkcjonalności RESULT CACHE w Oracle 11 i wyższych.

Tabele konfiguracyjne aplikacji
Tabele konfiguracyjne aplikacji przechowują dane wspólne dla całej aplikacji, nie różnicujące funkcjonalności systemu. Typowymi informacjami przechowywanymi w tabelach konfiguracyjnych aplikacji są informacje o:
- ścieżkach do wczytywanych plików
- ścieżkach do raportów
- stronie kodowej systemu
- ścieżce do katalogów archiwalnych
- wersja systemu
- stronach kodowych

Tabele konfigruracyjne tego typi mogą być zbudowane jak klasyczne tabele EAV – entity-attribute-value, gdzie przechowujemy informacje o nazwie zmiennej oraz jej wartości.

par_name
par_val
PATH_RAP
files/reports/
PATH_FILE_LOAD
files/load
PATH_FILE_ARCH
Files/arch/

Tabele konfiguracyjne podobnie jak tabele słownikowe nie są z reguły wersjonowane.  Powinny być pobierane za pomocą utworzonych funkcji API. Dane powinny być pobierane raz na początku przetwarzania naszego systemu i przechowywane w tablicach w pamięci. Dzięki temu zminimalizujemy ilość zapytań do fizycznych tabel oraz zmniejszymy ilość przełączeń między silnikami SQL i PL/SQL

Tak jak w przypadku tabel słownikowych pamiętać należy o określeniu zasad odświeżaniu danych w kolekcjach, szczególnie w procesach działających bez przerwy np. można odświeżać kolekcje w momencie zmiany daty lub skorzystać z funkcji RESULT CACHE dostępnej w Oracle 11 lub wyższych.

Tabele konfiguracyjne/parametryzacyjne
Tabele konfiguracyjne przechowują parametry funkcji aplikacji. Parametryzacja ma za zadanie umożliwić elastyczne działanie naszego systemu i zminimalizować hardcoding. Parametryzacja steruje naszym JAK ma działać system. Kluczem do właściwej parametryzacji systemu jest poprawne określenie funkcji systemu a następnie zróżnicowanie tej funkcjonalności według potrzeb specyficznych potrzeb.

Przykład:
Nasz system ma naliczać opłaty miesięczne i roczne. 
Czy to oznacza, że mamy dwie funkcjonalności:
1. naliczanie opłat miesięcznych
2. naliczanie opłat rocznych

W takiej sytuacji doprowadzimy do konieczności utworzenia dwóch procesów do naliczania opłat: do naliczania opłaty miesięcznej oraz naliczania opłaty rocznej. W większości oba procesy będą identyczne, jedyne co je różni to moment księgowania opłaty. Jeśli będziemy chcieli rozwinąć system i dodać nowe opłaty np. dzienne czy kwartalne – ilość potrzebnego kodu będzie nam proporcjonalnie rosła a co za tym idzie system będzie coraz bardziej zawiły i skomplikowany. W przypadku zmiany w procesie naliczania opłaty – zmiany będzie trzeba dokonywać wielokrotnie – w każdym procesie naliczającym opłaty.  

To może źle określiliśmy funkcjonalności?
Najpierw zdefiniujmy proces. U nas procesem będzie NALICZANIE OPŁAT.
Jakich opłat? Miesięcznych i rocznych. JAKICH -  to właśnie nasza parametryzaja.

Czyli w naszym systemie będziemy mieli jedną funkcjonalność:
1. naliczanie opłat

z parametrem: KIEDY NALICZAĆ: miesięcznie/rocznie

Tabela parametryzacyjna mogłaby wyglądać tak:
Id
Opis
naliczanie
data_od
data_do
1
OPL1
Y
1.12.2015

2
OPL2
M
1.12.2150


Tabele konfiguracyjne powinny być wersjonowane – parametry powinny mieć datę ważności od – do. Należy pilnować, by w danym czasie nie było dwóch ważnych parametrów, gdyż może to skutkować np. naliczaniem podwójnych opłat. 

Danych konfiguracyjnych nie powinno się modyfikować. Często możliwość sprawdzenia przeszłej konfiguracji jest kluczowe do analizy zgłoszeń helpdeskowych.  Starą konfigurację powinno się przeterminowywać  i tworzyć nowy zestaw parametrów.

Podczas przetwarzania danych bardzo często korzysta się z parametryzacji, dlatego też ciągłe sięganie do bazy danych może być obciążające. Dlatego do tabel konfiguracyjnych powinny być przygotowane funkcje API zarządzające pobieraniem parametrów.

Parametry powinny być pobierane do pamięci na początku procesu a w trakcie procesu parametry powinny być odczytywane z kolekcji. Zmniejszymy dzięki temu ilość odczytów z dysku oraz przełączeń między silnikami SQL i PL/SQL. 

Jak w przypadku tabel słownikowych i konfiguracyjnych aplikacji – potrzebne jest sprecyzowanie momentu odświeżania danych szczególnie w procesach działających 24/7 by zagwarantować aktualności parametrów. Można w tym celu albo uzależnić odświeżanie od zmiany daty lub zastosować funkcjonalność RESULT CACHE dostępną w Oracle 11 lub wyższych.

Tabele danych
Tabele danych to nasze najważniejsze dane w systemie. Główna encja, dla której przetwarzamy informacje. W systemach bankowych będą to rachunki, w systemach telefonii  czy telewizji – umowy. Generalnie jest to główny podmiot, dla którego wczytujemy dane, przetwarzamy, wyliczamy, zapisujemy  i generujemy dla niego raportu. 

Jeśli takich głównych podmiotów mamy kilka i nie są ze sobą powiązane – być może powinnyśmy utworzyć dla każdego z nich inny system.

Czasami wyodrębnienie głównego podmiotu naszego systemu może stwarzać niejakie kłopoty. Bo czy powinien to być klient czy umowa podpisana przez klienta? Czy rachunek czy karta klienta? Numer telefonu czy abonent?

Żeby właściwie określić główny podmiot należy się zastanowić, co jest najważniejsze w systemie. Mogą być różne rozwiązania jednak musi to być coś, co będzie w miarę stabilne w systemie i wobec czego chcielibyśmy skalować aplikację.

W tabelach danych powinniśmy przechowywać wszystkie istotne dla systemu informacje. Na tabelach z danymi będą też opierać się nasze wszystkie główne procesy.

Rekordów w tabelach danych jest z reguły bardzo dużo i musimy podczas przetwarzania pobrać i przetworzyć wszystkie albo prawie wszystkie dane z tabeli. Sięganie w takiej sytuacji do tabeli po pojedyncze rekordy jest mało wydajne, nawet jeśli mamy dobrze zaprojektowane indexy. O wiele lepiej na samym początku przetwarzania pobrać wszystkie rekordy z tabeli (full scan) do kolekcji, przeprocesować rekordy w pamięci, a dopiero na sam koniec zapisać przetworzone dane. 

Algorytm takiego procesu mógłby wyglądać tak:

1. pobranie wszystkich abonentów do kolekcji

select * into  kolekcja bulk collect from tabela;

for i in 1..kolekcja.count loop
1. wylicz opłaty dla pojedynczego abonenta
2. przygotuj upomnienia o przeterminowanych płatnościach
3. ……
end loop;

4. zapisz wyliczone opłaty
5. zapisz wygenerowane przypomnienia
6….
9. zapisz dane abonenta

Oczywiście procedury funkcjonalne również nie powinny pobierać z bazy danych rekordów abonentów tylko przetwarzać rekord przekazany parametrem.

Główne dane przetwarzane są najczęściej w ściśle określonych momentach. Z reguły takie procesy uruchamiane są na noc- na koniec dnia i/lub miesiąca. 

Tabele danych często się zmieniają, codziennie albo nawet kilka razy dziennie. Dlatego też nie ma sensu ich wersjonować. Warto za to zapisać datę ostatniej zmiany rekordu oraz kto tej zmiany dokonywał. 

Zmiany na tabelach danych rejestrowane są w tabelach logów. Najwydajniejszym sposobem logowani zmian są funkcje API odpowiadające za zapis rekordu. Często jednak wykorzystuje się w tym celu mniej wydajne triggery.

Tabele logów 
Tabele logów to tak naprawdę tabele zmian zachodzących w systemie. Można rozróżnić dwa główne typu tabel logów:
- tabele logów zmian wewnętrznych czyli tabele archiwizujące. Tabele takie przechowują zmiany zachodzące na tabelach danych by móc przechować historię zmian
- tabele logów zdarzeń – tabele zmian w systemie, których źródłem są różne zdarzenia wpływające na stan głównych danych aplikacji.

Tabele logów zdarzeń
Tabele logów zdarzeń zewnętrznych często są mylone z tabelami danych. Tabele logów zdarzeń z reguły są dużo większe niż tabele danych i mają sporą dynamikę przyrostów. Konsekwencją traktowania tabeli zdarzeń jako tabeli danych może być duży spadek wydajności aplikacji względem czasu oraz wzrostu głównych danych. Skalowalność wobec tabeli logów zdarzeń bardzo negatywnie wpływa na czasy przetwarzań. Zmiana tego stanu rzeczy jest niezmiernie trudna i wymaga najczęściej przeprojektowania przynajmniej części aplikacji. Dlatego niezwykle istotne jest właściwe określenie tabel zdarzeń.

Takimi tabelami zdarzeń mogą być: transakcje klientów banków, wykonane połączenia w systemach telefonii, zakupy w sklepach. Informacje te nie są niczym innym jak logiem zmian: salda na koncie klienta, salda na koncie numeru telefonu czy ilością towaru w magazynie itp. 

Z uwagi na fakt, że logów zdarzeń w systemie jest najwięcej, nie powinniśmy przetwarzać tych danych razem z głównymi danymi np. na koniec dnia czy miesiąca. Ilość danych znacznie spowolni nam czas przetwarzania. Takie dane powinny być przetwarzane jak najszybciej po ich otrzymaniu, zapisywane i – zapominane. Jeśli wczytujemy logi zdarzeń zewnętrznych z plików – powinniśmy przetwarzać je od razu po odczytaniu i zapisaniu do systemu, tak by nie musieć pobierać tych informacji ponownie (chyba że do raportów). Jeśli od tych danych mają być liczone prowizje, opłaty, odsetki  - najlepiej wszystkie te rzeczy zrobić w momencie otrzymania tych danych.

Rozrzucenie przetwarzania wielkich ilości danych z tabel zdarzeń na cały dzień odciąży nam przetwarzanie nocne (zamknięcie dnia czy miesiąca) i drastycznie skróci czas przetwarzania. 

Podsumowanie
Odpowiednie rozpoznanie rodzajów danych w tabelach umożliwi nam właściwe zaprojektowanie systemu. API do danych słownikowych i konfiguracyjnych uporządkuje funkcje pobierania parametryzacji, zminimalizuje hardcoding oraz zmniejszy ilość zapytań do bazy danych. Właściwe zaś rozróżnienie tabel danych od tabel zdarzeń pomoże w określeniu momentu przetwarzania danych oraz w zapanowaniu nad wydajnością i skalowalnością aplikacji.

Typ
Wersjonowanie
Dostęp (przetwarzanie)
Zapis
Moment przetwarzania
aktualizacja
Dane słownikowe
NIE/TAK
API
GUI lub skrypty
N/d
GUI lub skrypty
Parametryzacja aplikacji
NIE
API
GUI lub skrypty
N/d
GUI lub skrypty
Konfiguracja
TAK
API
GUI
N/d
brak
Dane
NIE
FULL SCAN
GUI, pliki
Eod, eom
GUI/ pliku (logi zmian)
Logi zmian wewnętrznych
NIE
BRAK
automatyczne
N/d
brak
Logi zmian zewnętrznych
NIE
BRAK
GUI, pliki
online
brak

Komentarze

  1. Skąd ja znam te tabelki :-) system na C..
    P.

    OdpowiedzUsuń
  2. WSZELKIE PODOBIEŃSTWO DO PRAWDZIWYCH PROCESÓW I TABEL JEST PRZYPADKOWE!

    :d

    OdpowiedzUsuń

Prześlij komentarz