Psikutas bez “s” czyli Big Data po mongo(l)sku

https://xpil.eu/TOBFQ

Niech się Czytelnik nie da zwieść głębią tytułu dzisiejszego wpisu, nie o andrologii kynologicznej bowiem dziś będzie, tylko o technologiach przetwarzania danych. A konkretnie - dużych zbiorów danych, o niekoniecznie z góry ustalonej (bądź znanej) strukturze, czyli o tym, co od jakiegoś czasu otrzymało znamienną nazwę "Big Data".

Jak się niektórzy z moich Czytelników być może orientują, ostatnimi czasy tematyka "Big Data" stała się bardzo popularna. W prasie branżowej już od kilku lat można znaleźć mnóstwo odniesień do różnych (nie zawsze udanych) projektów opartych o Big Data. Kilku producentów oprogramowania do przetwarzania danych deklaruje (mniej lub bardziej prawdziwie), że ich produkty obsługują Big Data - i tak dalej.

W związku z tym sam niedawno zainteresowałem się tematem. Na pierwszy (i póki co - jedyny) ogień poszła technologia MongoDB (Mongo czyli Mongoł bez "ł" - stąd właśnie pomysł na ów szalenie zabawny, khem, nieprawdaż, tytuł wpisu).

Czemu akurat Mongo, a nie na przykład Hadoop albo Dynamo? Nie mam pojęcia, pewnie przypadkiem. Tak czy siak, zainteresowałem się MongoDB, zainstalowałem sobie toto ustrojstwo na domowym kompie, pobawiłem się parę dni, potestowałem różne różności - dziś krótko opiszę swoje wrażenia.

Ma się rozumieć, proszę traktować ten wpis cum grano salis - żaden ze mnie fachowiec. Ot, przechodzień, który napatoczył się w okolice Luwru i teraz opowiada znajomym, że widział parę fikuśnych landszaftów.

Mongo jest bazą danych typu NoSQL, a więc zapytania do bazy odbywają się bez magicznego SELECT ... FROM...

Zamiast tego całość komunikacji z serwerem Mongo odbywa się za pomocą składni JSON/BSON (BSON to binarna wersja JSON). Również dane na serwerze są składowane w formacie BSON, dzięki czemu warstwa tłumacząca zapytania na wewnętrzny język struktur danych ma mniej pracy niż w przypadku tradycyjnych silników baz danych. Tak myślę.

Dane w Mongo poukładane są w następującej hierarchii: bazy danych => kolekcje => dokumenty => atrybuty. Kolekcja odpowiada tabeli (tradycyjnie rozumianej), tylko bez ścisłej struktury kolumn, natomiast dokument odpowiada rekordowi w tabeli - z tym, że każdy taki "rekord" w kolekcji może (ale nie musi) mieć inny zestaw "kolumn" (czyli atrybutów). Trochę to na początku przeszkadza, zwłaszcza jak się człowiek przesiada z tradycyjnej, strukturalnej bazy danych - ale przy odpowiednim podejściu do tematu okazuje się to odświeżająco wygodne. W tradycyjnym podejściu, jeżeli mamy tabelę z milionem rekordów historycznych, a potem dodamy do niej kolumnę, musimy albo zaakceptować w tej kolumnie wartości puste (null) albo wypełnić tę kolumnę danymi. A w Mongo, po prostu zaczynamy dopisywać do kolekcji kolejne dokumenty z nowym atrybutem - i nie ma konfliktu, "stare" dokumenty tego atrybutu nie mają, a "nowe" - tak. Proste? Proste...

Największą bolączką tradycyjnych systemów bazodanowych jest - na ogół - wysokowydajna implementacja operatora JOIN. Jeżeli mamy dwie tabele: klienci oraz faktury, faktury zazwyczaj mają kolumnę z identyfikatorem klienta. Żeby wyciągnąć dane z obydwu tabel na raz, trzeba wykonać operacje JOIN między tymi dwiema tabelami. O ile w tym przypadku taki JOIN będzie w miarę prosty (przy założeniu poprawnej strategii indeksowania), o tyle w niektórych bardziej zaawansowanych przypadkach (na przykład, znajdź mi wszystkich klientów, którzy mają wystawione faktury, w których co najmniej jeden towar ma datę produkcji odległą od daty wystawienia faktury o więcej niż siedem miesięcy, ale który jednocześnie nie zawiera w nazwie łańcucha "exp", chyba że faktura jest wystawiona w stanie Ohio) taki JOIN może już być bardzo kosztowny i przy niewłaściwym podejściu może zapchać serwer.

W MongoDB operatora JOIN po prostu nie ma. Powyższy przypadek (klienci - faktury) będzie w Mongo reprezentowany albo w postaci pojedynczej kolekcji Klienci, z zagnieżdżoną w każdym dokumencie tablicą faktur, albo też będą to dwie osobne kolekcje, jednak wówczas wyszukiwanie faktury danego klienta (bądź klientów) będzie odbywało się dwutorowo, po stronie aplikacji: najpierw odczytujemy dokument z kolekcji Klienci, w nim odszukujemy identyfikatory połączonych faktur, i w osobnym zapytaniu odczytujemy te faktury z kolekcji Faktury. Będzie się to odbywało asynchronicznie (co ma swoje wady i zalety), ale nie obciąży serwera operacją JOIN, niezależnie od ilości danych oraz stopnia komplikacji zależności między nimi. Z drugiej strony, znacząco zwiększy ilość zapytań do serwera, jeżeli będziemy chcieli w pętli przelecieć wszystkich klientów (dwuznaczność niezamierzona) i powyciągać ich faktury.

Sam format JSON jest nieco inny od wszystkiego, z czym się dotychczas spotkałem. Może się nieco kojarzyć z XML-em (i, jak się okazuje, jest w pełni przetłumaczalny na XML, w obie strony), jednak jest bardziej odeń zwarty i (moim zdaniem) czytelny.

Najmniejszą, niepodzielną częścią dokumentu JSON jest para atrybut:wartość. W przypadku XML byłoby to prawdopodobnie <atrybut>wartość</atrybut> - jak widać, wersja z JSON jest krótsza. Za pomocą składni JSON przekazujemy zarówno dane jak i parametry zapytań do bazy.

Przykład dokumentu:

{
    _id : 1,
    nazwisko : 'Kowalski',
    imie : 'Jan',
    data_urodzenia : '12 stycznia 1963',
    PESEL : '63011257601'
}

Powyższy dokument mógłby być elementem w kolekcji Osoby. Oto w jaki sposób można by go tam dodać:

db.Osoby.insert({_id : 1,nazwisko : 'Kowalski',imie : 'Jan',data_urodzenia : '12 stycznia 1963',PESEL :'63011257601'}

Możemy też wstawić do kolekcji kilka dokumentów na raz:

db.Osoby.insert(
    [
        {_id : 1,nazwisko : 'Kowalski',imie : 'Jan',data_urodzenia : '12 stycznia 1963',PESEL :'63011257601'},
        {_id : 2,nazwisko : 'Malinowski',imie : 'Albert',data_urodzenia : '21 Września 2008',PESEL :'21090855024'},
        {_id : 3,nazwisko : 'Czechowicz',imie : 'Klementyna',data_urodzenia : '1 czerwca 1926',PESEL :'01062633208'}
    ]
)

Jeżeli nie podamy pola _id, Mongo utworzy to pole za nas i wypełni je unikalnymi wartościami typu OID (96-bitowa liczba, w reprezentacji szesnastkowej 24 znaki hex). Pole _id jest obowiązkowe dla każdego dokumentu i musi być unikalne w obrębie kolekcji. Typ danych tego pola może być dowolny, co więcej, różne dokumenty w obrębie tej samej kolekcji mogą mieć pole _id różnych typów. Ważna jest wyłącznie unikalność.

Jeżeli w powyższym przykładzie kolekcja Osoby nie istnieje, zostanie automatycznie utworzona (w przeciwieństwie do podejścia tradycyjnego, gdzie trzeba najpierw utworzyć tabelę za pomocą CREATE TABLE ... a dopiero potem wypełniać ją danymi).

I jeszcze przykład pokazujący niejednorodność danych w obrębie kolekcji:

Najpierw skasujmy kolekcję Osoby:

db.Osoby.remove()

A następnie:

db.Osoby.insert(
    [
        {_id : 1,nazwisko : 'Kowalski',imie : 'Jan',data_urodzenia : '12 stycznia 1963',PESEL :'63011257601'},
        {_id : 2,nazwisko : 'Malinowski',data_urodzenia : '21 Września 2008',PESEL :'21090855024'},
        {_id : 3,nazwisko : 'Czechowicz',imie : 'Klementyna',PESEL :'01062633208',kolor_wlosow:'czarny'}
    ]
)

Jak widać, tym razem żadne dwa dokumenty w kolekcji Osoby nie mają takiej samej struktury. Nie stanowi to jednak żadnej przeszkody - próba odczytania imienia pana Malinowskiego zwróci NULL, podobnie jak próba odczytania koloru włosów Kowalskiego. Jest to (przynajmniej w teorii) nieco bałaganiarskie, jednak na dłuższą metę bardzo wygodne. Trzeba po prostu zmienić sposób myślenia.

No właśnie - a w jaki sposób zapytać o imię Kowalskiego?

O tak:

db.Osoby.find( { nazwisko : 'Kowalski'}, {imie : 1})

Operator find() przyjmuje dwa parametry (obydwa opcjonalne). Pierwszy to wyrażenie filtrujące (a więc, czego szukamy), a drugie mówi, które atrybuty chcemy wyświetlić na wyjściu. Ponadto, atrybut _id jest domyślnie zawsze włączony, a więc powyższe zapytanie zwróci nam imiona wszystkich osób o nazwisku Kowalski, wraz z ich unikalnymi identyfikatorami. Jeżeli chcemy wyłączyć zwracanie kolumny _id, musimy to explicite wyszczególnić, o tak:

db.Osoby.find( { nazwisko : 'Kowalski'}, {imie : 1, _id : 0})

Rzecz jasna czasem chcielibyśmy przeszukać kolekcję pod kątem więcej niż jednego parametru, na przykład szukamy wszystkich Kowalskich urodzonych 12 stycznia 1963 roku:

db.Osoby.find( { nazwisko : 'Kowalski', data_urodzenia : '12 stycznia 1963'}, {})

Czasem zależy nam na wyszukaniu według zakresu. Na przykład, znajdź wszystkie osoby, które mają co najmniej siedem palców:

db.Osoby.find({ile_palcow : {$gte : 7}}, {})

W przykładzie powyżej wyrażenie filtrujące {$gte : 7} jest samo w sobie dokumentem - w taki sposób możemy konstruować całkiem skomplikowane filtry do wyszukiwania danych. Jednak mają one pewne ograniczenia. Na przykład nie da się wyszukać dokumentów, które mają jakąś tablicę (nie tabelę tylko właśnie tablicę) o ilości elementów z zadanego przedziału. Może znaleźć takie, które mają w tej tablicy jedną, konkretną ilość elementów, ale wg przedziału już się nie da. Mongo jest jednak aktywnie rozwijane i bardzo możliwe, że brakujące obecnie opcje znajdą się niebawem w kolejnych wersjach.

A właśnie, tablice. Jeden przykład już podałem powyżej, wstawiając trzy osoby za pomocą pojedynczej komendy insert(). Tablicę definiujemy za pomocą nawiasów kwadratowych. Możemy mieć na ten przykład tablicę wszystkich krajów, które dana osoba odwiedziła:

{
_id : 1,
nazwisko : 'Kowalski',
imie : 'Jan',
data_urodzenia : '12 stycznia 1963',
PESEL : '63011257601',
kraje : ['PL','HU','GB','IE']
}

Powyższy dokument definiuje osobę, która odwiedziła Polskę, Węgry, Wielką Brytanię oraz Irlandię.

W jaki sposób wyszukać teraz wszystkie osoby, które odwiedziły, dajmy na to, Meksyk?

O tak:

db.Osoby.find({kraje : 'MX'}, {})

A w drugą mańkę? Jak znaleźć wszystkie osoby, które nigdy nie były w Polsce?

db.Osoby.find({kraje : {$nin : 'PL'}}, {})

Takich operatorów jak $nin czy $gte jest całkiem sporo, po szczegóły przekierowuję do dokumentacji Mongo.

Najwięcej problemów stwarza - zaskakująco - nie odpytywanie kolekcji o pasujące dane, tylko stworzenie optymalnego modelu tych danych. Na przykład klasyczna relacja wiele-do-wielu między książkami a autorami. Co lepiej: kolekcja książek z listą autorów w każdym dokumencie reprezentującym książkę, a może na odwrót, lista autorów z wylistowanymi książkami? Podejść jest kilka, żadne nie jest idealne, każde ma swoje wady i zalety. Ja bym prawdopodobnie utworzył osobną, "wąską" kolekcję, w której trzymałbym pary autor_id-książka_id, jednak zrobiłbym tak wyłącznie dlatego, że tak mi podpowiada moje SQL-owe doświadczenie.

Mongo jest od samego początku tworzone z myślą o nadmiarowości danych oraz skalowalności w poziomie, w związku z czym ma wbudowane mechanizmy do łatwego klastrowania oraz definiowania struktur fail-over. Niestety, nie byłem aż tak zajadły, żeby stawiać Mongo na więcej niż jednej maszynie, a więc tematu za bardzo nie zgłębiłem.

Nie opowiedziałem dziś również zbyt wiele o bardziej zaawansowanych aspektach pracy z Mongo - jednak, jak już nadmieniłem na samym początku, jestem tylko przechodniem, który zerknął na to i owo. Fachowcem zostanę (być może!), kiedy zacznę pracować na prawdziwych danych. Póki co tylko obwąchuję zagadnienie 🙂

Aha, najważniejsze na koniec: Mongo jest darmowe. I używane u takich gigantów jak CERN, Cisco, Foursquare czy MTV. Fajnie byłoby kiedyś przesiąść się na takie wielkie dane, jakie mają na przykład w CERN (tam podobno jest osobna farma serwerów, odpowiedzialna wyłącznie za wydajne tworzenie nowych partycji dla strumienia danych generowanych z eksperymentów - no ale co tu się dziwić, 25 petabajtów danych rocznie to całkiem niezła średnia...)

Wszystkich czytelników, którzy tutaj dotarli i jeszcze nie zasnęli, serdecznie pozdrawiam. Pozostałym mówię dobranoc :]

https://xpil.eu/TOBFQ

3 komentarze

  1. E tam Mongoły jakieś 😉

    Dbf! to były bazy danych,a do tego dbase, foxpro, clipper.

    Potem się z konieczności życiowej przerzuciłem na b-drzewa (nie chodzi o brzozy) czeli b-tree. Ale kto to dzisiaj pamięta 😀 i o czym ja w ogóle mówię?!

    1. Ja pamiętam dbase. O dziwo, jeszcze w 2005 roku miałem do czynienia z systemem opartym na dbase, na starym DOS-ie 3.30 bodajże, i za nic w świecie nie dało się go usunąć z firmy i zastąpić nowszym, bo był niesłychanie ważny, chociaż jego producent już dawno nie istniał.

      Co do b-drzew, to ostatni raz słyszałem o nich na studiach. Udało mi się nawet napisać w C++ algorytm równoważący takie b-drzewo, a potem jeszcze prosty (plikowy) silnik bazy danych oparty na b-drzewach. Ale to zamierzchła historia jest.

Leave a Comment

Komentarze mile widziane.

Jeżeli chcesz do komentarza wstawić kod, użyj składni:
[code]
tutaj wstaw swój kod
[/code]

Jeżeli zrobisz literówkę lub zmienisz zdanie, możesz edytować komentarz po jego zatwierdzeniu.