MongoDB – jak rozpocząć pracę z nierelacyjnymi bazami danych
Znasz SQL i chcesz poznać MongoDB? Zobacz, jak pracować z dokumentami i kolekcjami, modelować relacje oraz wykonywać operacje CRUD. Poznaj rolę indeksów i agregacji, a także sprawdź, kiedy baza nierelacyjna będzie dobrym wyborem.
MongoDB dla osób znających SQL: podstawowe różnice i mapowanie pojęć
Znajomość SQL daje dobry punkt wyjścia do pracy z MongoDB: nadal potrzebujesz filtrowania, sortowania, indeksów i kontroli poprawności danych. Zmienia się jednak model, w którym te zadania realizujesz. MongoDB jest bazą dokumentową — dane zapisuje w dokumentach BSON, czyli binarnym formacie zbliżonym do JSON, obsługującym dodatkowe typy danych. To nie tylko inna składnia zapytań, lecz także inny sposób organizowania informacji.
Na początek wystarczy orientacyjna mapa pojęć: tabeli odpowiada kolekcja, wierszowi — dokument, a kolumnie — pole. Pole _id pełni funkcję unikalnego identyfikatora dokumentu, zbliżoną do klucza głównego. Są to jednak analogie ułatwiające naukę, a nie ścisłe odpowiedniki. Przeniesienie struktury relacyjnej jeden do jednego do MongoDB nie musi być dobrym rozwiązaniem.
Najważniejsza zmiana dotyczy punktu wyjścia do projektowania bazy. W podejściu relacyjnym często rozdzielasz informacje między znormalizowane tabele i łączysz je podczas odczytu. W MongoDB większą rolę odgrywa pytanie: jakie dane aplikacja zwykle odczytuje i zapisuje razem? Dokument może zawierać zagnieżdżone struktury i tablice, dzięki czemu część informacji da się pobrać bez łączenia osobnych zbiorów. Nie oznacza to jednak, że wszystkie dane należy przechowywać w jednym dokumencie.
Inaczej wygląda również komunikacja z bazą. W typowej pracy z MongoDB zamiast instrukcji SQL korzystasz z MongoDB Query Language (MQL) oraz metod udostępnianych przez sterownik wybranego języka programowania. Znane operacje — wybór pól, filtrowanie wyników czy ich sortowanie — pozostają dostępne, ale zapisujesz je inaczej. MongoDB umożliwia też łączenie danych i obsługuje transakcje obejmujące wiele dokumentów. Określenie „nierelacyjna” nie oznacza więc ani zakazu tworzenia powiązań, ani braku transakcyjności.
MongoDB warto rozważyć między innymi w katalogach produktów o zróżnicowanych atrybutach, systemach zarządzania treścią oraz aplikacjach operujących na rozbudowanych obiektach. Model dokumentowy może w takich przypadkach lepiej odpowiadać strukturze danych używanej przez aplikację. Nie zwalnia jednak z ustalenia zasad ich zapisu i kontroli jakości.
MongoDB nie jest z założenia szybszym ani prostszym zamiennikiem bazy SQL. Jeśli system opiera się na licznych powiązaniach, ograniczeniach integralności referencyjnej i częstych zapytaniach przekrojowych, model relacyjny może być bardziej naturalny. O wyborze powinny decydować struktura danych, sposób dostępu do nich oraz wymagania dotyczące spójności — nie sama etykieta SQL lub NoSQL.
Dokumenty i kolekcje: odpowiedniki w świecie relacyjnym
Podstawową jednostką danych w MongoDB jest dokument, a zbiór dokumentów tworzy kolekcję. Dla osoby pracującej wcześniej z SQL najbliższymi odpowiednikami będą wiersz i tabela. To przydatne porównanie na początek, ale nie pełna zgodność: dokument może przechowywać bardziej złożoną strukturę niż pojedynczy rekord w typowym modelu relacyjnym. W Cognity często słyszymy pytania, jak praktycznie przejść od tabel i wierszy do dokumentów i kolekcji — odpowiadamy na nie także na blogu.
Dokument — więcej niż wiersz
Dokument składa się z pól i przypisanych im wartości. Pole można porównać do kolumny, jednak jego wartością nie musi być wyłącznie tekst, liczba czy data. MongoDB obsługuje również tablice oraz zagnieżdżone dokumenty. Dzięki temu dokument opisujący produkt może zawierać zarówno nazwę i cenę, jak i listę dostępnych kolorów czy zestaw parametrów technicznych.
Dokumenty są zapisywane w formacie BSON, czyli binarnej reprezentacji danych zbliżonej do JSON, lecz obsługującej dodatkowe typy, między innymi daty i identyfikatory ObjectId. W zwykłej kolekcji każdy dokument ma pole _id, które pełni funkcję unikalnego identyfikatora — podobną do klucza głównego. Jeśli podczas dodawania dokumentu nie podasz jego wartości, zazwyczaj zostanie ona wygenerowana automatycznie jako ObjectId.
Kolekcja — zbiór dokumentów o wspólnym przeznaczeniu
Kolekcja porządkuje dokumenty w obrębie bazy danych. Może gromadzić na przykład produkty, zamówienia albo zdarzenia rejestrowane przez aplikację. Podobnie jak tabela ma własną nazwę i stanowi miejsce, do którego aplikacja kieruje operacje odczytu oraz zapisu. Nie jest jednak tabelą o z góry ustalonym zestawie kolumn: jej dokumenty nie muszą zawierać identycznych pól, o ile nie narzucono odpowiednich reguł walidacji.
Praktyczny układ jest więc prosty: baza danych zawiera kolekcje, kolekcje zawierają dokumenty, a dokumenty — pola z wartościami. Takie uporządkowanie dobrze pasuje do danych opisujących konkretne obiekty aplikacji, zwłaszcza gdy mają one listy właściwości lub wielopoziomową strukturę. Nie oznacza to jednak, że całą zawartość relacyjnej tabeli należy zamienić w jeden dokument. Dokument reprezentuje pojedynczy obiekt lub logiczną całość, a kolekcja grupuje wiele takich elementów.
Elastyczny schemat: jak działa, zalety, ryzyka i walidacja
W MongoDB dodanie nowego pola do dokumentu zwykle nie wymaga wcześniejszej zmiany definicji kolekcji. Dokumenty mogą mieć różne zestawy pól, a bez odpowiednich ograniczeń także różne typy wartości pod tą samą nazwą. Elastyczny schemat nie oznacza jednak, że dane nie mają struktury — oznacza, że nie trzeba jej w całości narzucać na poziomie bazy. Nadal wynika ona z potrzeb aplikacji i powinna być świadomie kontrolowana.
W typowej tabeli SQL zestaw kolumn i ich typy określa się przed zapisem danych, choć relacyjne bazy również udostępniają mechanizmy elastyczności, takie jak kolumny JSON. W MongoDB można zacząć od wspólnego minimum pól i stopniowo rozszerzać strukturę. Jeśli nowe dokumenty otrzymają pole description, starsze nie zyskają go automatycznie. Aplikacja musi więc poprawnie obsługiwać jego brak.
Kiedy elastyczność jest zaletą
Takie podejście sprawdza się tam, gdzie dane są niejednorodne lub ich zakres często się zmienia. W katalogu produktów inne atrybuty opisują książkę, a inne urządzenie elektroniczne. Podobnie zdarzenia rejestrowane przez aplikację mogą mieć wspólne pola identyfikujące typ i czas wystąpienia, lecz różnić się pozostałą zawartością.
Elastyczność ułatwia również stopniowe wdrażanie zmian: nowa wersja aplikacji może zacząć zapisywać dodatkowe, opcjonalne pole, zanim wszystkie starsze dokumenty zostaną uzupełnione. Nie każda zmiana wymaga migracji całego zbioru, ale każda wymaga oceny zgodności z kodem odczytującym dane. Zmiana znaczenia pola lub jego typu nadal może wymusić aktualizację aplikacji i przekształcenie istniejących dokumentów.
Ryzyka: niespójność zamiast swobody
Bez ustalonych reguł łatwo zapisać tę samą informację na kilka sposobów: datę jako tekst i jako wartość BSON Date, liczbę jako tekst albo pole pod dwiema nazwami różniącymi się literówką. Takie rozbieżności utrudniają filtrowanie, porównywanie wartości i raportowanie. Zwiększają też liczbę wyjątków, które trzeba obsługiwać w aplikacji.
Istotne jest również rozróżnienie między brakiem pola a wartością null. Pole nieobecne w dokumencie nie jest tym samym co pole zapisane z pustą wartością. Warto zawczasu ustalić, co każdy z tych stanów oznacza, zamiast pozostawiać interpretację poszczególnym fragmentom kodu.
Walidacja: kontrola tam, gdzie jest potrzebna
MongoDB pozwala przypisać kolekcji reguły walidacji, między innymi za pomocą operatora $jsonSchema. Można określić pola obowiązkowe, dozwolone typy BSON, zakresy liczb czy zamknięte listy wartości. Dzięki temu część dokumentu pozostaje elastyczna, a kluczowe dane podlegają kontroli przy zapisie.
Poniższy przykład wymaga niepustej nazwy produktu. Pole stock jest opcjonalne, ale jeśli występuje, musi zawierać nieujemną liczbę całkowitą typu BSON int:
db.createCollection("products", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name"],
properties: {
name: { bsonType: "string", minLength: 1 },
stock: { bsonType: "int", minimum: 0 }
}
}
},
validationLevel: "strict",
validationAction: "error"
})Reguły nie zabraniają tutaj dodatkowych pól. Samo umieszczenie pola w properties również nie czyni go obowiązkowym — odpowiada za to lista required. Walidacja sprawdza dane, ale nie uzupełnia brakujących wartości ani nie konwertuje automatycznie tekstu na liczbę.
Ustawienie strict obejmuje walidacją wszystkie operacje wstawiania i aktualizacji, a error powoduje odrzucenie zapisu naruszającego reguły. Podczas porządkowania istniejącej kolekcji można rozważyć validationAction: "warn": zapis zostanie dopuszczony, lecz naruszenie trafi do logu. Dodanie walidatora nie naprawia ani nie sprawdza automatycznie wszystkich wcześniej zapisanych dokumentów — ich zgodność trzeba zweryfikować osobno.
Praktycznym punktem wyjścia jest walidowanie stabilnego rdzenia danych: pól niezbędnych aplikacji, ich typów i dopuszczalnych wartości. Pozostałe atrybuty mogą zachować elastyczność. To pozwala rozwijać strukturę bez rezygnowania z przewidywalności danych.
Modelowanie danych: embedded vs referenced, relacje i typowe wzorce
Projektowanie modelu w MongoDB warto zacząć od pytania: które dane aplikacja będzie najczęściej odczytywać i zmieniać razem? Odpowiedź pomaga wybrać między osadzaniem danych w jednym dokumencie (embedded) a przechowywaniem ich osobno i łączeniem za pomocą identyfikatorów (referenced). W praktyce oba podejścia często występują w tej samej aplikacji. W Cognity omawiamy ten wybór zarówno od strony technicznej, jak i praktycznej — odnosząc go do sposobów odczytu i aktualizacji danych w aplikacjach, z którymi pracują uczestnicy.
Osadzanie danych — gdy informacje tworzą jedną całość
Osadzanie polega na umieszczeniu powiązanych danych wewnątrz dokumentu, jako zagnieżdżonego obiektu lub tablicy obiektów. Przykładem jest zamówienie zawierające pozycje zakupowe i adres dostawy. Jeśli aplikacja zwykle potrzebuje całego zamówienia, taki model pozwala pobrać je bez dodatkowego odczytywania powiązanych dokumentów.
Embedded sprawdza się, gdy dane mają wspólny cykl życia, są odczytywane razem, a ich rozmiar pozostaje przewidywalny. Dodatkową zaletą jest atomowość zapisu w obrębie jednego dokumentu: zmiana kilku jego pól w jednej operacji zostaje zastosowana w całości albo wcale.
Nie należy jednak osadzać wszystkiego. Dokument BSON ma limit rozmiaru 16 MiB, a stale powiększana tablica może stać się problemem znacznie wcześniej. Przechowywanie całej historii aktywności użytkownika w jego profilu to ryzykowny wybór — liczba zdarzeń może rosnąć bez wyraźnej granicy.
Referencje — gdy dane żyją niezależnie
W modelu referencyjnym powiązane informacje znajdują się w osobnych dokumentach. Zamówienie może na przykład zawierać identyfikator klienta zamiast kopii całego jego profilu. To dobry wybór, gdy ten sam obiekt jest wykorzystywany w wielu miejscach, często się zmienia lub powinien być zarządzany niezależnie.
Referencje ograniczają powielanie danych, ale zwiększają koszt odtworzenia pełnego obrazu: aplikacja musi pobrać powiązane dokumenty albo skorzystać z mechanizmu łączenia danych po stronie bazy. Samo zapisanie identyfikatora nie zapewnia integralności referencyjnej znanej z kluczy obcych SQL. Trzeba więc ustalić, jak aplikacja będzie reagować na usunięcie powiązanego obiektu i jak zadba o spójność zmian obejmujących kilka dokumentów.
| Kryterium | Osadzanie | Referencje |
|---|---|---|
| Sposób odczytu | Dane zwykle potrzebne razem | Dane często pobierane niezależnie |
| Liczba powiązanych elementów | Niewielka lub ograniczona | Duża albo stale rosnąca |
| Współdzielenie danych | Dane należą głównie do jednego obiektu | Jeden obiekt jest używany w wielu miejscach |
| Aktualizacje | Wspólna zmiana w jednym dokumencie | Niezależna zmiana danych bez aktualizowania ich kopii |
Relacje: liczebność to dopiero początek decyzji
Relacja jeden do jednego często nadaje się do osadzenia, o ile obie części są używane razem. Przy relacji jeden do wielu znaczenie ma przede wszystkim skala: kilka adresów użytkownika można przechowywać w jego dokumencie, lecz jego zamówienia zwykle lepiej wydzielić i wskazywać w nich identyfikator użytkownika.
Relacje wiele do wielu najczęściej opierają się na referencjach. Jeżeli samo powiązanie ma własne dane, takie jak data dołączenia czy poziom uprawnień, warto rozważyć osobną kolekcję opisującą te powiązania. Nie trzeba przy tym utrzymywać tablic identyfikatorów po obu stronach relacji — taka redundancja oznacza dodatkową pracę przy synchronizacji.
Typowe wzorce: świadome łączenie obu podejść
- Migawka danych: zamówienie przechowuje identyfikator produktu oraz jego nazwę i cenę z chwili zakupu. Późniejsza zmiana katalogu nie powinna zmieniać historii transakcji.
- Podzbiór danych: dokument produktu zawiera kilka ostatnich opinii, a pełny zbiór opinii znajduje się osobno. Widok produktu pozostaje lekki, lecz wymaga ustalonego sposobu odświeżania osadzonego podzbioru.
- Grupowanie w porcje: pomiary lub zdarzenia są gromadzone w dokumentach obejmujących ograniczony przedział czasu albo określoną liczbę elementów, zamiast trafiać do jednej nieustannie rosnącej tablicy.
Każdą celową kopię danych warto sklasyfikować: czy ma odzwierciedlać stan bieżący, czy zachowywać stan historyczny? W pierwszym przypadku potrzebna jest strategia synchronizacji; w drugim brak aktualizacji może być właśnie oczekiwanym zachowaniem modelu.
Operacje CRUD w MongoDB z przykładami (insert, find, update, delete)
CRUD obejmuje cztery podstawowe działania na danych: tworzenie, odczyt, aktualizację i usuwanie. W MongoDB wykonujesz je za pomocą metod kolekcji, przekazując dokumenty opisujące dane, warunki wyszukiwania lub zmiany. Poniższe przykłady są przeznaczone do uruchomienia w mongosh, czyli powłoce MongoDB. Korzystają z kolekcji produkty w aktualnie wybranej bazie.
Insert: dodawanie dokumentów
Metoda insertOne() dodaje pojedynczy dokument, a insertMany() przyjmuje tablicę dokumentów. To odpowiedniki operacji realizowanych w SQL za pomocą INSERT.
db.produkty.insertOne({
sku: 'KLA-001',
nazwa: 'Klawiatura',
cena: 199,
stan: 15
})Jeśli nie podasz pola _id, zostanie ono automatycznie uzupełnione identyfikatorem typu ObjectId. Wynik operacji zawiera między innymi insertedId, czyli identyfikator dodanego dokumentu. Warto go zachować, jeśli później chcesz jednoznacznie wskazać ten sam rekord.
Find: wyszukiwanie i wybór zwracanych pól
Metoda find() służy do wyszukiwania dokumentów. Pierwszy argument jest filtrem — pełni funkcję zbliżoną do klauzuli WHERE. Drugi, opcjonalny argument to projekcja, która określa, jakie pola mają znaleźć się w wyniku.
db.produkty.find(
{ cena: { $gte: 100 }, stan: { $gt: 0 } },
{ _id: 0, nazwa: 1, cena: 1 }
).sort({ cena: 1 }).limit(10)Zapytanie wybiera produkty o cenie co najmniej 100 i stanie większym od zera. Warunki podane obok siebie są domyślnie łączone operatorem logicznym AND. Wynik zawiera tylko nazwę oraz cenę, jest uporządkowany rosnąco według ceny i ograniczony do dziesięciu dokumentów. W projekcji wartość 1 oznacza uwzględnienie pola; zapis _id: 0 wyłącza identyfikator, który domyślnie jest zwracany.
find() zwraca kursor, a nie gotową tablicę. Gdy potrzebujesz pojedynczego dokumentu, użyj findOne({ sku: 'KLA-001' }). Jeśli nic nie pasuje do filtra, ta metoda zwróci null.
Update: zmiana wybranych wartości
Metoda updateOne() aktualizuje jeden dokument pasujący do filtra. Z kolei updateMany() zmienia wszystkie dopasowane dokumenty. Zakres modyfikacji określasz za pomocą operatorów: $set ustawia wartość pola, a $inc zwiększa lub zmniejsza wartość liczbową.
db.produkty.updateOne(
{ sku: 'KLA-001', stan: { $gt: 0 } },
{
$set: { cena: 179 },
$inc: { stan: -1 }
}
)Ta operacja ustawia cenę na 179 i zmniejsza stan o jeden, ale tylko wtedy, gdy stan jest dodatni. Pozostałe pola pozostają bez zmian. Modyfikacja pojedynczego dokumentu jest atomowa: oba działania zostaną zastosowane jako jedna operacja.
W odpowiedzi sprawdź matchedCount i modifiedCount — liczbę dokumentów dopasowanych oraz faktycznie zmienionych. Dopasowanie nie zawsze oznacza zmianę, na przykład gdy ustawiasz wartość identyczną z obecną. Domyślnie brak dopasowania nie powoduje dodania dokumentu; takie zachowanie można włączyć opcją upsert: true.
Delete: usuwanie z kontrolą zakresu
Metoda deleteOne() usuwa jeden pasujący dokument, a deleteMany() — wszystkie dokumenty spełniające warunek. Wynik zawiera pole deletedCount, które informuje, ile dokumentów usunięto.
db.produkty.deleteOne({ sku: 'KLA-001' })Przed usuwaniem większej liczby dokumentów sprawdź filtr za pomocą find() lub countDocuments(). Pusty filtr {} pasuje do wszystkich dokumentów, dlatego deleteMany({}) usuwa całą zawartość kolekcji, choć nie usuwa samej kolekcji. Metody z końcówką One ograniczają działanie do jednego dokumentu, ale nie gwarantują unikalności filtra — gdy cel ma być jednoznaczny, użyj jego _id.
Indeksy w MongoDB: rodzaje, projektowanie i wpływ na wydajność
Indeksy w MongoDB pełnią podobną funkcję jak w bazach SQL: pozwalają odnaleźć potrzebne dane bez przeglądania całej kolekcji. Mogą również przyspieszać sortowanie, a w niektórych przypadkach umożliwiają obsłużenie zapytania bez odczytywania dokumentów. Najważniejsza zasada pozostaje taka sama: indeks projektuje się pod konkretne zapytania, nie pod samą obecność pola w danych.
Przy braku odpowiedniego indeksu MongoDB może wykonać pełne skanowanie kolekcji. Dla niewielkiego zbioru bywa to wystarczające, ale wraz ze wzrostem liczby dokumentów koszt takiej operacji zazwyczaj rośnie. W standardowej kolekcji MongoDB automatycznie tworzy unikalny indeks pola _id; indeksy potrzebne do pozostałych sposobów wyszukiwania trzeba zaplanować osobno.
Podstawowe rodzaje indeksów i ich zastosowania
W porównaniu z typowym zastosowaniem indeksów w SQL MongoDB oferuje wygodną obsługę pól zagnieżdżonych i tablic. Można indeksować pojedyncze pole wewnątrz dokumentu, wskazując jego ścieżkę, np. address.city, zamiast indeksować cały obiekt.
| Rodzaj indeksu | Kiedy jest przydatny | Istotna cecha |
|---|---|---|
| Jednopolowy | Wyszukiwanie lub sortowanie według jednego pola. | Najprostszy punkt wyjścia dla często używanego warunku. |
| Złożony | Zapytania filtrujące i sortujące według kilku pól. | Kolejność pól decyduje o tym, które zapytania indeks obsłuży efektywnie. |
| Multikey | Wyszukiwanie po wartościach przechowywanych w tablicach. | MongoDB automatycznie traktuje indeks jako multikey, gdy indeksowane pole zawiera tablicę. Indeks złożony ma ograniczenia dotyczące indeksowania kilku pól tablicowych. |
| Tekstowy | Podstawowe wyszukiwanie słów w treści. | Nie zastępuje rozbudowanego mechanizmu wyszukiwania, takiego jak MongoDB Search. |
| Geoprzestrzenny | Wyszukiwanie obiektów w pobliżu lub w określonym obszarze. | Indeks 2dsphere służy do zapytań uwzględniających geometrię powierzchni Ziemi. |
| Haszowany | Między innymi rozkład danych przy shardingu opartym na haszowanym kluczu. | Wspiera dopasowania równościowe, ale nie zapewnia uporządkowania oryginalnych wartości dla zapytań zakresowych. |
| Wildcard | Zapytania po zmiennych lub nieznanych wcześniej ścieżkach pól. | Przydatny przy dynamicznej strukturze danych, lecz nie zastępuje dobrze dobranych indeksów dla znanych zapytań. |
Osobno warto rozróżnić właściwości i specjalne zastosowania indeksów. Indeks unikalny wymusza niepowtarzalność klucza, a częściowy obejmuje tylko dokumenty spełniające zadany warunek. Indeks TTL pozwala automatycznie usuwać dokumenty po upływie określonego czasu, np. wygasłe sesje. Usuwanie odbywa się w tle, więc TTL nie gwarantuje zniknięcia danych dokładnie w chwili wygaśnięcia.
Jak dobierać kolejność pól w indeksie złożonym
Załóżmy, że aplikacja regularnie pobiera zamówienia o określonym statusie, zaczynając od najnowszych. Odpowiednim kandydatem jest indeks:
db.orders.createIndex({ status: 1, createdAt: -1 })Pole status pozwala zawęzić wyniki przez dopasowanie równościowe, a createdAt określa porządek dat w obrębie danego statusu. Wartości 1 i -1 oznaczają odpowiednio porządek rosnący i malejący.
Pomocnym punktem wyjścia jest reguła ESR: Equality, Sort, Range — najpierw pola z warunkami równościowymi, następnie sortowanie, a na końcu zakresy. Nie jest to jednak reguła bez wyjątków: bardzo selektywny warunek zakresowy może uzasadniać inną kolejność. Indeksy złożone najłatwiej wykorzystać przez ich początkowe pola, czyli prefiksy. Powyższy indeks może wspierać filtrowanie po samym statusie, ale nie zapewni analogicznej korzyści przy sortowaniu całej kolekcji wyłącznie po dacie.
Wpływ indeksów na wydajność i sposób weryfikacji
Szybszy odczyt ma koszt. Każdy indeks zajmuje miejsce na dysku i konkuruje o pamięć podręczną. Wstawianie i usuwanie dokumentów oraz aktualizacje indeksowanych wartości wymagają utrzymania odpowiednich wpisów. Nadmiar indeksów może więc spowolnić zapis, nawet jeśli pojedyncze zapytania odczytujące działają szybciej.
Skuteczność indeksu sprawdzaj na reprezentatywnych danych za pomocą explain("executionStats"). Zwróć uwagę na plan wykonania oraz wartości nReturned, totalKeysExamined i totalDocsExamined. Samo wystąpienie skanowania indeksu, np. etapu IXSCAN, nie oznacza jeszcze dobrego wyniku: zapytanie może nadal przeglądać bardzo wiele kluczy i dokumentów.
Przy projektowaniu uwzględniaj częstotliwość zapytań, liczbę dopasowań i wymagane sortowanie. Pole o niewielu różnych wartościach nie zawsze jest dobrym samodzielnym kandydatem, ale może być użyteczne w indeksie złożonym lub częściowym. Najlepszym kryterium pozostaje mierzalna poprawa działania rzeczywistych zapytań przy akceptowalnym koszcie zapisu i przechowywania.
Agregacje (Aggregation Pipeline): grupowanie, filtrowanie i transformacje danych
Ile wyniosła sprzedaż w poszczególnych miesiącach? Które kategorie produktów przynoszą największy przychód? Jaka jest średnia wartość zamówienia dla wybranej grupy klientów? W MongoDB na takie pytania odpowiada Aggregation Pipeline, czyli mechanizm przetwarzania dokumentów w kolejnych etapach. Pozwala obliczać statystyki i przygotowywać zestawienia bez przesyłania wszystkich danych do aplikacji.
Osobom znającym SQL agregacje będą kojarzyć się z zapytaniami wykorzystującymi WHERE, GROUP BY, HAVING i ORDER BY. Różnica polega na sposobie zapisu operacji: w MongoDB budujesz potok, w którym wynik jednego etapu staje się wejściem następnego. Możesz najpierw wybrać zamówienia z określonego okresu, następnie pogrupować je według kategorii, obliczyć przychód i uporządkować wyniki.
Podstawowe etapy potoku agregacji
Na początek warto poznać kilka etapów, z których można zbudować wiele użytecznych raportów:
- $match — filtruje dokumenty według wskazanych warunków. Przed grupowaniem pełni funkcję zbliżoną do WHERE, a po grupowaniu pozwala filtrować obliczone wyniki, podobnie jak HAVING.
- $group — grupuje dane według wybranego klucza, na przykład kategorii produktu lub miesiąca. W połączeniu z operatorami takimi jak $sum i $avg umożliwia obliczanie sum oraz średnich dla każdej grupy.
- $project — określa postać dokumentów wynikowych: wybiera pola, pomija zbędne informacje i pozwala tworzyć pola obliczane.
- $unwind — rozwija tablicę, tworząc osobny dokument dla każdego jej elementu. Przydaje się na przykład wtedy, gdy analizujesz pozycje zamówień zamiast całych zamówień.
- $sort i $limit — odpowiednio porządkują wyniki i ograniczają ich liczbę. Razem pozwalają przygotować ranking, na przykład dziesięciu kategorii o najwyższej sprzedaży.
Dlaczego kolejność etapów ma znaczenie?
Potok nie jest zbiorem niezależnych poleceń. Każdy etap pracuje na danych przekazanych przez poprzedni, dlatego zmiana kolejności może zmienić znaczenie wyniku. Ograniczenie liczby dokumentów przed obliczeniem sumy oznacza analizę tylko części danych. Z kolei ograniczenie wyników po grupowaniu i sortowaniu pozwala wybrać najwyższe wartości z pełnego zestawienia. Sam etap $group nie gwarantuje uporządkowania wyników.
Filtrowanie warto umieszczać możliwie wcześnie, o ile warunki dotyczą pól dostępnych już na wejściu. Dzięki temu kolejne etapy przetwarzają mniej dokumentów. Warunek dotyczący obliczonego przychodu musi jednak pojawić się dopiero po etapie, który ten przychód wyznacza.
Transformacje danych bez zmiany źródła
Agregacje służą nie tylko do grupowania. Pozwalają również wykonywać obliczenia na polach, przekształcać daty i teksty oraz dostosowywać strukturę wyniku do raportu lub odpowiedzi API. Typowy potok zwraca przetworzone dane, ale nie zmienia dokumentów źródłowych. Zapis wyniku do kolekcji wymaga użycia przeznaczonego do tego etapu, takiego jak $merge lub $out.
Przy projektowaniu agregacji najpierw ustal, co ma reprezentować pojedynczy dokument wynikowy: klienta, kategorię, miesiąc czy pozycję zamówienia. To ułatwia dobranie klucza grupowania i pomaga uniknąć błędów obliczeniowych — szczególnie po rozwinięciu tablic, gdy wartości dotyczące całego zamówienia mogą wystąpić w wielu dokumentach.
Kiedy MongoDB ma sens, a kiedy lepiej wybrać relacyjną bazę danych
O wyborze bazy danych powinien decydować przede wszystkim sposób, w jaki aplikacja odczytuje i zmienia informacje. MongoDB warto rozważyć, gdy dane tworzą naturalne, samodzielne całości, a aplikacja najczęściej operuje właśnie na nich. Baza relacyjna zwykle lepiej pasuje do systemów, w których najważniejsze są liczne powiązania między encjami, reguły integralności oraz swobodne zestawianie danych z różnych obszarów.
Kiedy warto wybrać MongoDB
Dobrym przykładem jest katalog produktów, w którym poszczególne kategorie mają odmienne zestawy cech. Podobnie bywa w systemach zarządzania treścią: artykuł, galeria i materiał wideo mają część wspólnych informacji, ale różnią się pozostałą zawartością. MongoDB może dobrze odpowiadać takim potrzebom, szczególnie gdy aplikacja pobiera kompletny produkt lub materiał do wyświetlenia, zamiast za każdym razem zestawiać wiele niezależnych zbiorów.
To również kandydat do przechowywania profili użytkowników, konfiguracji czy danych zdarzeniowych o zróżnicowanej strukturze. Sama różnorodność danych nie przesądza jednak o wyborze. Istotne jest to, czy sposób ich przechowywania odpowiada najczęstszym operacjom aplikacji. Jeżeli większość odczytów dotyczy konkretnego obiektu, model dokumentowy może uprościć pracę programistów.
Kiedy baza relacyjna będzie lepszym punktem wyjścia
PostgreSQL, MySQL lub inny system relacyjny warto rozważyć w pierwszej kolejności, gdy poprawność danych zależy od wielu wzajemnych zależności. Dotyczy to między innymi księgowości, rozliczeń i gospodarki magazynowej. W takich zastosowaniach duże znaczenie mają mechanizmy pilnujące powiązań między rekordami oraz możliwość wygodnego wykonywania operacji obejmujących wiele tabel.
Model relacyjny często okazuje się też praktyczniejszy, gdy użytkownicy potrzebują przekrojowych raportów, a pytania analityczne zmieniają się niezależnie od działania aplikacji. SQL i dojrzałe integracje z narzędziami raportowymi ułatwiają pracę zespołom, które regularnie łączą dane o klientach, zamówieniach, płatnościach i innych procesach.
Nie oznacza to, że MongoDB nie obsługuje transakcji lub łączenia danych — obsługuje oba mechanizmy. Z kolei bazy relacyjne potrafią przechowywać i przetwarzać JSON. Różnica nie sprowadza się więc do listy dostępnych funkcji, lecz do tego, w którym rozwiązaniu wymagania będą prostsze i tańsze do realizacji.
Jak sprawdzić wybór przed wdrożeniem
Zamiast kierować się hasłem „NoSQL lepiej się skaluje”, przetestuj reprezentatywny fragment aplikacji: najczęstszy odczyt, krytyczny zapis oraz trudny raport. Oceń nie tylko czas wykonania, ale też ilość logiki potrzebnej do utrzymania poprawności danych, koszty infrastruktury i kompetencje zespołu. W Cognity łączymy teorię z praktyką — dlatego zagadnienia doboru bazy danych rozwijamy także w formie ćwiczeń na szkoleniach.
Jeżeli obecna baza relacyjna spełnia wymagania, sama popularność MongoDB nie jest powodem do migracji. Nową technologię warto wprowadzić wtedy, gdy rozwiązuje konkretny problem i daje korzyść większą niż koszt jej utrzymania.
Najczęściej zadawane pytania i odpowiedzi odnośnie MongoDB – jak rozpocząć pracę z nierelacyjnymi bazami danych
Zacznij od modelu dokumentowego, a następnie przećwicz podstawowe operacje CRUD w mongosh. Kolekcję możesz początkowo porównywać do tabeli, dokument do wiersza, a pole do kolumny. Nie przenoś jednak automatycznie całej struktury relacyjnej. Dobrym pierwszym ćwiczeniem jest kolekcja produktów:
- dodaj dokument za pomocą
insertOne(); - wyszukaj go przez
findOne(); - zmień wybrane pole przez
updateOne(); - usuń dokument metodą
deleteOne().
MongoDB warto wybrać, gdy dane tworzą samodzielne obiekty, na których aplikacja najczęściej pracuje jako na całości. Takim przypadkiem może być katalog produktów o różnych atrybutach lub system zarządzania treścią. Baza relacyjna bywa praktyczniejsza przy licznych powiązaniach, wymaganiach integralności referencyjnej i przekrojowych raportach. Przed decyzją przetestuj typowy odczyt, krytyczny zapis i trudne zapytanie raportowe, zamiast zakładać przewagę wydajnościową MongoDB.
MongoDB nie wymaga sztywnego zestawu pól w kolekcji, ale struktura danych powinna być świadomie zaplanowana. Dokumenty mogą się różnić, dlatego aplikacja musi obsługiwać brak opcjonalnych pól. Stabilne wymagania można egzekwować walidatorem $jsonSchema, określając pola obowiązkowe, typy BSON i dopuszczalne wartości. Walidacja nie uzupełnia braków ani nie konwertuje typów. Jej dodanie nie naprawia również wcześniej zapisanych dokumentów.
Osadzaj dane używane razem i mające ograniczony rozmiar, a referencje stosuj do obiektów niezależnych lub współdzielonych. Pozycje zamówienia mogą należeć do jednego dokumentu, natomiast stale rosnąca historia aktywności użytkownika zwykle wymaga osobnej kolekcji. Przy wyborze sprawdź:
- czy informacje są odczytywane i zmieniane razem;
- czy liczba powiązanych elementów ma przewidywalną granicę;
- czy ten sam obiekt jest wykorzystywany w wielu miejscach.
MongoDB obsługuje transakcje obejmujące wiele dokumentów. Niezależnie od tego pojedyncza operacja modyfikująca jeden dokument jest atomowa: wszystkie objęte nią zmiany zostają zastosowane razem albo wcale. Transakcje wielodokumentowe pozwalają zachować spójność operacji dotyczących kilku dokumentów, ale nie zastępują dobrego modelu danych. Najpierw ustal, które informacje rzeczywiście muszą zmieniać się wspólnie i czy powinny znajdować się w jednym dokumencie.
Przed usunięciem dokumentów sprawdź filtr za pomocą find() lub countDocuments(). Metoda deleteOne() usuwa jeden pasujący dokument, natomiast deleteMany() usuwa wszystkie dopasowane. Pusty filtr {} obejmuje całą kolekcję, więc wymaga szczególnej ostrożności. Jeśli chcesz wskazać konkretny dokument, użyj jego _id. Po operacji sprawdź deletedCount, aby potwierdzić liczbę usuniętych dokumentów. Usunięcie całej zawartości nie oznacza usunięcia samej kolekcji.
Skuteczność indeksu sprawdzisz za pomocą explain("executionStats") na reprezentatywnych danych. Porównaj liczbę zwróconych dokumentów z liczbą przejrzanych kluczy indeksu i dokumentów, korzystając z pól nReturned, totalKeysExamined oraz totalDocsExamined. Sam etap IXSCAN nie dowodzi dobrej wydajności. Oceń również czas wykonania zapytania oraz koszt utrzymywania indeksu podczas zapisu, ponieważ dodatkowe indeksy zajmują miejsce i mogą spowalniać modyfikacje danych.
Użyj Aggregation Pipeline, gdy potrzebujesz grupowania, obliczeń lub wieloetapowego przekształcania danych, a nie tylko wyszukania dokumentów. Metoda find() wystarcza do filtrowania, wyboru pól i sortowania wyników. Potok agregacji pozwala natomiast obliczyć sprzedaż według kategorii, średnią wartość zamówienia czy statystyki pozycji zapisanych w tablicach. Kolejność etapów wpływa na wynik, a typowa agregacja nie zmienia dokumentów źródłowych.