- Praktický úvod do MongoDB (1): NoSQL opravdu snadno
- Praktický úvod do MongoDB (2): Indexy a agregace
- Praktický úvod do MongoDB (3): clustering
Minule jsme se naučili MongoDB používat. Dnes si ho trochu “narychlíme” vytvořením indexů, ale hlavně se podíváme na velmi mocné možnosti agregování dat.
Indexy
MongoDB ve výchozím stavu optimalizuje svoje chování pro vyhledávání podle _id. Pokud se potřebujeme dozvědět víc o tom, co náš dotaz znamená z pohledu zpracování, můžeme využít příkaz explain. Takhle tedy Mongo vyhledává podle jména:
db.lide.explain().find({"jmeno":"tomas"}) { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.lide", "indexFilterSet" : false, "parsedQuery" : { "jmeno" : { "$eq" : "tomas" } }, "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "jmeno" : { "$eq" : "tomas" } }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "ubuntu", "port" : 27017, "version" : "3.0.6", "gitVersion" : "1ef45a23a4c5e3480ac919b28afcba3c615488f2" }, "ok" : 1 }
Předpokládám, že hledání nebo třídění podle jména bude velmi časté. Pojďme tedy vytvořit index, abychom tyto operace výrazně urychlili:
db.lide.createIndex({"jmeno":1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 }
Na takto malém vzorku rozdíl těžko naměříme, ale použijme explain() a přesvědčme se, že byl index použit.
db.lide.explain().find({"jmeno":"tomas"}) { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.lide", "indexFilterSet" : false, "parsedQuery" : { "jmeno" : { "$eq" : "tomas" } }, "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "jmeno" : 1 }, "indexName" : "jmeno_1", "isMultiKey" : false, "direction" : "forward", "indexBounds" : { "jmeno" : [ "[\"tomas\", \"tomas\"]" ] } } }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "ubuntu", "port" : 27017, "version" : "3.0.6", "gitVersion" : "1ef45a23a4c5e3480ac919b28afcba3c615488f2" }, "ok" : 1 }
Indexů je víc kategorií, ale pro účely praktického úvodu do MongoDB nám tohle myslím stačí.
Agregace s využitím pipeline
MongoDB není nejrychlejší databáze na světě pro analýzu dat – pro hardcore nasazení použijte třeba HBase + Hive (nad Hadoop). Nicméně pro běžné prostředí jsou možnosti Mongo velmi zajímavé zejména s ohledem na to, že fungují i v clusteru a jsou velmi jednoduché a pohodlné. Začneme zabudovaným enginem, který funguje jako pipeline, tedy jednotlivé operace můžete řetězit. Nejlepší bude si to jednoduše ukázat.
Připomínám naše jednoduchá data z předchozího dílu:
db.lide.find() { "_id" : ObjectId("55dc48a4bdd068b804752b9f"), "jmeno" : "alenka", "udaje" : { "vek" : 35, "pohlavi" : "zena", "velikost" : "L" }, "objednavky" : [ { "faktura" : 1, "polozky" : [ { "produkt" : "tricko", "pocet" : 2 }, { "produkt" : "ubrousky", "pocet" : 1 } ] } ] } { "_id" : ObjectId("55df5dbd82f8b5415410f913"), "jmeno" : "martin", "udaje" : { "vek" : 25, "pohlavi" : "muz" }, "objednavky" : [ { "faktura" : 1, "polozky" : [ { "produkt" : "nocnik", "pocet" : 1 }, { "produkt" : "ubrousky", "pocet" : 3 } ] } ] } { "_id" : ObjectId("55df5dcd82f8b5415410f914"), "jmeno" : "tomas", "udaje" : { "vek" : 30, "pohlavi" : "muz" }, "objednavky" : [ { "faktura" : 1, "polozky" : [ { "produkt" : "kartacek", "pocet" : 4 }, { "produkt" : "ubrousky", "pocet" : 3 } ] }, { "faktura" : 2, "polozky" : [ { "produkt" : "nocnik", "pocet" : 1 } ] } ] }
Pojďme zjistit průměrný věk všech lidí v kolekci dokumentů. Použijeme operaci $group, která funguje podobně, jako GROUP BY ve světě SQL – tedy seskupí záznamy podle nějakého klíče. V našem případě ale seskupení zatím nepotřebujeme, takže tento klíč necháme prázdný. To co potřebujeme je schopnost dělat operace na nějakém klíči – třeba součet, minimum, maximum nebo právě průměr.
db.lide.aggregate([ {$group: { _id:"", "PrumernyVek" : {$avg : "$udaje.vek"} }}]) { "_id" : "", "PrumernyVek" : 30 }
Co kdybychom chtěli spočítat průměrný věk mužů a žen? Jednoduché – seskupme $group funkci podle pohlaví. Všimněte si, že pohlaví není přímo klíčem hlavního dokumentu, ale vnořeného dokumentu “udaje” – to ale nevadí, použijte tečkovou notaci.
db.lide.aggregate([ {$group: { _id:"$udaje.pohlavi", "PrumernyVek" : {$avg : "$udaje.vek"} }}]) { "_id" : "muz", "PrumernyVek" : 27.5 } { "_id" : "zena", "PrumernyVek" : 35 }
Zkusme něco jiného – co třeba spočítat, kolik má každý člověk u nás objednávek. K tomu použijme funkci $project. Vybereme, že chceme zobrazit políčka jméno a pohlaví a také počet objednávek, kde využijeme $size, která počítá počet objektů v poli (objednavky jsou vnořený dokument v podobě pole s objednávkami).
db.lide.aggregate([ {$project: {"jmeno":"$jmeno", "pohlavi":"$udaje.pohlavi", "Objednavek" : {$size : "$objednavky"}}}]) { "_id" : ObjectId("55dc48a4bdd068b804752b9f"), "jmeno" : "alenka", "pohlavi" : "zena", "Objednavek" : 1 } { "_id" : ObjectId("55df5dbd82f8b5415410f913"), "jmeno" : "martin", "pohlavi" : "muz", "Objednavek" : 1 } { "_id" : ObjectId("55df5dcd82f8b5415410f914"), "jmeno" : "tomas", "pohlavi" : "muz", "Objednavek" : 2 }
Co kdybychom chtěli znát počet objednávek ne podle jména, ale podle pohlaví? Něco podobného už jsme dělali – bylo to $group a použijeme ho i tentokrát. Rozdíl je, že nejprve potřebujeme spočítat ty objednávky (tedy to co jsme v předchozím kroku provedli) a jako druhý krok v pipeline využijeme $group seskupený podle pohlaví, u kterého sečteme “Objednavek” (tedy informaci, která vznikla v prvním kroku pipeline):
db.lide.aggregate([ {$project: {"jmeno":"$jmeno", "pohlavi":"$udaje.pohlavi", "Objednavek" : {$size : "$objednavky"}}}, {$group: {_id:"$pohlavi", "CelkemObjednavek": {$sum: "$Objednavek"} }}]) { "_id" : "muz", "CelkemObjednavek" : 3 } { "_id" : "zena", "CelkemObjednavek" : 1 }
Snad to ještě není na začátek příliš složité. To následující už ale trochu bude – možná by bylo dobré vědět, kolik jsme prodali jaké kategorie zboží, což není jen tak, protože do dokumentu s člověkem je vnořen dokument objednavky (resp. pole několika objednávek), kde každá objednávka má na faktuře vícero položek (tedy do objednávky je vnořen dokument polozky ve formě pole). Ale i to s MongoDB zvládneme.
Vnoření nám neumožňuje aplikovat další operace jako je seskupování. Nejprve pojďme tedy objednávky rozmontovat – čili aby každá objednávka měla vlastní řádek:
db.lide.aggregate([{$unwind : "$objednavky"}]) { "_id" : ObjectId("55dc48a4bdd068b804752b9f"), "jmeno" : "alenka", "udaje" : { "vek" : 35, "pohlavi" : "zena", "velikost" : "L" }, "objednavky" : { "faktura" : 1, "polozky" : [ { "produkt" : "tricko", "pocet" : 2 }, { "produkt" : "ubrousky", "pocet" : 1 } ] } } { "_id" : ObjectId("55df5dbd82f8b5415410f913"), "jmeno" : "martin", "udaje" : { "vek" : 25, "pohlavi" : "muz" }, "objednavky" : { "faktura" : 1, "polozky" : [ { "produkt" : "nocnik", "pocet" : 1 }, { "produkt" : "ubrousky", "pocet" : 3 } ] } } { "_id" : ObjectId("55df5dcd82f8b5415410f914"), "jmeno" : "tomas", "udaje" : { "vek" : 30, "pohlavi" : "muz" }, "objednavky" : { "faktura" : 1, "polozky" : [ { "produkt" : "kartacek", "pocet" : 4 }, { "produkt" : "ubrousky", "pocet" : 3 } ] } } { "_id" : ObjectId("55df5dcd82f8b5415410f914"), "jmeno" : "tomas", "udaje" : { "vek" : 30, "pohlavi" : "muz" }, "objednavky" : { "faktura" : 2, "polozky" : [ { "produkt" : "nocnik", "pocet" : 1 } ] } }
Když už máme takhle pěkně rozmontováno, mohli bychom se spočítat počet položek v každé objednávce. Něco podobného už jsme dělali – prostě přidáme druhý krok do pipeline, kterým bude $project s využitím $size pro spočítání počtu objektů v každé objednávce.
db.lide.aggregate([{$unwind : "$objednavky"}, {$project:{"Polozek":{$size:"$objednavky.polozky"}}}]) { "_id" : ObjectId("55dc48a4bdd068b804752b9f"), "Polozek" : 2 } { "_id" : ObjectId("55df5dbd82f8b5415410f913"), "Polozek" : 2 } { "_id" : ObjectId("55df5dcd82f8b5415410f914"), "Polozek" : 2 } { "_id" : ObjectId("55df5dcd82f8b5415410f914"), "Polozek" : 1 }
Samozřejmě by teď nebyl problém spočítat průměrný počet položek v objednávce. Přidejte třetí operaci do řetězce typu $group bez vyplněného políčka a vytiskneme průměr počtu položek:
db.lide.aggregate([{$unwind : "$objednavky"}, {$project:{"Polozek":{$size:"$objednavky.polozky"}}}, {$group:{_id:"", "PrumerPolozek":{$avg:"$Polozek"}}}]) { "_id" : "", "PrumerPolozek" : 1.75 }
My se ale chceme dostat až k produktům v položkách, pojďme tedy rozmontovat i polozky. V první kroku tedy rozebereme objednávky, v druhém položky:
db.lide.aggregate([{$unwind : "$objednavky"}, {$unwind : "$objednavky.polozky"}]) { "_id" : ObjectId("55dc48a4bdd068b804752b9f"), "jmeno" : "alenka", "udaje" : { "vek" : 35, "pohlavi" : "zena", "velikost" : "L" }, "objednavky" : { "faktura" : 1, "polozky" : { "produkt" : "tricko", "pocet" : 2 } } } { "_id" : ObjectId("55dc48a4bdd068b804752b9f"), "jmeno" : "alenka", "udaje" : { "vek" : 35, "pohlavi" : "zena", "velikost" : "L" }, "objednavky" : { "faktura" : 1, "polozky" : { "produkt" : "ubrousky", "pocet" : 1 } } } { "_id" : ObjectId("55df5dbd82f8b5415410f913"), "jmeno" : "martin", "udaje" : { "vek" : 25, "pohlavi" : "muz" }, "objednavky" : { "faktura" : 1, "polozky" : { "produkt" : "nocnik", "pocet" : 1 } } } { "_id" : ObjectId("55df5dbd82f8b5415410f913"), "jmeno" : "martin", "udaje" : { "vek" : 25, "pohlavi" : "muz" }, "objednavky" : { "faktura" : 1, "polozky" : { "produkt" : "ubrousky", "pocet" : 3 } } } { "_id" : ObjectId("55df5dcd82f8b5415410f914"), "jmeno" : "tomas", "udaje" : { "vek" : 30, "pohlavi" : "muz" }, "objednavky" : { "faktura" : 1, "polozky" : { "produkt" : "kartacek", "pocet" : 4 } } } { "_id" : ObjectId("55df5dcd82f8b5415410f914"), "jmeno" : "tomas", "udaje" : { "vek" : 30, "pohlavi" : "muz" }, "objednavky" : { "faktura" : 1, "polozky" : { "produkt" : "ubrousky", "pocet" : 3 } } } { "_id" : ObjectId("55df5dcd82f8b5415410f914"), "jmeno" : "tomas", "udaje" : { "vek" : 30, "pohlavi" : "muz" }, "objednavky" : { "faktura" : 2, "polozky" : { "produkt" : "nocnik", "pocet" : 1 } } }
Teď už tedy můžeme přidat $group podle produktů a sečíst jejich prodaný počet:
db.lide.aggregate([{$unwind : "$objednavky"}, {$unwind : "$objednavky.polozky"}, {$group:{_id:"$objednavky.polozky.produkt", "celkem" : {$sum:"$objednavky.polozky.pocet"}}}]) { "_id" : "nocnik", "celkem" : 2 } { "_id" : "ubrousky", "celkem" : 7 } { "_id" : "kartacek", "celkem" : 4 } { "_id" : "tricko", "celkem" : 2 }
Z SQL možná znáte GROUP BY … HAVING – co kdybychom chtěli znát zboží a jeho počty spotřebované pouze muži? Přidáme novou operaci $match, která provede filtraci před dalším zpracováním.
db.lide.aggregate([ {$match:{"udaje.pohlavi":"muz"}}, {$unwind : "$objednavky"}, {$unwind : "$objednavky.polozky"}, {$group:{_id:"$objednavky.polozky.produkt", "celkem" : {$sum:"$objednavky.polozky.pocet"}}} ]) { "_id" : "kartacek", "celkem" : 4 } { "_id" : "ubrousky", "celkem" : 6 } { "_id" : "nocnik", "celkem" : 2 }
Mohli bychom dát match klidně na druhé i na třetí místo – na začátek to ale bude nejlepší. Jednak tím snížíme počet záznamů, které vstupují do dalších operací (zvyšujeme výkon a snižujeme náročnost na zdroje) a také to umožní využít indexů (je to vlastně filtrace dat stejná jako při použití find).
Tohle zdaleka není všechno, nicméně pro praktický úvod je to asi dost na to, aby bylo zřejmé, že MongoDB má v oblasti agregace dat hodně co nabídnout.
Map / reduce
Pipeline využívá principů map/reduce, ale nemusíte o tom vědět a používáte jednoduché příkazy. Nicméně MongoDB vám umožní si napsat svůj vlastní map/reduce! Jak už jsem říkal na chroupání petabajtů dat použijte Hadoop, HBase, Hive – ale tady můžete mít velmi příjemnou dokumentovou a velmi flexibilní schema-less databázi s rozumnou škálovatelností pro běžné použití pro vaši aplikaci a přitom disponuje velmi mocným arzenálem na chroupání. Pro účel jednoduchého úvodu si ukážeme jen něco velmi primitivního, něco, co by v pipeline bylo podstatně jednodušší. Nicméně uvidíte, že tady už si píšete vlastní chroupací kód – můžete tak dělat cokoli. Jazykem v MongoDB pro map/reduce je Javascript.
Nejprve definujte mapovací funkci (zadáte třeba přímo v Mongo CLI) – v našem případě vrátíme jednoduše pohlaví a číslo jedna za každý záznam (ale představte si jaké šílené výpočty tady můžete s každým záznamem dělat):
var mojemap = function() { emit(this.udaje.pohlavi, 1); };
Naše reduce funkce přijímá z map pohlaví a počet (tím je v našem primitivním příkladě vždy jednička). Reduce funkce ten počet jedniček jednoduše sečte.
var mujreduce = function(pohlavi, pocet) { return Array.sum(pocet); };
Nakonec zadejte map/reduce po naši kolekci dokumentů “lide”:
db.lide.mapReduce( mojemap, mujreduce, { out: "Prvni_mapreduce_pokus" } )
Tohle je výstup posledního příkazu:
{ "result" : "Prvni_mapreduce_pokus", "timeMillis" : 51, "counts" : { "input" : 3, "emit" : 3, "reduce" : 1, "output" : 2 }, "ok" : 1 }
Teď už můžeme tuto speciální map/reduce operaci vyvolat kdykoli chceme:
db.Prvni_mapreduce_pokus.find() { "_id" : "muz", "value" : 2 } { "_id" : "zena", "value" : 1 }
Ve většině případů vám pipeline poskytne dostatečné možnosti – ale klidně si napište vlastní map/reduce kód.
Příště si ukážeme využití MongoDB v aplikaci a také nahlédneme do možnosti clusteringu.