Sharding w bazach danych

Co to jest Sharding?

Sharding jest wzorcem architektonicznym bazy danych związanym z partycjonowaniem horyzontalnym. Praktyka ta polega na rozdystrybuowaniu wierszy w tabeli na różne tabele zwane partycjami. Każda partycja ma ten sam schemat, te same kolumny, ale kompletnie różne wiersze. Dane znajdujące się w jednej partycji są unikalne i niezależne od danych z pozostałych partycji. Każda partycja nazywana jest Shardem, co w tłumaczeniu na polski oznacza odłamek. Poniższy opisuje sposób zarządzania takimi odłamkami bazy danych.

Sharding jest dobrym przykładem implementacji Shared Nothing Architecture.

Różnice między skalowaniem wertykalnym a horyzontalnym

W powyższym akapicie wspominałem o partycjonowaniu, a teraz piszę o skalowaniu. To nie jest to samo. O partycjonowaniu napiszę niżej. Pojęcia horyzontalny i wertykalny bardzo dobrze opisują to, co zamierzamy wdrożyć. Poniższe definicje na pewno pomogą w zrozumieniu zagadnienia.

Skalowanie wertykalne (scale-up) to podejście, w którym próbujemy zwiększać wydajność poprzez zwiększanie możliwości pojedynczej maszyny. Może to być zwiększanie mocy procesora, dodanie większej ilości pamięci lub inne tego typu zabiegi.

Mówiąc o skalowaniu horyzontalnym (scale-out) mamy na myśli dodawanie większej ilości równoległych maszyn czy budowanie klastra. Na każdej maszynie będzie uruchomiony takie samo oprogramowanie. W tym przypadku potrzebujemy dodatkowego mechanizmu, jakim jest load balancer, który zapewni, że każde żądanie trafi w odpowiednie miejsce.

Partycjonowanie wertykalne vs. horyzontalne

Mówiąc o partycjonowaniu mamy na myśli dane, a dokładniej sposób ich ułożenie w sensowny sposób. Z partycjonowaniem na pewno mieliście do czynienia w przypadku podziału dysków w systemie operacyjnym na mniejsze dyski.

Partycjonowanie wertykalne w bazach danych polega na tworzeniu tabel z mniejszą liczbą kolumn i używaniu innych tabel do zapisywania pozostałych danych. Ogólnie takie partycjonowanie polega na rozdzielaniu danych, które mógłby znajdować się w jednej tabeli na kilka innych. Dobrym motywem do wydzielenia może być większa częstotliwość korzystania (odczyt lub zapis) z niektórych danych. Dla przykładu możemy mieć tabelę z imionami i nazwiskami osób (częste wyszukiwania po tych danych) oraz drugą tabelę z ich numerami telefonów, adresem oraz innymi danymi.

Partycjonowanie horyzontalne polega na zapisywaniu różnych wierszy w różnych tabelach tego samego typu. Za przykład posłuży baza danych firm. Schemat bazy danych będzie wyglądał tak samo dla każdej instancji. Różnica będzie w podziale danych. W przypadku firm możemy przyjąć kryterium liczby pracowników np. mniej niż 100 pracowników, 100 – 10000 pracowników oraz powyżej 10000. Na tej podstawie możemy stworzyć 3 bazy danych z takim samym schematem, ale różnymi danymi. Takie partycjonowanie może być przydatne w systemach multitenant, gdzie dodatkową zaletą shardingu będzie brak konieczności tworzenia dodatkowych zabezpieczeń, aby uniknąć wyciekom danych między tenantami.

Zalety Shardingu

  • Mechanizm shardingu pozwala nam na skalowanie horyzontalne,
  • Zwiększenie czasu odpowiedzi zapytań – dzięki podziałowi danych zapytania nie muszą przeglądać wszystkich wierszy w tabeli,
  • Zmniejszenie ryzyka kompletnej awarii systemu – nawet w przypadku gdy któraś z maszyn przestanie działać, pozostałe nadal będą uruchomione. Może nie jest to idealne rozwiązanie, ale lepiej gdy aplikacja działa przynajmniej dla części użytkowników, niż miałaby wcale nie działać.
  • Może redukować koszty. Dużo implementacji tej architektury opiera się o niskokosztowe darmowe bazy danych, które nie wymagają drogiego hardware, aby działać wydajnie.

Wady Shardingu

  • Zaprojektowanie systemu w taki sposób, aby wykorzystać mechanizm shardingu, jest skomplikowane. Nieprawidłowa implementacja może spowodować niespójność, a nawet utratę danych
  • Bazy danych mogą okazać się nieprawidłowo zbalansowane. To oznacza, że niektóre shardy mogą być bardziej eksploatowane niż inne. Mamy wtedy do czynienia z hotspotem.
  • Raz podzielona baza danych może być trudna do przywrócenia gdy jednak zdecydujemy się korzystać z jednej instancji.
  • Nie każda baza danych natywnie wspiera sharding.

Co to hotspot?

Hotspot to shard, który jest używany dużo częściej niż inne. Załóżmy, że w bazie danych mamy tabelę użytkowników i zdecydowaliśmy, że podział będzie odbywał się na podstawie nazwisk. Tak więc mamy dwie grupy: A-M oraz N-Z. Jakimś dziwnym sposobem 3/4 użytkowników ma nazwisko zaczynające się od litery z drugiej grupy. W tym przypadku hotspotem będzie ta instancja, która obsługuję użytkowników N-Z i nie jest to pożądane zjawisko.

Zapisz się na newsletter, aby otrzymywać informacje o nowych artykułach oraz inne dodatki.

Architektura

Key Based Sharding

Inna nazwa dla tego podejścia to hash based sharding. Polega ono na użyciu jakiegoś klucza (ID, IP, kod pocztowy, kod państwa itp) z nowo generowanej wartości i użycia go jako wsad do funkcji generującej hash. Na podstawie tej wygenerowanej wartości podejmowana jest decyzja, który shard ma obsługiwać te dane.

Key Based Sharding
Key Based Sharding

Próby dodania nowej maszyny mogą być nieco skomplikowane. W takim przypadku potrzebujemy wygenerować nowy klucz dla nowego sharda. Może się również okazać, że będziemy musieli przegenerować część lub nawet wszystkie klucze dla wartości z pozostałych shardów oraz odpowiednio te wartości przemigrować.

Zaletą tego podejścia jest równomierne rozłożenie elementów między maszynami, aby uniknąć hotspotów.

Range Based Sharding

To podejście cechuje się podziałem bazującym na pewnych zakresach. Może to być np. cena czy rozmiar.

Range based sharding
Range Based Sharding

Zaletą tego podejścia jest łatwość implementacji. Jeśli z góry wiemy, że mamy ustalenie gdzie trafiają które dane to wystarczy napisać odpowiedni kod, który sprawdzi ten warunek.

Wadą jest możliwość wystąpienia hotspotów. Nie jesteśmy w stanie zapewnić, że nasze zakresy będą równomiernie rozłożone między shardami.

Directory Based Sharding

Aby zaimplementować tę architekturę, najpierw trzeba utworzyć specjalną tabelę, która będzie zawierać klucze. Klucze w tabeli mówią nam, który shard zawiera dane, które nas interesują.

To podejście jest podobne do range based sharding. Różnica jest taka, że zamiast nie musimy ustalać za każdym razem, gdzie trafią dane, tabela z kluczami po prostu nam to “powie”.

Directory Based Sharding
Directory Based Sharding

Dużą zaletą tej architektury jest jej elastyczność. W przeciwieństwie do dwóch wyżej opisanych podejść możemy zastosować dowolny algorytm rozmieszczenia danych. Zwiększenie ilości maszyn również będzie łatwiejsze.

Minusem jest fakt, że przy każdym żądaniu musimy wykonać zapytanie do dodatkowej tabeli. Jeśli okaże się, że została ona nieoptymalnie zbudowana, to przy większych ilościach danych może spowalniać działanie.

Co zoptymalizować, zanim zdecydujesz się na sharding?

Zanim zdecydujesz się wdrożyć sharding w swoim systemie, rozważ poniższe rozwiązania.

  • Zdalna baza danych – jeśli Twój system bazy danych jest zainstalowany na tym samym serwerze co aplikacja, możesz zwiększyć wydajność przez odciążenie maszyny i przeniesienie bazy na zupełnie inną. To nie jest skomplikowana operacja, ale powinna zwiększyć wydajność.
  • Implementacja pamięci podręcznej – implementacja cacheingu może być dobrym pomysłem gdy problemem w Twoim systemie są wolne odczyty.
  • Stworzenie repliki – chodzi po prostu o skopiowanie bazy danych tak, aby równocześnie działało więcej instancji. W tym przypadku mamy różne podejścia: master-slave, jedna instancja do zapisu a druga do odczytu itp.
  • Skalowanie wertykalne – być może dołożenie większej ilości zasobów sprzętowych do maszyny może spowodować zwiększenie wydajności Twojego systemu.

Podsumowanie

Sharding może wprowadzać większą złożoność do systemu oraz tworzyć potencjalne miejsca wytwarzania się błędów. Należy pamiętać, że sharding nie jest natywnie wspierany w każdym systemie baz danych. Mimo to jest to dobry sposób na zwiększenie wydajności.

Warto wspomnieć o architekturze mikroserwisów. Moim zdaniem, zanim zdecydujemy się na zbudowanie architektury mikrousług, warto zastanowić się, czy sharding nie rozwiąże problemów, z którymi będziemy się zderzać.

P.S. Być może zastanawiasz się, czemu na obrazku wyróżniającym dodałem jakiś budynek. Ta wieża wygląda jak odłamek czegoś większego i nazywa się The Shard.

Dziękuję za przeczytanie mojego artykułu. Zapraszam do poczytania innych wpisów oraz do kontaktu bezpośredniego lub na grupie na facebooku.

Migracja danych z PostgresSQL do Neo4j

Często zdarza się, że podczas projektowania aplikacji nie jesteś w stanie przewidzieć jaki model danych będzie najlepszym dla Ciebie. Dopiero w późniejszym etapie wychodzą różne przypadki, które wskazują na użyteczność konkretnego modelu bazy danych. Czasem wygląda to następująco:
– zaczynasz eksperymentować z różnymi modelami danych,
– zespół wybiera jeden z nich,
– pojawia się pierwszy skomplikowany przypadek, ale nadal wierzycie, że wasz wybór jest dobry,
– aplikacja zostaje wgrana na produkcje (zostaje udostępniona dla użytkowników),
– użytkownicy zaczynają ją używać i generują dużo danych,
– zespół dochodzi do wniosku, że aplikacja zaczyna działać coraz wolniej i rozważa pewne modyfikacje,
– zmiany w kodzie nie przynoszą efektów, więc zespół rozważa zmianę bazy danych,
– pojawia się pewien problem: “Co zrobić z istniejącymi danymi i jak przenieść je do nowo wybranej bazy danych?”,

Istnieje spore prawdopodobieństwo, że w Twojej karierze wystąpi powyższy przypadek. Przeważnie migracje danych występują między tymi samymi rodzajami baz danych. Takim przypadkiem może być sytuacja gdy z monolitycznego systemu wydzielamy mikroservice, wtedy migrujemy część bazy danych, z której korzysta wydzielony kod.

Ciekawszym przypadkiem, na jaki można natrafić, jest migracja między różnymi systemami baz danych. W tym poście opiszę migrację z PostgresSQL do Neo4j. Na początku sama myśl o tym, że migracja jest konieczna bylem przerażony, ale po przeczytaniu dokumentacji znacząco zmieniłem swoje zdanie.

Wprowadzenie

Aplikacja z tłumaczeniami

Razem z kolegą zaczęliśmy pracę nad aplikacją, w której użytkownik mógł uczyć się słówek w innych językach. Mając to wymaganie w głowach, zbudowaliśmy elastyczny model bazy danych pozwalający na dodawanie słów i połączeń między nimi bez potrzeby modyfikacji schematu. To wszystko na Postgresie. Dodam, że zaplanowaliśmy możliwość uczenia się więcej niż dwóch języków (na początku było ich 5). W planie mieliśmy połączenie tych wszystkich języków między sobą, aby dodając jedno tłumaczenie, system automatycznie dodawał tłumaczenia z innych języków. Udało się, ale największym problemem było dla nas zrobienie tego poprawnie. Kod stał się bardzo zawiły i każda modyfikacja sprawiała, że czułem się bardzo sfrustrowany.

Połączenia między słowami

Krótko wspomnę o problemie, który popchnął mnie w jednym kierunku – migracja na Neo4j. Załóżmy, że w bazie danych mamy tłumaczenie z języka angielskiego na niemiecki (np. mother -> die Mutter). Teraz chciałbym dodać tłumaczenie z języka polskiego na angielski (matka -> mother). Nasza idea zakładała, że system automatycznie zrozumie, że matka po polsku to die Mutter po niemiecku. Stworzyliśmy specjalny mechanizm, który łączył za nas te dane i nawet działał. Problem zaczął się pojawiać gdy dodaliśmy dwa dodatkowe języki oraz synonimy w każdym z nich. Aplikacja działała wolniej, a w bazie danych, tworzyły się tysiące relacji.

Duża złożoność przy pobieraniu danych

Mając tak skomplikowany model danych, pobieranie informacji z bazy jest dość złożoną operacją. Nasze zapytanie o wszystkie tłumaczenia z jednego języka na drugie wymagało złączenia przynajmniej trzech tabel, z czego niektóre musiały się łączyć same ze sobą. Praktycznie zawsze tak duże zapytania z dużą ilością JOINów sprawiają późniejsze problemy.

Migracja na Neo4j – Lekarstwo na nasze problemy

Kiedy już zacząłem gubić się w swoim własnym kodzie, zaczęliśmy myśleć o kompletnie innej bazie danych. Słowo połączenia, które często się pojawiało, mylnie kierowało nas w kierunku relacyjnej bazy danych. Znacznie szybszym, wygodniejszym i stworzonym do tego typu problemów rozwiązaniem jest Neo4j. W normalnym przypadku ta baza nie jest darmowa do komercyjnego użytku, ale jest to narzędzie darmowe dla startupów oraz do nauki. Mając wyjaśnioną tę sytuację, zabraliśmy się za migrację.

Relacyjna baza danych

Relacyjny model bazy danych

Powyższy schemat prezentuje część naszego modelu bazy danych (tak wiem, schemat nie jest zgodny z żadnym standardem – to rysunek poglądowy). Faktycznie baza jest dużo większa, ale na potrzeby tego posta wystarczy to, co jest na obrazku.

Phrase w tym modelu oznaczał tabelę, w której przechowujemy wszystkie słowa oraz proste zdania w różnych językach. PhraseAssociation jest miejscem, gdzie zapisujemy połączenia między wyrazami. Każde połączenie ma jeden z trzech typów TRANSLATION, SYNONYM lub ANTONYM. Tabela Sentence jest kontenerem na zdania, służące jako przykładowe użycia poszczególnych słów. PhraseSentence jest tabelą łączącą słowa z ich przykładowym użyciem.

W tym momencie wydaje się, że jest to prosty model. Problem pojawia się w momencie gdy do bazy danych dodajemy kolejne wyrazy w różnych językach, przykłady ich użycia i łączymy to wszystko ze sobą. W przypadku pięciu różnych języków aplikacja do jednego dodanego słowa tworzy przynajmniej 5 dodatkowych połączeń. Zrozumienie tych danych po pewnym czasie było nie lada wyzwaniem.

Mając taki model danych, pobieranie przykładowych tłumaczeń (w tym przypadku z polskiego na niemiecki) wymagało napisania kilku JOINów.

SELECT p1.text          as TEXT_FROM,
       l1.lang          as LANG_FROM,
       p2.id            AS ID_TO,
       l2.lang          as LANG_TO
FROM zettelchen_phrase p1
       JOIN zettelchen_language l1 ON p1.lang_id = l1.id
       JOIN zettelchen_phrase_phrase_association zppa1 on p1.id = zppa1.phrase_id
       JOIN zettelchen_phrase_association zpa ON zppa1.phrase_association_id = zpa.id
       JOIN zettelchen_association_type zat ON zat.id = zpa.association_type_id
       JOIN zettelchen_phrase_phrase_association zppa2 ON zppa2.phrase_association_id = zpa.id
       JOIN zettelchen_phrase p2 ON zppa2.phrase_id = p2.id
       JOIN zettelchen_language l2 ON p2.lang_id = l2.id
WHERE p1.id <> p2.id
  AND zat.name = 'TRANSLATION'
  AND (p1.text = '') IS NOT TRUE
  AND (p2.text = '') IS NOT TRUE
  AND (l1.lang = 'pl')
  AND (l2.lang = 'de')

Łączymy tylko 5 tabel, ale przez złożoność aplikacji zapytanie wygląda na dużo większe.

Model grafowy

Sprawdźmy teraz jak wygląda nasz model danych z użyciem grafu.

Model bazy jako graf

Nasz model bazy danych wygląda następująco. Mamy tylko węzły i 4 typy relacji między nimi. Przyznasz, że ten diagram wygląda dużo bardziej zrozumiale niż ten relacyjny? Gdy chcemy pobrać dokładnie te same tłumaczenia co wczesniej, wystarczy, że użyjemy poniższego zapytania.

MATCH (p:Phrase {lang: 'pl'})-[:TRANSLATES]->(p2:Phrase {lang: 'de'}) 
RETURN p.text, p.lang, p2.text, p2.lang

To wszystko, czego potrzebujemy, aby pobrać tłumaczenia! Zabierzmy się zatem za migrację!

Zapisz się na newsletter, aby otrzymywać informacje o nowych artykułach oraz inne dodatki.

Migracja danych krok po kroku

Zarówno nasza aplikacja jak i baza danych jest uruchomiona na dockerze, dlatego niektóre kroki zawierają komendy z dockera.

1. Przygotowanie zapytania SQL do wyciągnięcia danych z Postgresa i export ich do pliku CSV.

COPY (
  SELECT p1.id            AS ID_FROM,
         p1.text          as TEXT_FROM,
         l1.lang          as LANG_FROM,
         p2.id            AS ID_TO,
         p2.text          as TEXT_TO,
         l2.lang          as LANG_TO
  FROM zettelchen_phrase p1
         JOIN zettelchen_language l1 ON p1.lang_id = l1.id
         JOIN zettelchen_phrase_phrase_association zppa1 on p1.id = zppa1.phrase_id
         JOIN zettelchen_phrase_association zpa ON zppa1.phrase_association_id = zpa.id
         JOIN zettelchen_association_type zat ON zat.id = zpa.association_type_id
         JOIN zettelchen_phrase_phrase_association zppa2 ON zppa2.phrase_association_id = zpa.id
         JOIN zettelchen_phrase p2 ON zppa2.phrase_id = p2.id
         JOIN zettelchen_language l2 ON p2.lang_id = l2.id
  WHERE p1.id <> p2.id
    AND zat.name = 'TRANSLATION'
    AND (p1.text = '') IS NOT TRUE
    AND (p2.text = '') IS NOT TRUE
) TO '/tmp/translations.csv' WITH CSV header;

W powyższym zapytaniu ważne są użyte aliasy. Posłużą one jako nagłówki w pliku CSV.

Trzeba pamiętać, że gdy uruchomimy to zapytanie do dockerze, plik zostanie zapisany w kontenerze w podanej lokalizacji.

2. Przygotuj zapytanie CYPHER przy użyciu narzędzia LOAD CSV do importowania danych

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM 'file:/translations.csv' AS row
MERGE (from:Phrase {
  externalId: row.id_from
})
  ON CREATE SET
  from.uuid = randomUUID(),
  from.text = row.text_from,
  from.lang = row.lang_from
MERGE (to:Phrase {
  externalId: row.id_to
})
  ON CREATE SET
  to.uuid = randomUUID(),
  to.text = row.text_to,
  to.lang = row.lang_to
MERGE (from)-[r:TRANSLATES {
  code: row.lang_from + '-' + row.lang_to
}]->(to);

W powyższym skrypcie czytamy wszystkie wiersze z pliku CSV linia po linii, tworząc nowe węzły i połączenia między nimi. W tym miejscu, tworząc właściwości wierszy i relacji, używamy wcześniej przygotowanych aliasów. Plik CSV powinien być umieszczony w katalogu import. Nazwa jest dowolna, ja wybrałem import.cypher.

3. Uruchom skrypt z pierwszego kroku

Aby móc wyeksportować dane, możemy użyć narzędzia psql. Poniższa komenda ładuje podany plik i uruchamia go. Możemy do wywołać z zewnątrz kontenera dockera, ale plik i tak zostanie odłożony wewnątrz niego.

psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USER -d $POSTGRES_DB < export_csv.sql

4. Pobierz wyeksportowany plik z dockera

docker cp $POSTGRES_CONTAINER_ID:/tmp/translations.csv translations.csv

Podpowiedź: Jeśli chcesz dowiedzieć się, jaki jest Twój POSTGRES_CONTAINER_ID użyj poniższej komendy.

docker ps -aqf "name=$POSTGRES_CONTAINER_NAME"

5. Przenieś plik CSV do katalogu import w kontenerze neo4j

docker cp translations.csv $NEO4J_CONTAINER_ID:/import/translations.csv

6. Przenieś plik ze skryptem z kroku 2 do kontenera neo4j ale do katalogu /tmp

docker cp import_csv.cypher $NEO4J_CONTAINER_ID:/tmp/import_csv.cypher

7. Użyj cypher-shell do uruchomienia przygotowanego skryptu – właściwa migracja

Przygotuj skrypt shelowy i przenieś go w to samo miejsce co skrypt z kroku 2.

USERNAME=$1
PASSWORD=$2
cat /tmp/import_csv.cypher | /var/lib/neo4j/bin/cypher-shell -u $USERNAME -p $PASSWORD

Teraz uruchom skrypt na dockerze, korzystając z opcji exec.

docker exec $NEO4J_CONTAINER_NAME /tmp/$YOUR_SHELL_SCRIPT.sh $USERNAME $PASSWORD

Operacja powinna się zakończyć z informacją o sukcesie. W przeciwnym przypadku otrzymasz klarowną informację, co poszło nie tak. Aby zobaczyć więcej szczegółów odnośnie błędów, wystarczy dodać flagę -debug -format verbose do cypher-shell.

8. Usuń tymczasowe pliki

Na koniec warto posprzątać po sobie i usunąć niepotrzebne pliki.

if [ -f translations.csv ] ; then
    rm translations.csv
fi

Podsumowanie

Jak widzisz, migracja danych jest w zasadzie kwestią przygotowania odpowiednich skryptów do wyciągnięcia danych z jednej bazy i wrzucenia na drugą. Dzięki narzędziu LOAD CSV przenoszenie danych jest bardzo proste. Jak już wspominałem w poprzednim poście, Neo4j pozwala na reprezentację prawdziwych relacji między danymi w bardzo prosty sposób. W pewnych przypadkach baza grafowa sprawdza się świetnie w porównaniu do relacyjnej bazy danych. W naszym przypadku migracja okazała się krokiem w dobrym kierunku. Teraz kod aplikacji jest czystszy i łatwiejszy w utrzymaniu. Dzięki algorytmom grafowym, szybkość pobierania danych znacznie się zwiększyła.

Uważam, że podejście grafowe jest bardzo użytecznie i przyjazne, dlatego rekomenduję każdemu przeczytanie jeszcze raz wymagań waszego systemu, ponieważ może i w waszym przypadku Neo4j będzie użyteczne.

Dziękuję za przeczytanie i zapraszam do dyskusji oraz bezpośredniego kontaktu.
Post napisany na podstawie posta z mojego innego bloga oraz własnych doświadczeń.

Neo4j – Wprowadzenie do prawdziwych relacji między danymi

Kiedy pierwszy raz usłyszałem o Neo4j, pomyślałem, że tego typu bazy danych są używane tylko do budowania silników rekomendacji. Moja percepcja zmieniła się kiedy zobaczyłem prezentację o GAAND Stack (GraphQL, Apollo, Angular, Neo4j Database). Podczas tego szkolenia zdałem sobie sprawę, że jest o wiele więcej przypadków użycia tego narzędzia. Zaraz po szkoleniu postanowiłem “zanurkować” nieco głębiej w tę technologię. Okazuje się, że Neo4j to potężne narzędzie między innymi dzięki szybkości oraz sposobowi reprezentacji danych.

Są dwa powody, dla których powstał ten artykuł. Pierwszym jest stworzenie kompletnej instrukcji tworzenia GRAND Stack (to samo co GAAND tylko z React.js zamiast Angulara). Drugim powodem jest ustrukturyzowanie wiedzy o Neo4j jako przygotowanie do profesjonalnego certyfikatu z tej technologii (* już posiadam certyfikat w momencie gdy tłumaczę artykuł z innego mojego bloga).

Co to jest grafowa baza danych?

Krótko mówiąc, grafowa baza danych to baza, która używa grafu do zapisywania danych i połączeń między nimi. Dokładnie tak samo jak w grafie takie bazy danych mają węzły (nodes) oraz krawędzie (edges), które mogą być jednostronnie (unidirectional) lub dwustronnie (bidirectional) skierowane.

Tak ustukturyzowane dane są bardzo łatwe do zrozumienia dla ludzi oraz pozwalają na szybkie wyszukiwanie dużo lepiej niż inne struktury danych.

Struktura bazy danych Neo4j

Przykładowy graf

Co to jest węzeł?

Węzeł (node) jest obiektem który reprezetuje pojedynczą encję. Węzeł może przechowywać dane w postaci właściwości (properties).

Węzeł z Neo4j

Co to jest relacja?

Relacja to niekoniecznie trafne tłumaczenie angielskiego słowa relation, ale chodzi o połączenie między węzłami. W grafie będzie to po prostu krawędź. Jak sama nazwa mówi, jest ona elementem bazy danych reprezentującą związek, jaki zachodzi między dwoma węzłami. Relacja, podobnie jak węzeł, może mieć właściwości oraz musi mieć dokładnie jeden typ.

Relacje w Neo4j

Co to jest typ relacji?

Typ relacji definiuje jaką rolę jeden węzeł pełni względem innego i wyjaśnia, dlaczego dwa węzły są ze sobą połączone.

Typ relacji w Neo4j

Co to jest etykieta?

Etykieta (label) jest używana do przypisywania węzłów do różnych grup. Węzeł może mieć dowolną ilość etykiet.

O etykietach możemy myśleć jak o nazwach tabel z relacyjnej bazy danych takiej jak np. MySQL. Etykiety definiują rodzaje węzłów. W poniższym przypadku mamy dostępne dwa typy: Person oraz Movie.

Etykiety w Neo4j

Co to jest właściwość?

Właściwości są właściwie zwykłymi danymi, które mogą być przechowywane przez węzły lub relacje.

Właściwości w Neo4j

Co to jest traversal?

Używam angielskiej nazwy, bo jej polski odpowiednik – przejście – brzmi trochę śmiesznie. Grafowe bazy danych korzystają z path traversal, aby wykonać zapytanie, o których dane potrzebujemy. Traversing (przechodzenie) grafu oznacza odwiedzanie poszczególnych węzłów, podążając zdefiniowanymi krawędziami. Algorytm przechodzi po krawędziach zgodnie z regułami ustawionymi w zapytaniu.

Co to jest index?

Index – podobnie jak w innych bazach danych – pozwala nam zwiększyć wydajność pobierania danych. Baza danych tworzy kopię danych i zapisuje je w możliwie najbardziej efektywny sposób. Powoduje to większe zużycie pamięci i nieco wolniejsze zapisy danych.

Co to jest constraint?

W bazach danych programiści mogą tworzyć pewne ograniczenia zabezpieczające system przed wprowadzaniem niepoprawnych danych. O to właśnie dbają konstrukcje zwane constraintami. Programista definiuje reguły a baza danych przed zatwierdzeniem danych, sprawdza ich poprawność.

Język zapytań w Neo4j – Cypher

Cypher jest językiem zapytań używanym w Neo4j. Dla osób, które miały okazję korzystać z SQL ten język będzie wyglądał znajomo. Cypher nieco przypomina mi streamy w Javie, ponieważ pisząc zapytania, przypominają one coś w rodzaju strumienia. Czytanie zapytania od lewej do prawej przypomina czytanie zdania w języku naturalnym.

Ten język zapytań używa ASCII-Art do tworzenia wzorców, które czynią Cypher bardziej czytelnym. Po spojrzeniu na kod, od razu wiemy, co jest węzłem, co relacją oraz jak zamierzamy użyć tych informacji.

Podstawowe zapytania

Pobieranie danych

MATCH (actor { name: 'Charlie Sheen' })-[:ACTED_IN]->(movie)<-[:DIRECTED]-(director)
RETURN movie.title, director.name

Powyższy kod “mówi” bazie danych, aby zwróciła tytuły filmów oraz imiona reżyserów, w których jednym z aktorów był Charlie Sheen.

Zauważ, że używamy relacji jednokierunkowej poprzez wpisanie strzałki (-[:RELATION_TYPE]->). Ta strzałka precyzyjnie wyjaśnia związek między węzłami.

Tworzenie węzła

CREATE (a:Artist { Name : "Strapping Young Lad" })

Możemy również tworzyć wiele węzłów naraz – używając jednej komendy – oddzielając je przecinkami

CREATE (a:Album { Name: "Killers"}), (b:Album { Name: "Fear of the Dark"}) 
RETURN a,b

lub przez użycie oddzielnych instrukcji CREATE

CREATE (a:Album { Name: "Piece of Mind"}) 
CREATE (b:Album { Name: "Somewhere in Time"}) 
RETURN a,b

Tworzenie relacji

MATCH (a:Actor),(b:Movie)
WHERE a.Name = "John Tree" AND b.Name = "The neo4j movie"
CREATE (a)-[r:ACTED_IN]->(b)
RETURN r

Jak widać powyżej, do tworzenia związków między węzłami używane jest to samo słowo kluczowe. Jedyne co trzeba dodać to informacje o węzłach, które mają być ze sobą powiązane.

Rożnica w porównaniu do SQL

Jeśli znasz SQL, prawdopodobnie zobaczysz wiele podobieństw między tymi językami zapytań. Klauzule takie jak WHERE, UNION, ORDER BY oraz CREATE istnieją w obydwu językach. Główną różnicą jest brak instrukcji JOIN dzięki temu, że Neo4j jest zbudowana w zupełnie inny sposób niż klasyczne relacyjne bazy danych.

Transakcje w Neo4j

Neo4j wspiera ACID, aby w pełni wspierać integralność danych oraz zapewnić dobre zachowanie transakcji.

Wszystkie operacje na danych takie jak dostęp do grafu, indexów czy schematu powinniśmy wykonywać w transakcji.

Ważne do zapamiętania:

  • Dane pobrane podczas przeglądania grafu nie są z żaden sposób chronione przed modyfikacją przez inną transakcję,
  • Mogę wystąpić niepowtarzalne odczyty (non-repeatable) – podczas transakcji zakładane są tylko blokady zapisu,
  • Istnieje możliwość manualnego założenia blokad na węzły oraz relacje, aby osiągnąć wyższy poziom izolacji,
  • Wykrywanie zakleszczeń (deadlock) jest mechanizmem wbudowanym w systemie zarządzania transakcjami.

Aby przeczytać więcej o transakcjach w Neo4j, odwiedź tę stronę.

Poziom Izolacji

Transakcje w Neo4j używają poziomu READ_COMMITED. To oznacza, że transakcje nie widzą żadnych niezatwierdzonych zmian z innych transakcji. Dodatkowo Java API udostępnia możliwość doprecyzowania blokad na węzłach oraz relacjach. Blokady dają możliwość symulowania wyższych poziomów izolacji poprzez zakładanie i zdejmowanie blokad.

Zapisz się na newsletter, aby otrzymywać informacje o nowych artykułach oraz inne dodatki.

Badanie zapytań w Neo4j

EXPLAIN

Komenda EXPLAIN umożliwia nam sprawdzenie planu wykonania zapytanie bez potrzeby uruchamiania kodu. Aby wykonać plan zapytania, wystarczy poprzedzić nasze zapytanie słowem kluczowym EXPLAIN. Taka konstrukcja zwróci nam pusty wynik i nie spowoduje wprowadzenia żadnych zmian na bazie danych.

Poniżej umieściłem wynik następujacego zapytania

EXPLAIN MATCH p=()-[r:ACTED_IN]->() RETURN p LIMIT 25
Plan zapytania
Plan zapytania w Neo4j

PROFILE

Aby sprawdzić, co w naszym zapytaniu wykonuje większość pracy, możemy użyć komendy PROFILE na początku zapytania. Ta komenda uruchamia zapytanie i śledzi ile wierszy wyników przeszło rzez poszczególne operatory. Dodatkowo sprawdzany jest czas, jaki operator potrzebował na interakcję z bazą danych, aby otrzymać dane.

Przykład:

PROFILE MATCH p=()-[r:ACTED_IN]->() RETURN p LIMIT 25
Plan Profilu

Nazewnictwo w Neo4j

Etykieta węzła

Do nazywania etykiet węzłów używamy CamelCase

Poprawna nazwaNiepoprawna nazwa
VehicleOwnervehicle_owner
NetworkNodenetworkNode

Nazwa relacji/związku

Do nazywania relacji używamy wielkich liter, gdzie słowa oddzielone są od siebie znakiem “podłogi” (underscore)

Poprawna nazwaNiepoprawna nazwa
ACTED_INacted_in
OWNED_BYownedBy

Nazwa właściwości

Do nazywania właściwości używamy loweCamelCase.

Poprawna nazwaNiepoprawna nazwa
firstNamefirst_name
amountOfStudentsAMOUNT_OF_STUDENTS

Porównanie do relacyjnej bazy danych

Zakładając hipotetyczną sytuację, że chcemy przemigrować dane z bazy relacyjnej do Neo4j, musielibyśmy myśleć o poszczególnych wierszach jak o węzłach. Mając tę analogię, nazwa tabeli byłaby etykietą węzła. Właściwości w węźle byłby po prostu danymi z poszczególnych wierszy. Nazwa każdej kolumny z kluczem obcym może być wzięta pod uwagę podczas budowania związków między węzłami.

Protokół komunikacyjny w Neo4j – Bolt

Bolt jest nieustandaryzowanym protokołem open-source stworzonym na potrzeby baz danych. Protokół ten jest zorientowany na komunikaty (znów dziwne tłumaczenieni z statement-oriented). Mówiąc prościej, oznacza to, że klient może wysłać komunikaty zawierające ciągi znaków wraz ze zbiorem parametrów. Serwer będzie odpowiadał wiadomościami oraz opcjonalnym strumieniem danych. Neo4j używa tego protokołu a domyślny port to 7687.

Neo4j Bloom

Bloom jest aplikacją dostępną w Graph Platform, która umożliwia użytkownikowi nawiązać wizualną interakcję z danymi w postaci grafu. W prostych słowach jest to aplikacja internetowa, która wizualnie przedstawia graf, z którym pracujemy.

Bloom

Aby zobaczyć więcej o Neo4j Bloom, zachęcam do obejrzenia poniższego wideo.

Licencja

Są dwa typy licencji. Community jest w pełni działającą bazą danych, która może być używana do projektów open-source, projektów wewnątrz organizacji lub do aplikacji uruchamianych na prywatnych urządzeniach. Enterprise udostępnia większą dostępność oraz skalowalność do komercyjnego użycia.

Baza Neo4j wspiera startupy. Aby otrzymać licencję Enterprise dla startupu, wystarczy dołączyć do programu dla startupów oraz spełnić opisane tam wymagania. Zobaz więcej tutaj.

Podsumowanie

Z mojej perspektywy grafowe bazy danych są idealnym wyborem gdy musimy zamodelować prawdziwe zależności lub jakiekolwiek bardziej złożone związki między obiektami. Wyszukiwanie w grafach jest niesamowicie szybkie, co jest obecnie wielką zaletą. Struktura danych w grafie jest o wiele łatwiejsza do wyobrażenia niż w jakiejkolwiek dokumentowej czy tabelarycznej bazie danych.

W Neo4j bardzo lubię sposób operowania na danych. Cypher jest intuicyjnym językiem, który dokładnie pokazuje, co chcemy zrobić. Cypher swoją czytelność zawdzięcza użyciu ASCII-Art. Ten język jest czysty, a napisany w nim kod możemy czytać jak zwykłe zdanie. Dodatkowo zachwycony jestem narzędziem Bloom. Dzięki idealnej wizualizacji grafów oraz dobremu interfejsowi praca z nim jest intuicyjna i przyjemna.

Neo4j nie jest najtańszym narzędziem, ale w przypadku gdy szybkość pobierania danych ma znaczenie, może okazać się idealnym wyborem. Dzięki temu, że bazy grafowe są elastyczne i łatwe w utrzymaniu, dają nieproporcjonalnie dużo zalet w niektórych typach projektów.

Aby podsumować, chciałbym wszystkim polecić Neo4j jako bazę danych do wszystkich projektów, które mogłyby skorzystać z dobrodziejstwa path traversal i innych algorytmów grafowych, jak również z elastycznych relacji i modelowania danych.

Artykuł napisany na podstawie mojego bloga EagerToIt oraz własnych doświadczeń. Zapraszam do kontaktu i dyskusji.