Wprowadzenie
„Musisz biec najszybciej, jak tylko możesz, aby tylko pozostać w tym samym miejscu,
a żeby gdzieś dotrzeć, musisz biec przynajmniej dwa razy szybciej!”
(c) Alicja w Krainie Czarów
Kilka czasów temu poproszono mnie o wygłoszenie wykładu analitykom naszej firmy na temat projektowania modeli danych, ponieważ siedząc długo na projektach (czasem przez kilka lat) umykają nam zdarzenia wokół w świecie technologii IT. W naszej firmie (tak się złożyło) w wielu projektach nie są używane bazy danych NoSQL (przynajmniej na razie), dlatego w swoim wykładzie szczególnie poświęciłem im trochę uwagi na przykładzie HBase i starałem się dostosować materiał do tych, którzy nigdy z nimi nie pracowali. W szczególności ilustrowałem niektóre cechy projektowania modelu danych na przykładzie, który kilka lat temu przeczytałem, . Analizując przykłady, porównywałem kilka rozwiązań tego samego zadania, aby lepiej przekazać słuchaczom główne pomysły.
Ostatnio, „z nudów”, zadałem sobie pytanie (długie majowe weekendy w trybie kwarantanny do tego szczególnie sprzyjają), na ile teoretyczne wywody będą odpowiadać praktyce? Właściwie w ten sposób narodził się pomysł tego artykułu. Programista, który niejedno już widział w NoSQL, może nie wynieść z niego nic nowego (więc może od razu przewinąć pół artykułu). Ale dla analityków, którzy jeszcze nie pracowali intensywnie z NoSQL, sądzę, że będzie to przydatne do zdobycia podstawowych pojęć o cechach projektowania modeli danych dla HBase.
Analiza przykładu
Moim zdaniem, zanim zaczniemy korzystać z baz danych NoSQL, należy dobrze przemyśleć i zważyć "za" i "przeciw". Często zadanie można rozwiązać także przy użyciu tradycyjnych relacyjnych baz danych. Dlatego lepiej unikać stosowania NoSQL bez istotnych powodów. Jeśli jednak podjęto decyzję o użyciu bazy danych NoSQL, należy wziąć pod uwagę, że podejścia do projektowania są tutaj nieco inne. Zwłaszcza niektóre z nich mogą być nieznane tym, którzy mieli do czynienia tylko z relacyjnymi bazami danych (z moich obserwacji). Tak więc, w "relacyjnym" świecie zazwyczaj zaczynamy od modelowania obszaru zadania, a dopiero potem, w razie potrzeby, przeprowadzamy denormalizację modelu. W NoSQL natomiast musimy od razu uwzględniać przewidywane scenariusze pracy z danymi i początkowo denormalizować dane. Ponadto istnieje wiele innych różnic, o których będzie mowa poniżej.
Rozważmy następujące "syntetyczne" zadanie, z którym będziemy dalej pracować:
Należy zaprojektować strukturę przechowywania listy znajomych użytkowników pewnej abstrakcyjnej sieci społecznościowej. Dla uproszczenia przyjmijmy, że wszystkie połączenia są kierunkowe (jak na Instagramie, a nie na LinkedIn). Struktura powinna umożliwiać efektywne:
- Odpowiadanie na pytanie, czy użytkownik A obserwuje użytkownika B (szablon odczytu)
- Umożliwianie dodawania/usuwania połączeń w przypadku subskrypcji/odsubskrypcji użytkownika A od użytkownika B (szablon zmiany danych)
Oczywiście istnieje wiele możliwości rozwiązania problemu. W zwykłej relacyjnej bazie danych prawdopodobnie po prostu stworzyliśmy by tabelę połączeń (możliwe, że typowaną, jeśli na przykład wymagane jest przechowywanie grupy użytkowników: rodzina, praca itp., do której należy ten "znajomy"), a dla optymalizacji szybkości dostępu dodalibyśmy indeksy/partycjonowanie. Prawdopodobnie tabela końcowa wyglądałaby mniej więcej tak:
user_id
friend_id
Wania
Piotr
Wania
Ola
tutaj i dalej dla przejrzystości i lepszego zrozumienia zamiast ID podam imiona
W przypadku HBase wiemy, że:
- efektywne wyszukiwanie, które nie prowadzi do pełnego skanowania tabeli, jest możliwe wyłącznie po kluczu
- Właśnie dlatego pisanie typowych zapytań SQL do takich baz danych to zła idea; technicznie rzecz biorąc, możesz wysłać zapytanie SQL z joinami i inną logiką do HBase z Impali, ale jak efektywne to będzie…
Dlatego ID użytkownika musimy użyć jako klucz. A pierwszą myślą na temat „gdzie i jak przechowywać ID przyjaciół?” może być pomysł przechowywania ich w kolumnach. Ta najbardziej oczywista i „naiwna” wersja będzie wyglądać mniej więcej tak (nazwijmy to Opcja 1 (domyślna), aby móc odwoływać się do niej później):
RowKey
Kolumny
Wania
1: Piotr
2: Ola
3: Daria
Piotr
1: Maria
2: Wacław
Każdy wiersz odpowiada jednemu użytkownikowi sieci. Kolumny mają nazwy: 1, 2, … — odpowiadające liczbie przyjaciół, a w kolumnach przechowywane są ID przyjaciół. Ważne jest zauważenie, że każda linia ma różną liczbę kolumn. Na rysunku powyżej jedna linia ma trzy kolumny (1, 2 i 3), a druga tylko dwie (1 i 2) – tutaj skorzystaliśmy z dwóch cech HBase, których nie ma w relacyjnych bazach danych:
- możliwości dynamicznego zmieniania struktury kolumn (dodajemy przyjaciela → dodajemy kolumnę, usuwamy przyjaciela → usuwamy kolumnę)
- różne wiersze mogą mieć różny skład kolumn
Sprawdźmy naszą strukturę pod kątem wymagań zadania:
- Odczyt danych: aby zrozumieć, czy Wacław jest subskrybowany przez Olę, musimy odczytać cały wiersz za pomocą klucza RowKey = „Wacław” i przeszukiwać wartości kolumn, aż natrafimy na Olę. Możemy również przeszukać wszystkie wartości kolumn, „nie znalezienie” Oli i zwrócenie odpowiedzi False;
- Zmiana danych: dodanie przyjaciela: do tego zadania również będziemy musieli odczytać cały wiersz za pomocą klucza RowKey = „Wacław”, aby policzyć całkowitą liczbę jego przyjaciół. Ta całkowita liczba przyjaciół jest nam potrzebna, aby określić numer kolumny, do której należy zapisać ID nowego przyjaciela.
- Zmiana danych: usunięcie przyjaciela:
- Musimy odczytać cały wiersz za pomocą klucza RowKey = „Wacław” i przeszukiwać kolumny, aby znaleźć tę, w której zapisany jest usuwany przyjaciel;
- Następnie musimy, po usunięciu przyjaciela, „przesunąć” wszystkie dane o jedną kolumnę, aby uniknąć „przerw” w ich numeracji.
Teraz ocenimy, jak wydajne będą algorytmy, które musimy zaimplementować po stronie „hypotetycznej aplikacji”, korzystając z Oznaczmy rozmiar naszej hipotetycznej sieci społecznej jako n. Wtedy maksymalna liczba przyjaciół jednego użytkownika może wynosić (n-1). Tego (-1) możemy w dalszej kolejności zignorować dla naszych celów, ponieważ w ramach użycia O-symboliki jest to nieistotne.
- Odczyt danych: należy przeczytać cały wiersz i przeanalizować wszystkie jego kolumny w granicach. Zatem górna ocena kosztów będzie wynosić około O(n)
- Zmiana danych: dodanie przyjaciela: aby określić liczbę przyjaciół, należy przejrzeć wszystkie kolumny wiersza, po czym dodać nową kolumnę => O(n)
- Zmiana danych: usunięcie przyjaciela:
- Podobnie jak przy dodaniu – trzeba w granicach przejrzeć wszystkie kolumny => O(n)
- Po usunięciu kolumn musimy je 'przesunąć'. Jeśli będziemy to realizować 'na sztywno', to w granicach będzie potrzebne jeszcze do (n-1) operacji. Ale tutaj i w dalszej części praktycznej aplikacji zastosujemy inny podejście, które będzie realizować 'pseudo-przesunięcie' przy stałej liczbie operacji – to znaczy będzie to zajmować stały czas niezależnie od n. Tego stałego czasu (jeśli być dokładnym, to O(2)) w porównaniu do O(n) można zignorować. Podejście jest zilustrowane na poniższym rysunku: po prostu kopiujemy dane z 'ostatniej' kolumny do tej, z której należy usunąć dane, po czym usuwamy ostatnią kolumnę:

Podsumowując, we wszystkich scenariuszach uzyskaliśmy asymptotyczną złożoność obliczeniową O(n).
Prawdopodobnie zauważyliście już, że niemal zawsze musimy odczytywać z bazy całą linię, a w dwóch na trzy przypadki tylko po to, aby przejrzeć wszystkie kolumny i policzyć całkowitą liczbę przyjaciół. Dlatego jako próbę optymalizacji można dodać kolumnę 'count', w której będzie przechowywać łączną liczbę przyjaciół każdego użytkownika sieci. W takim przypadku możemy nie odczytywać całej linii dla policzenia łącznej liczby przyjaciół, a przeczytać tylko jedną kolumnę 'count'. Najważniejsze, aby nie zapomnieć zaktualizować 'count' podczas manipulacji danymi. Otrzymujemy w ten sposób poprawioną Opcja 2 (count):
RowKey
Kolumny
Wania
1: Piotr
2: Ola
3: Daria
liczba: 3
Piotr
1: Maria
2: Wacław
liczba: 2
W porównaniu do pierwszej opcji:
- Odczyt danych: aby uzyskać odpowiedź na pytanie 'Czy Wania czyta Olę?' nic się nie zmieniło => O(n)
- Zmiana danych: dodanie przyjaciela: Uprościliśmy dodawanie nowego przyjaciela, ponieważ teraz nie musimy przeszukiwać całego wiersza i sprawdzać jego kolumn, a możemy po prostu uzyskać wartość kolumny „count” i tym samym od razu określić numer kolumny do dodania nowego przyjaciela. Prowadzi to do zmniejszenia złożoności obliczeniowej do O(1)
- Zmiana danych: usunięcie przyjaciela: Przy usuwaniu przyjaciela również możemy skorzystać z tej kolumny, aby zmniejszyć liczbę operacji wejścia/wyjścia przy „przesuwaniu” danych o jedną komórkę w lewo. Niemniej jednak konieczność przeszukiwania kolumn w celu znalezienia kolumny, którą należy usunąć, wciąż pozostaje, dlatego => O(n)
- Z drugiej strony, teraz przy aktualizacji danych musimy za każdym razem również aktualizować kolumnę „count”, ale zajmuje to stały czas, którym w ramach złożoności O możemy zignorować
Ogólnie wariant 2 wydaje się nieco bardziej optymalny, ale to raczej „ewolucja zamiast rewolucji”. Aby przeprowadzić „rewolucję”, będziemy potrzebować Wariant 3 (col).
Odwróćmy wszystko „do góry nogami”: przypiszemy nazwę kolumny jako identyfikator użytkownika! To, co zostanie zapisane w samej kolumnie, nie ma dla nas istotnego znaczenia, niech będzie liczba 1 (w ogóle można tam przechowywać np. grupę „rodzina/przyjaciele/itp.”). To podejście może zaskoczyć nieprzygotowanego „przeciętnego” użytkownika, który wcześniej nie miał do czynienia z bazami danych NoSQL, ale to właśnie ono pozwala wykorzystać potencjał HBase w tym zadaniu znacznie efektywniej:
RowKey
Kolumny
Wania
Piotr: 1
Ola: 1
Daria: 1
Piotr
Marta: 1
Witek: 1
Tutaj zyskujemy od razu kilka korzyści. Aby je zrozumieć, przeanalizujmy nową strukturę i oceńmy złożoność obliczeniową:
- Odczyt danych: aby odpowiedzieć na pytanie, czy Witek subskrybuje Olę, wystarczy przeczytać jedną kolumnę „Ola”: jeśli ona istnieje, to odpowiedź to True, jeśli nie – False => O(1)
- Zmiana danych: dodanie przyjaciela: Dodanie przyjaciela: wystarczy po prostu dodać nową kolumnę „ID przyjaciela” => O(1)
- Zmiana danych: usunięcie przyjaciela: wystarczy po prostu usunąć kolumnę „ID przyjaciela” => O(1)
Jak widać, istotną zaletą tego modelu przechowywania jest to, że w wszystkich niezbędnych scenariuszach operujemy tylko jedną kolumną, unikając odczytywania całego wiersza z bazy, a tym bardziej przeszukiwania wszystkich kolumn tego wiersza. Na tym można by było zakończyć, ale…
Można zająć się tym i pójść jeszcze dalej w kierunku optymalizacji wydajności oraz ograniczenia operacji wejścia-wyjścia podczas dostępu do bazy danych. Co jeśli przechowamy pełne informacje o relacji bezpośrednio w samym kluczu wiersza? To znaczy, zrobimy klucz złożony w formie userID.friendID? W takim przypadku nie musimy nawet odczytywać kolumn wiersza (Wariant 4(row)):
RowKey
Kolumny
Wania.Petya
Piotr: 1
Wania.Olia
Ola: 1
Wania.Dasha
Daria: 1
Petya.Masha
Marta: 1
Petya.Wania
Witek: 1
Oczywiście, że ocenianie wszystkich scenariuszy manipulacji danymi w tej strukturze, tak jak w poprzednim wariancie, będzie O(1). Różnica z wariantem 3 będzie ograniczona wyłącznie do efektywności operacji wejścia-wyjścia w bazie danych.
No i ostatni 'bąbelek'. Łatwo zauważyć, że w wariancie 4 klucz wiersza będzie miał zmienną długość, co może wpłynąć na wydajność (przypominamy, że HBase przechowuje dane jako zestaw bajtów, a wiersze w tabelach są uporządkowane według klucza). Ponadto mamy separator, który w niektórych scenariuszach może być problematyczny. Aby wyeliminować ten wpływ, można użyć hashy od userID i friendID, a ponieważ obie hashe będą mieć stałą długość, można je po prostu skonkatenować, bez separatora. Wtedy dane w tabeli będą wyglądać tak (Wariant 5(hash)):
RowKey
Kolumny
dc084ef00e94aef49be885f9b01f51c01918fa783851db0dc1f72f83d33a5994
Piotr: 1
dc084ef00e94aef49be885f9b01f51c0f06b7714b5ba522c3cf51328b66fe28a
Ola: 1
dc084ef00e94aef49be885f9b01f51c00d2c2e5d69df6b238754f650d56c896a
Daria: 1
1918fa783851db0dc1f72f83d33a59949ee3309645bd2c0775899fca14f311e1
Marta: 1
1918fa783851db0dc1f72f83d33a5994dc084ef00e94aef49be885f9b01f51c0
Witek: 1
Oczywiście, że złożoność obliczeniowa pracy z taką strukturą w naszych rozważanych scenariuszach będzie taka sama jak w wariancie 4 – czyli O(1).
W sumie, zebrano wszystkie nasze oceny złożoności obliczeniowej w jedną tabelę:
Dodawanie przyjaciela
Sprawdzanie przyjaciela
Usuwanie przyjaciela
Opcja 1 (domyślna)
O(n)
O(n)
O(n)
Wariant 2 (liczba)
O(1)
O(n)
O(n)
Wariant 3 (kolumna)
O(1)
O(1)
O(1)
Wariant 4 (wiersz)
O(1)
O(1)
O(1)
Wariant 5 (hash)
O(1)
O(1)
O(1)
Jak widać, opcje 3-5 wydają się być najbardziej preferowane i teoretycznie zapewniają realizację wszystkich niezbędnych scenariuszy manipulacji danymi w stałym czasie. W warunkach naszego zadania nie ma wyraźnego wymogu uzyskania listy wszystkich przyjaciół użytkownika, ale w rzeczywistej działalności projektowej, jako dobrzy analitycy, powinniśmy 'przewidzieć', że takie zadanie może się pojawić i 'podłożyć słomkę'. Dlatego moje sympatie są po stronie opcji 3. Jednak może się zdarzyć, że w prawdziwym projekcie to zapytanie mogło zostać już rozwiązane innymi środkami, więc bez ogólnego widzenia całego zadania lepiej nie wyciągać ostatecznych wniosków.
Przygotowanie eksperymentu
Powyższe teoretyczne rozważania chciałbym zweryfikować w praktyce – to było celem pomysłu, który pojawił się podczas długiego weekendu. Aby to zrobić, należy ocenić prędkość działania naszego 'warunkowego aplikacji' w wszystkich opisanych scenariuszach użycia bazy, a także wzrost tego czasu wraz ze wzrostem rozmiaru sieci społecznościowej (n). Parametrem, który nas interesuje i który będziemy mierzyć w trakcie eksperymentu, jest czas, jaki 'warunkowa aplikacja' potrzebuje na wykonanie jednej 'operacji biznesowej'. Przez 'operację biznesową' rozumiemy jedną z następujących:
- Dodanie jednego nowego przyjaciela
- Sprawdzenie, czy użytkownik A jest przyjacielem użytkownika B
- Usunięcie jednego przyjaciela
W ten sposób, biorąc pod uwagę wymagania określone w pierwotnym zadaniu, scenariusz weryfikacji przedstawia się następująco:
- Zapis danychWygenerować losowo początkową sieć o rozmiarze n. Aby lepiej przybliżyć 'rzeczywisty świat', liczba przyjaciół każdego użytkownika – to również losowa wielkość. Zmierzyć czas, w jakim nasze 'warunkowe aplikacja' zapisze w HBase wszystkie wygenerowane dane. Następnie otrzymany czas podzielić przez całkowitą liczbę dodanych przyjaciół – w ten sposób uzyskamy średni czas na jedną 'operację biznesową'.
- Odczyt danych. Dla każdego użytkownika należy sporządzić listę «osób», dla których trzeba otrzymać odpowiedź, czy użytkownik jest na nich zapisany, czy nie. Długość listy = około liczby przyjaciół użytkownika, przy czym dla połowy sprawdzanych przyjaciół odpowiedź powinna być „Tak”, a dla drugiej połowy – „Nie”. Sprawdzenie przeprowadza się w taki sposób, aby odpowiedzi „Tak” i „Nie” się przeplatały (to znaczy w każdym drugim przypadku będziemy musieli przeszukać wszystkie kolumny wiersza dla wariantów 1 i 2). Całkowity czas sprawdzenia następnie należy podzielić przez liczbę sprawdzanych przyjaciół, aby uzyskać średni czas sprawdzenia jednego podmiotu.
- Usuwanie danych. Usunąć u użytkownika wszystkich przyjaciół. Przy czym kolejność usuwania – losowa (to znaczy „mieszamy” początkową listę, która była używana do zapisywania danych). Całkowity czas sprawdzenia należy następnie podzielić przez liczbę usuwanych przyjaciół, aby uzyskać średni czas na jedną kontrolę.
Scenariusze należy przeprowadzić dla każdego z 5 wariantów modeli danych oraz dla różnych rozmiarów sieci społecznościowej, aby zobaczyć, jak zmienia się czas wraz z jej wzrostem. W ramach jednego n połączeń w sieci lista użytkowników do sprawdzenia powinna być, naturalnie, taka sama dla wszystkich 5 wariantów.
Dla lepszego zrozumienia poniżej przedstawiam przykład wygenerowanych danych dla n= 5. Napisany „generator” wydaje na wyjściu trzy słowniki ID:
- pierwszy – do wstawiania
- drugi – do sprawdzania
- trzeci – do usuwania
{0: [1], 1: [4, 5, 3, 2, 1], 2: [1, 2], 3: [2, 4, 1, 5, 3], 4: [2, 1]} # łącznie 15 przyjaciół
{0: [1, 10800], 1: [5, 10800, 2, 10801, 4, 10802], 2: [1, 10800], 3: [3, 10800, 1, 10801, 5, 10802], 4: [2, 10800]} # łącznie 18 sprawdzanych podmiotów
{0: [1], 1: [1, 3, 2, 5, 4], 2: [1, 2], 3: [4, 1, 2, 3, 5], 4: [1, 2]} # łącznie 15 przyjaciół
Jak można zauważyć, wszystkie ID większe niż 10 000 w słowniku do sprawdzania – to właśnie te, które z góry dadzą odpowiedź False. Wstawianie, sprawdzanie i usuwanie „przyjaciół” odbywa się dokładnie w wskazanej kolejności w słowniku.
Eksperyment przeprowadzono na laptopie działającym na Windows 10, gdzie w jednym kontenerze dokera była uruchomiona baza HBase, a w innym – Python z Jupyter Notebook. Dokkerowi przypisano 2 rdzenie CPU oraz 2 GB pamięci operacyjnej. Cała logika, zarówno emulacja działania „warunkowej aplikacji”, jak i „otoczka” do generowania danych testowych oraz pomiaru czasu została napisana w Pythonie. Do pracy z HBase użyto biblioteki , do obliczeń skrótów (MD5) dla wariantu 5 — hashlib
Biorąc pod uwagę moc obliczeniową konkretnego laptopa, eksperymentalnie wybrano uruchomienie dla n = 10, 30, … 170 – kiedy całkowity czas pracy pełnego cyklu testowania (wszystkie scenariusze dla wszystkich wariantów dla wszystkich n) był jeszcze w miarę rozsądny i mieścił się w czasie jednego popołudniowego herbatki (średnio 15 minut).
Tutaj należy zaznaczyć, że w tym eksperymencie oceniamy przede wszystkim nie absolutne liczby wydajności. Nawet względne porównanie dwóch różnych wariantów może być nie do końca poprawne. Obecnie interesuje nas przede wszystkim charakter zmiany czasu w zależności od n, ponieważ biorąc pod uwagę ww. konfigurację 'stołu testowego', uzyskanie oszacowań czasowych 'oczyszczonych' z wpływu czynników losowych i innych jest bardzo trudne (a nawet nie stawiano takiego celu).
Wynik eksperymentu
Pierwszy test – jak zmienia się czas poświęcony na wypełnienie listy znajomych. Wynik – na poniższym wykresie.

Warianty 3-5 przewidywalnie pokazują praktycznie stały czas 'operacji biznesowej', który nie zależy od wzrostu wielkości sieci i nie różni się znacząco pod względem wydajności.
Wariant 2 również pokazuje stałą, ale nieco gorszą wydajność, przy czym praktycznie dokładnie o 2 razy w porównaniu do wariantów 3-5. I to nie może nie cieszyć, gdyż ma to odniesienie do teorii – w tym wariancie liczba operacji wejścia-wyjścia do/z HBase jest dokładnie 2 razy większa. To może służyć jako pośredni dowód, że nasz stół testowy generalnie daje przyzwoitą dokładność.
Wariant 1 również przewidywalnie okazuje się najwolniejszy i wykazuje liniowy wzrost czasu w zależności od wielkości sieci, poświęcanego na dodawanie jednego znajomego.
Przyjrzyjmy się teraz wynikom drugiego testu.

Opcje 3-5 znowu zachowują się zgodnie z oczekiwaniami – czas jest stały, niezależny od rozmiaru sieci. Opcje 1 i 2 pokazują liniowy wzrost czasu wraz ze wzrostem rozmiaru sieci i podobną wydajność. Przy czym opcja 2 okazuje się nieco wolniejsza – najwyraźniej z powodu konieczności odczytu i przetwarzania dodatkowej kolumny „count”, co staje się bardziej zauważalne przy wzroście n. Niemniej jednak wstrzymam się od jakichkolwiek wniosków, ponieważ dokładność tego porównania jest stosunkowo niska. Ponadto te relacje (która opcja, 1 czy 2, jest szybsza) zmieniały się z uruchomienia na uruchomienie (przy zachowaniu charakteru zależności i „idąc równo”).
A tutaj ostatni wykres – wynik testowania usuwania.

Znowu bez niespodzianek. Opcje 3-5 dokonują usunięcia w stałym czasie.
Co ciekawe, opcje 4 i 5, w przeciwieństwie do poprzednich scenariuszy, pokazują nieco gorszą wydajność niż opcja 3. Najwyraźniej operacja usunięcia wiersza jest bardziej kosztowna niż operacja usunięcia kolumny, co w sumie ma sens.
Opcje 1 i 2, zgodnie z oczekiwaniami, demonstrują liniowy wzrost czasu. Opcja 2 jest przy tym stabilnie wolniejsza od opcji 1 – z powodu dodatkowej operacji wejścia/wyjścia związanej z „obsługą” kolumny count.
Ogólne wnioski z eksperymentu:
- Opcje 3-5 wykazują większą efektywność, ponieważ korzystają z zalet HBase; przy tym ich wydajność różni się między sobą o stałą wartość i nie zależy od rozmiaru sieci.
- Różnica między opcjami 4 i 5 nie została stwierdzona. Ale to nie oznacza, że opcja 5 nie powinna być używana. Jest całkiem możliwe, że zastosowany scenariusz eksperymentu z uwagi na parametry techniczne testowego stanowiska nie pozwolił jej ujawnić.
- Charakter wzrostu czasu potrzebnego na realizację „operacji biznesowych” z danymi w dużej mierze potwierdził wcześniej uzyskane teoretyczne wnioski dla wszystkich opcji.
Epilog
Przeprowadzane surowe eksperymenty nie powinny być postrzegane jako absolutna prawda. Istnieje wiele czynników, które nie zostały uwzględnione i wpływały na wyniki (zwłaszcza dobrze te fluktuacje widoczne są na wykresach przy małej wielkości sieci). Na przykład, prędkość działania thrift, który jest używany przez happybase, objętość i sposób realizacji logiki, którą napisałem w Pythonie (nie twierdzę, że kod został napisany optymalnie i efektywnie wykorzystywał możliwości wszystkich komponentów), być może cechy buforowania HBase, tło aktywności Windows 10 na moim laptopie itd. Ogólnie można uznać, że wszystkie teoretyczne wywody eksperymentalnie pokazały swoją słuszność. Przynajmniej nie udało się ich obalić takim „szturmem na wprost”.
Na zakończenie - rekomendacje dla wszystkich, którzy dopiero zaczynają projektować modele danych w HBase: abstrahujcie od wcześniejszego doświadczenia pracy z relacyjnymi bazami i pamiętajcie o „przykazaniach”:
- Podczas projektowania wychodzimy od zadania i wzorców manipulacji danymi, a nie od modelu obszaru tematycznego.
- Efektywny dostęp (bez pełnego skanowania tabeli) – tylko po kluczu.
- Denormalizacja.
- Różne wiersze mogą zawierać różne kolumny.
- Dynamiczny skład kolumn.
Źródło: habr.com

