NoSQL databáze

Databáze, které nejsou založené na relačním datovém modelu (tj. nejsou relační). Dovedou zpracovávat obrovské množství dat v reálném čase a zpravidla jsou distribuované a otevřené. V databázích se obvykle neřeší transakční zpracování (ACID), ale data jsou snadno dostupná vždy i v částečně konzistentním stavu (BASE).


Základní kategorizace

Přibližně od roku 2008 ke dnešnímu dni existují stovky NoSQL databázových systémů pro různé způsoby využití. Během tohoto času došlo k jejich základnímu rozdělení, minimálně na tyto 4 základní kategorie:

  1. Klíč-hodnota (Key Value, případně Tuple Store): Do databáze se obvykle ukládá dvojice: klíč a jeho hodnota. Na základě znalosti klíče jsme schopni z databáze získat uloženou hodnotu. Typickými zástupci jsou: Redis, Riak a další.
  2. Dokumentové (Document Store): Do databáze se ukládají dokumenty ve formátech JSON, případně BSON. Každý ukládaný dokument může mít jinou strukturu. Typickými zástupci jsou: MongoDB, Elasticsearch a další.
  3. Sloupcové (Wide Column Store, případně Column Families): Ke každému klíči je možné uložit více hodnot odpovídající příslušnému sloupci. Každý klíč může mít vyplněné hodnoty jiných sloupců. Typickými zástupci jsou: Apache Hadoop, Apache Cassandra a další.
  4. Grafové (Graph Databases): Do databáze se ukládají uzly a jejich vlastnosti a také hrany mezi těmito uzly. Hlavním přínosem je vyhledání příslušných uzlů v rozsáhlém grafu na základě implementovaných grafových algoritmů, který je neporovnatelně rychlejší, než běžná relační databáze. Typickými zástupci jsou: Neo4j, Infinite Graph a další.
  5. Vícemodelové (Multimodel Databases): Kombinují více modelů do jediné databáze. Obvykle se kombinují grafové databáze s dokumentovými nebo s databázemi klíč-hodnota. Typickým zástupcem jsou ArangoDB, OrientDB a další.

Seznam aktuálních NoSQL databází naleznete na webu nosql-database.org.


Dokumentové databáze: MongoDB

Za vývojem databáze MongoDB je společnost MongoDB Inc., která také stojí za vývojem ovladačů pro připojení k databázi. Databáze MongoDB je šířena s otevřený zdojovým kódem a je multiplatformní, kterou si můžeme stáhnout z adresy https://www.mongodb.com/try/download/community.


Instalace

Stáhneme si odpovídající verzi Community Serveru pro naši platformu (pro MS Windows 10 x64 vybereme verzi Windows 64-bit) v podobě zip archivu, který rozbalíme do adresáře, kam máme právo zápisu. Pro komunikaci s databází potřebuje také nějakého klienta, na začátek si vystačíme se MongoDB Shellem, který je dostupný ke stažení na adrese https://www.mongodb.com/products/shell. Oba stažené archivy rozbalíme do stejného místa, tj. adresář bin bude obsahovat všechny spustitelné programy. Pro komfortnější práci je vhodné zahrnout cestu do adresáře bin do systémové (nebo uživatelské) proměnné PATH.

Pokud spuštění MongoDB serveru hlásí chyby ohledně chybějících knihoven, je potřeba instalovat balík Microsoft Visual C++ 2015-2022 Redistributable, který je dostupný v adresáři bin MongoDB serveru v podobě souboru vc_redist.x64.exe.

Datové úložiště je nutné založit na disku, kde máme právo zápisu, vhodným kandidátem je lokální disk D, kde založíme odpovídající adresář pro úložiště, např. D:\Database\MongoDB\data a adresář D:\Database\MongoDB\log pro logy. Zjištění, jakou verzi MongoDB používáme, provedeme příkazem:

mongod.exe --version

a systém odpoví

db version v6.0.4
Build Info: {
    "version": "6.0.4",
    "gitVersion": "44ff59461c1353638a71e710f385a566bcd2f547",
    "modules": [],
    "allocator": "tcmalloc",
    "environment": {
        "distmod": "windows",
        "distarch": "x86_64",
        "target_arch": "x86_64"
    }
}

Vytvoření databáze na lokálním uzlu

MongoDB databázový systém je možné používat jak na jediném uzlu, tak distribuovaně na několika uzlech. V případě lokální databáze systém nabízí zjednodušený způsob založení a konfigurace uzlu

mongod.exe --dbpath "D:\\Database\\MongoDB\\data" --port 8000

mongod.exe --dbpath "D:\\Database\\MongoDB\\data" --port 8000 --logpath "D:\\Database\\MongoDB\\log\\mongod.log"

mongod.exe --dbpath "D:\\Database\\MongoDB\\data" --port 8000 --logpath "D:\\Database\\MongoDB\\log\\mongod.log" --logappend

mongod.exe --dbpath "D:\\Database\\MongoDB\\data" --port 8000 --logpath "D:\\Database\\MongoDB\\log\\mongod.log" --logappend --directoryperdb

a jeho spuštění. Uzel komunikuje na portu 8000. Činnost uzlu ukončíme buď kombinací kláves CTRL+C nebo příkazem db.shutdownServer() nad databází admin z MongoDB Shellu. Protože při vytváření databáze na lokálním uzlu lze zadat spoustu rozličných parametrů, je výhodnější vše uložit do konfiguračního souboru, např. mongod.conf (viz níže), který umístíme do adresáře D:\Database\MongoDB a databázi založit či se připojit již k existující provedeme příkazem:

mongod.exe --config "D:\\Database\\MongoDB\\mongod.conf"

Po shození lokálního uzlu je možné smazat obsah adresáře D:\Database\MongoDB\data.


Konfigurační soubor

Konfigurační soubor je psán ve skriptovacím jazyce YAML, který nepodporuje tabulátor (odsazení je nutné vyjádřit mezerami) a také nelze soubor uložit v UTF-8 kódování, ale jen v ASCII. Náš konfigurační soubor může vypadat takto:

storage:
  dbPath: "D:\\Database\\MongoDB\\data"
  directoryPerDB: true  

systemLog:
  destination: file
  path: "D:\\Database\\MongoDB\\log\\mongod.log"
  logAppend: true  

net:
  port: 8000

Ukládané dokumenty

Dokumenty ukládané do MongoDB databáze jsou psány ve formátu JSON a fyzicky jsou ukládány ve formátu BSON, což je binární podoba JSON doplněná o datové typy.

Zjednodušeně řečeno, dokument (JSON objekt) je uzavřen do složených {} závorek, jehož obsahem je zpravidla množina dvojic tvaru

název: hodnota

Hodnotou může být:

Dvojice v dokumentu jsou oddělené čárkou. Každý takový JSON dokument, uložený v MongoDB databázi, obdrží unikátní číslo, dostupné v atributu _id.


MongoDB Shell

Jednoduchou konzolovou klientskou aplikací poslouží program MongoDB Shell. Tímto programem je možné se připojit k běžícímu databázovému uzlu, vytvářet a používat databáze a pracovat s jejich daty. Data (dokumenty) se obvykle ukládají do tzv. kolekcí (collections), které vzdáleně připomínají tabulku. MongoDB Shell se spouští programem mongosh.exe. K databázi umístěné na lokální uzlu se připojíme takto:

mongosh.exe --host localhost --port 8000

Objeví se verze MongoDB s informací, že jsme se připojili k databázi test.

Current Mongosh Log ID: 6405b6f595156a167d66cb4f
Connecting to:          mongodb://localhost:8000/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.8.0
Using MongoDB:          6.0.4
Using Mongosh:          1.8.0...
>

MongoDB Web Shell

Pokud nechceme lokálně instalovat MongoDB server, můžeme využít serveru společnosti MongoDB Inc. a připojit se k němu prostřednictvím MongoDB Web Shellu. Tento nástroj je využíván v dokumentaci produktu, kde si uživatelé mohou naživo vyzkoušet jednotlivé příkazy.

MongoDB Web Shell spustíme na adrese mws.mongodb.com, kde musíme následně kdekoliv kliknout myší. Tím se pro aktuální spojení vytvoří nový server a budeme k němu následně připojeni. S touto webovou aplikací jsou spojena následující tlačítka:


Příkazy MongoDB Shellu nebo MongoDB Web Shellu

Další příkazy ukazují, jaké databáze jsou k dispozici a jak zvolenou databázi vybrat, resp. vytvořit a zjistit, jakou databázi právě používám:

> show dbs
admin  0.000GB
config 0.000GB
local  0.000GB
> use local
switched to db local
> use db2
switched to db db2
> db
db2

Vkládání dokumentů

Dokumenty se do databáze vkládají prostřednictvím tzv. kolekcí (collections). Vložit lze buď jeden dokument příkazem insertOne() nebo více dokumentů najednou příkazem insertMany(). Vkládání dokumentů do kolekce predmety pak vypadá takto:

> db.predmety.insertOne( json_doc )
> db.predmety.insertMany( [json_doc1, json_doc2, ...] )

I když definice formátu JSON vyžaduje, aby v dvojici nazev: hodnota byl uzavřen v uvozovkách, MongoDB tento požadavek nevyžaduje a dané uvozovky doplňuje automaticky. Každý vkládaný dokument obdrží atribut "_id" s unikátní hodnotou. V archivu MongoDB data najdete tři soubory, které dohromady do kolekce predmety vkládají celkem 7 JSON dokumentů. Více detailů o vkládání dokumentů se dočtete v manuálu MongoDB. Zajímavostí je skutečnost, že samotný MongoDB Shell u názvů atributů každého dokumentu žádné uvozovky nezobrazuje.


Získávání dokumentů

Pokud chceme získat uložené dokumenty nebo je vyhledávat podle různých kritérií, potřebujeme znát, v jakých kolekcích jsou uložené. Výčet kolekcí dokumentů zíkáme příkazem:

> show collections

Získání všech dokumentů nebo pouze jediného dokumentu uložených v kolekci predmety:

> db.predmety.find()
> db.predmety.findOne()

odpovídají jednoduchým dotazům

SELECT * FROM predmety;
SELECT * FROM predmety LIMIT 1;

Obě uvedené funkce mohou mít až tři nepovinné parametry ve tvaru JSON dokumentu. Pokud není daný parametr uveden, vždy je brán v potaz prázdný JSON dokument. To znamená, že volání příkazu db.predmety.find() je shodné jako db.predmety.find({}, {}, {}). Pokud chceme vyplnit, jen druhý a/nebo třetí parametr, je nezbytné uvést první (a druhý) parametr v podobě prázdného JSON dokumentu. Parametry funkcí mají tento význam:

  1. parametr - selekce,
  2. parametr - projekce,
  3. parametr - další volby, např. řazení výsledku.

Projekce

Jak ukazovaly první dotazy, funkce find() i findOne() nabízí kompletní JSON dokumenty ve své odpovědi. Pokud budeme chtít z dokumentu vracet jen některé atributy, je třeba dotaz doplnit o tzv. projekční dokument, ve kterém vyjmenujeme

> db.predmety.find( {}, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1 } )
> db.predmety.find( {}, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )


Selekce

Kritéria výběru dokumentů se formulují ve tvaru JSON dokumentu. Podmínka jednoduché selekce, kdy vybíráme pouze aktivní předměty vypadá takto: { aktivni: true }:

> db.predmety.find( { aktivni: true } )
> db.predmety.find( { aktivni: true }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )

Další dotazy zjišťují, zda máme k dispozici předměty za požadovaný počet kreditů:

> db.predmety.find( { kredity: 4 }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )
> db.predmety.find( { kredity: 5 }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )
> db.predmety.find( { kredity: 6 }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )

Pokud chceme definovat několik podmínek v konjunkci, sestavíme v argumentu funkce find() dokument obsahující dvojice oddělené čárkou, které definují naše kritéria, nebo použijeme operátor $and. Např. chceme všechny aktivní předmety, za které student při jeho úspěšném absolvování obdrží právě 6 kreditů:

> db.predmety.find( { aktivni: true, kredity: 6 }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )
> db.predmety.find( { $and: [ { aktivni: true }, { kredity: 6 } ] }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )

Řešení podmínek v disjunkci je trochu komplikovanější. Je potřeba použít operátor $or, na který aplikujeme podmínky, které jsou definovány v poli dokumentů. Změníme v podmínce požadovaný počet kreditů na 5:

> db.predmety.find( { $or: [ { aktivni: true }, { kredity: 5 } ] }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )

Další operátory, jako např. porovnávání, najdete dostupné v dokumentaci. Vyzkoušejte následující dotazy.

> db.predmety.find( { aktivni: true, kredity: { $gte: 5 } }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )
> db.predmety.find( { $or: [ { aktivni: true }, { kredity: { $gte: 5 } } ] }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )
> db.predmety.find( { zkratka: { $in: [ "KIV/DB1", "KIV/DB2", "KIV/DB3" ] } }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )
> db.predmety.find( { zkratka: { $regex: /KIV/ } }, { zkratka: 1, nazev: 1, kredity: 1, aktivni: 1, _id: 0 } )

Vyhledání předmětů, které nabízí zápis typu A (povinný) řešíme stejně, jako kdyby hodnota atributu zapis byla jen konstanta, ne pole hodnot. Níže uvedené dotazy budou zobrazovat celé nalezené JSON dokumenty:

> db.predmety.find( { zapis: "A" } )
> db.predmety.find( { zapis: [ "A" ] } )
> db.predmety.find( { zapis: [ "A", "C" ] } )
> db.predmety.find( { $and: [ { zapis: "A" }, { zapis: "C" } ] } )

Další jednoduchý typ dotazu hledá takové předměty, u kterých nejsou uvedeni učitelé:

> db.predmety.find( { ucitele: null } )
> db.predmety.find( { ucitele: { $exists: false } } )

Definice filtrační podmínky, která se týká atributu vnořeného dokumentu vyžaduje kvalifikovaný název vnořeného atributu uzavřeného v uvozovkách nebo apostrofech. Stejná podmínka platí i v případě, že vnořený dokument je prvkem pole. Chceme získat všechny předměty, které vyučuje Martin:

> db.predmety.find( { "ucitele.jmeno": "Martin" } )

Více detailů o získávání dokumentů se dočtete v manuálu MongoDB.


Aktualizace dokumentů

Aktualizace dokumentu/ů lze v MongoDB databázi realizovat třemi různými příkazy. První dva z příkazů umí nastavit např. nové hodnoty vybraným atributům, a to buď jen pro první nalezený dokument, a nebo pro všechny dokumenty, které splňují filtrační podmínku. Z neaktivních předmětů uděláme aktivní a změníme jim počet kreditů:

> db.predmety.updateOne(  { aktivni: false }, { $set: { aktivni: true, kredity: 6 } } )
> db.predmety.updateMany( { aktivni: false }, { $set: { aktivni: true, kredity: 6 } } )

Třetí příkaz pro aktualizaci dokumentů provede náhradu existujícího dokumentu za nový. Nahrazován bude jen první nalezený dokument, který vyhovuje filtrační podmínce. V našem případě vrátíme předmět KIV/DBH do původního stavu:

> db.predmety.replaceOne( { zkratka: "KIV/DBH" }, { zkratka: "KIV/DBH", nazev: "Databázové systémy pro humanitní obory", kredity: 5, aktivni: false, semestr: ["LS"] } )

Filtrační podmínka má stejný tvar, jako podmínka u výše zmiňovaného příkazu find(). Více detailů o aktualizaci dokumentů se dočtete v manuálu MongoDB.


Mazání dokumentů

Smazání dokumentu/ů lze v MongoDB databázi realizovat dvěma různými příkazy. První z příkazů smaže jen jeden dokument, který vyhovuje zadané filtrační podmínce. Druhý příkaz maže všechny dokumenty, které vybírá zadáná filtrační podmínka. Opět zde platí, že filtrační podmínka má stejný tvar, jako podmínka u výše zmiňovaného příkazu find().

> db.predmety.deleteOne( { aktivni: true } )
> db.predmety.deleteMany( { aktivni: true } )
> db.predmety.deleteMany( {} )

Pokročilé dotazování

Dotazy v MongoDB databázi nemusí být formulovány jen funkcemi find() a findOne(), ale je možné pro složitější dotazy využít funkci aggregate(). Tato funkce nabízí celou paletu možných dotazů, které jsou skládány do pole z jednotlivých fází (stage), u kterých záleží na pořadí - tvoří tzv. pipeline.


Agregované dotazy

Pro formulaci agregovaných dotazů je nutné použít fázi $group, která má povinný atribut _id, jehož různé hodnoty určují počet skupin. V agregovaném výrazu lze použít mimo jiné známé agregované funkce z jazyka SQL.

Představení a použití známých agregovaných dotazů ukzují následující dotazy:

db.predmety.aggregate( [
  { 
    $group: {
      _id: "$semestr",
      soucet:  { $sum: "$kredity" },
      minimum: { $min: "$kredity" },
      maximum: { $max: "$kredity" },
      prumer:  { $avg: "$kredity" }
    }
  }
]
)

db.predmety.aggregate( [
  {
    $group: {
      _id: "$kredity",
      pocet:  { $count: {} }
    }
  }
]
)

Druhý dotaz počítá četnosti dokumentů, ve kterých je uvedena stejná hodnota atributu kredity. Pokud bychom nechtěli pracovat se všemi dokumenty, ale jen s některými, je nutné na začátek vložit fázi $match, která poslouží jako filtrační podmínka, kterou známe z funkce find().

db.predmety.aggregate( [
  {
    $match: { semestr: "ZS" }
  },
  {
    $group: {
      _id: "$kredity",
      pocet:  { $count: {} }
    }
  }
]
)

Z jazyka SQL známe konstrukci ORDER BY, která dovolila výsledek dotazu seřadit podle požadovaných sloupců v odpovídajícím směru. Chceme-li výsledek dotazu funkce aggregate() seřadit, vložíme do jeho zápisu fázi $sort.

db.predmety.aggregate( [
  {
    $match: { semestr: "ZS" }
  },
  {
    $group: {
      _id: "$kredity",
      pocet:  { $count: {} }
    }
  },
  {
    $sort: { pocet: -1, _id: 1}
  }
]
)

Pokud ve funkci aggregate() vložíme fázi $match až za fázi $group, tak to neznamená filtrační podmínku, ale podmínku nad agregovaným výrazem, který odpovídá konstrukci HAVING z jazyka SQL. Abychom mohli zobrazit nějaká data, odstraníme první fázi $match, tj. filtraci dokumentů.

db.predmety.aggregate( [
  {
    $group: {
      _id: "$kredity",
      pocet:  { $count: {} }
    }
  },
  {
    $match: { pocet: { $gte: 2 } }
  },
  {
    $sort: { pocet: -1, _id: 1}
  }
]
)

Spojování kolekcí

Pokud chceme ve svém dotazu zobrazovat data ze dvou či více kolekcí současně, musíme opět použít funkci aggregate(). Než k tomu přistoupíme, musíme si vytvořit novou kolekci, která bude vhodná pro spojení s kolekcí predmety. Poslouží nám k tomu agregovaný dotaz, který bude doplněn o dvě nové fáze.

db.predmety.aggregate( [
  {
    $unwind: "$semestr"
  },
  { 
    $group: {
	  _id: "$semestr",
	  maximum: { $max: "$kredity" },
	  minimum: { $min: "$kredity" }
	}
  },
  {
    $out: "semestr"
  }
]
)

Ke spojování dvou kolekcí do jednoho dotazu použijeme fázi $lookup, která bude procházet dokumenty jedné kolekce a k ní bude dohledávat odpovídající dokumenty kolekce druhé. Obecně se předpokládá, že každému dokumentu procházené kolekce bude odpovídat více dokumentů druhé kolekce , tj. mezi kolekcemi platí vztah typu 1:N, který známe z E-R-A modelů.

db.semestr.aggregate( [
  {
    $lookup: {
      from:         "predmety",
      localField:   "_id",
      foreignField: "semestr",
      as:           "predmety"
    }
  }
]
)

Chceme-li pro spojované dokumenty obou kolekcí stanovit nějakou podmínku, musíme fázi $lookup doplnit o atribut let pro definici proměnné, obsahující hodnotu atributu dokumentu procházené kolekce a pole pipeline, ve kterém stanovíme požadovanou podmínku. V této podmínce se k výše definované proměnné přistupuje skrze "$$proměnná".

db.semestr.aggregate( [
  {
    $lookup: {
      from:         "predmety",
      localField:   "_id",
      foreignField: "semestr",
      let:          { max_kreditu: "$maximum" },
      pipeline: [
        {
          $match:
            { 
              $expr: 
                { $eq: [ "$kredity", "$$max_kreditu" ] }
            }
        }
      ],
      as: "predmety"
    }
  }
]
)

Projekce odvozené hodnoty

Poslední dotaz, který si ukážeme, řeší zobrazení hodnoty, která byla spočítána/odvozena z několika atributů. Nejen k zobrzení této hodnoty použijeme fázi $project funkce aggeregate(), funkce find() a findOne() to ve svém projektovém JSON dokumentu zapsat neumí.

db.semestr.aggregate( [
  {
    $project: {
      maximum: 1,
      minimum: 1,
      diff: {
        $subtract: ['$maximum','$minimum']
      }
    }
  }
]
)

MongoDB Compass

MongoDB Compass je multiplatformní grafické uživatelské prostředí pro práci s MongoDB databází, které plně nahrazuje méně komfortního klienta MongoDB Shell. Také za jeho vývojem stojí společnost MongoDB Inc..


Stažení a instalace

Na adrese https://www.mongodb.com/try/download/compass je ke stažení aktuální verze programu pro různé platformy. Pro operační systém MS Windows je instalační balíček dostupný také ve formátu ZIP, který použijeme.


Připojení k MongoDB serveru

Po spuštění programu je nutné vytvořit nové spojení, kterým se připojíme k běžícímu MongoDB serveru. Náš server beží na lokálním počítači a komunikuje na portu 8000, proto do kolonky URI vložíme tento řetězec:

mongodb://localhost:8000/

Další parametry pro komunikaci jsme na serveru nenastavili, proto stačí stisknout tlačítko Save & Connect a můžeme program používat.


Jednoduché dotazy

Z nabídky po levé straně vybereme databázi, kterou cheme používat (db2) a z databáze kolekci, nad kterou chceme formulovat dotaz (predmety). Otevře se nová záložka s dokumenty vybrané kolekce. Vykonání dotazu funkcí find() provedeme z nabídky Documents, kde do editačního políčka Filter vložíme jen JSON dokument, kterým formulujeme podmínku selekce. Vykonání dotazu zařídíme stiskem tlačítka Find.

Chceme-li specifikovat např. projekci dotazu, musíme před vykonáním dotazu specifikovat další volby, které zobrazíme přes odkaz More Options.


Pokročilé dotazy

Pokročilé dotazy, které volají funkci aggregate(), se postupně sestavují pod nabídkou Aggregations. To znamená, že každá fáze dotazu se tvoří samostatně, stiskem tlačítka + Add Stage. Vedle definice každé fáze je hned vidět její výsledek, přesněji výsledek dotazu po dané fázi. To znamená, že výsledek celého dotazu je vidět vedle definice poslední fáze dotazu.


Vestavěnný MongoDB Shell

V dolní části programu je vidět jeden řádek uvozený slovem MONGOSH. Když na něj klepnete, ukáže se konzole kleinta MongoDB Shellu, kterou můžeme plnohodnotně používat. Pokud skrze tuto konzoli budou nějak měněna data v databázích, je nutné/vhodné je znovu načtení vyvolat z nabídky spojení položkou Reload Data.


Copyright © 2023 Martin Zíma