RE:NODE

Agregační pipeline v MongoDB: $match, $group, $lookup

Text: Tým RE:NODE
18 min čtení


Aggregation pipeline je pole stage. Každá stage vezme dokumenty, které vydala ta předchozí, nějak je upraví a předá dál. $match filtruje, $group sloučí mnoho dokumentů do jednoho na každý klíč, $lookup připojí jinou kolekci a pořadí, v jakém je seřadíš, rozhoduje o tom, jestli dotaz využije index, kolik paměti potřebuje a jestli doběhne za 40 milisekund, nebo skončí chybou paměti. Stručně: nejdřív filtruj, potom seskupuj, spojuj až nakonec a podívej se do explain(), než pipeline uvěříš na kolekci, která bude růst.

Tenhle návod postupně staví jednu pipeline na malé sadě dat z obchodu, vysvětluje, co dělá každá běžná stage, ukazuje, které stage mohou využít index a které ne, a probírá limit 100 MB, o který lidé zakopnou ve chvíli, kdy se report poprvé spustí na skutečných datech. Předpokládá, že už umíš navázat spojení a spouštět dotazy; pokud ne, začni connection stringy v MongoDB pro začátečníky.

Co je pipeline a čím se liší od find()#

find() vrací dokumenty tak, jak jsou uložené, případně vyfiltrované a zúžené na vybraná pole. aggregate() umí vrátit něco, co nikdy uložené nebylo: součty podle zákazníků, počet objednávek za den, objednávky spojené se zákazníkem, který je zadal. Všechno, co bys v SQL napsal jako GROUP BY, JOIN nebo okenní funkci, v MongoDB projde pipeline.

Pipeline se spouští jako db.collection.aggregate([ stage1, stage2, ... ]). Dokumenty procházejí stage v daném pořadí a každá stage vidí jen to, co vydala ta před ní. Z toho plynou dva důsledky a většina problémů s pipeline se vrací k jednomu z nich:

  • Stage může použít jen pole, která v tu chvíli ještě existují. Pokud brzký $project nebo $group pole zahodil, pozdější $match na něj nic nenajde a žádná chyba ti to neřekne.
  • Stage, které potřebují celý vstup, než cokoli vydají ($group, $sort bez indexu, $bucket), drží dokumenty v paměti. Stage, které dokumenty propouštějí po jednom ($match, $project, $addFields, $limit), stojí skoro nic.

Příklady níže používají dvě kolekce. orders má jeden dokument na objednávku, customers jeden na účet:

javascript
db.orders.insertMany([  { customerId: 1, status: "paid", total: 42.5,    createdAt: ISODate("2026-10-01T09:12:00Z"),    items: [{ sku: "A-100", qty: 2, price: 10 }, { sku: "B-200", qty: 1, price: 22.5 }] },  { customerId: 2, status: "paid", total: 15,    createdAt: ISODate("2026-10-02T14:40:00Z"),    items: [{ sku: "A-100", qty: 1, price: 10 }, { sku: "C-300", qty: 1, price: 5 }] },  { customerId: 1, status: "refunded", total: 10,    createdAt: ISODate("2026-10-03T08:05:00Z"),    items: [{ sku: "A-100", qty: 1, price: 10 }] }])db.customers.insertMany([  { _id: 1, name: "Nino", plan: "pro" },  { _id: 2, name: "Dato", plan: "free" }])

Spouštěj je v mongosh. Stejné dokumenty stage fungují beze změny i z ovladače; v Node.js, Pythonu a Go je pipeline prostě pole objektů.

$match: filtruj jako první a využij index#

$match přijímá stejnou syntaxi dotazu jako find(). Je to nejdůležitější stage v pipeline, protože je to ta, která může využít index, a jediná, která zlevní všechno, co přijde po ní.

javascript
db.orders.aggregate([  { $match: { status: "paid", createdAt: { $gte: ISODate("2026-10-01") } } }])

$match na úplném začátku pipeline se plánuje stejně jako filtr v find(): pokud index pokrývá status a createdAt, MongoDB projde rozsah indexu místo celé kolekce. $match později v pipeline pracuje s výstupem předchozích stage, takže index kolekce využít vůbec nemůže. Proto platí pravidlo "match co nejdřív".

V jednom případě ti optimalizátor pomůže. Pokud napíšeš $match za $project nebo $addFields a podmínka se týká jen polí, která tyto stage neměnily, MongoDB $match posune dopředu samo. Pokud podmínka závisí na poli, které předchozí stage vypočítala, posunout ho nejde, a spoléhat na to bys neměl: piš pipeline v pořadí, v jakém chceš, aby běžela.

Dva detaily, které bolí:

  • Obyčejný $match porovnává pole s hodnotou, ne s jiným polem. Cesty k polím jako "$total" se čtou jen ve výrazech. Použij $expr, když potřebuješ porovnat dvě pole stejného dokumentu: { $match: { $expr: { $gt: ["$total", "$limit"] } } }. $match s $expr obecně nemůže pro takové porovnání využít index, takže ho používej záměrně.
  • Data se porovnávají jako data, jen když jsou uložená jako data. Pokud byl createdAt vložen jako ISO řetězec, $gte: ISODate(...) nenajde nic. Spusť db.orders.findOne() a podívej se na typ, než napíšeš rozsah.

$group: jeden dokument na klíč#

$group sloučí všechny příchozí dokumenty se stejným výrazem _id do jednoho výstupního dokumentu a nad skupinou spočítá akumulátory. Tržby na zákazníka z uhrazených objednávek:

javascript
db.orders.aggregate([  { $match: { status: "paid" } },  { $group: {      _id: "$customerId",      orders: { $sum: 1 },      revenue: { $sum: "$total" },      average: { $avg: "$total" },      lastOrder: { $max: "$createdAt" }  } },  { $sort: { revenue: -1 } }])

_id skupiny je to, podle čeho seskupuješ. Cesta k poli ("$customerId"), celý vnořený dokument pro složený klíč ({ customer: "$customerId", day: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } } }), nebo null, kterým sloučíš celý vstup do jednoho dokumentu, což je způsob, jak spočítat celkový součet.

Tohle jsou akumulátory, které budeš používat nejčastěji:

AkumulátorVýsledek
$sumSoučet pole, nebo 1 za dokument pro počítání
$avgPrůměr pole, chybějící hodnoty se ignorují
$min, $maxNejmenší nebo největší hodnota ve skupině
$first, $lastHodnota z prvního nebo posledního dokumentu (smysl dává jen po $sort)
$pushPole všech hodnot, duplicity zůstávají
$addToSetPole unikátních hodnot

$push a $addToSet jsou nebezpečné. Drží v paměti každou hodnotu každé skupiny a hotová skupina je jeden dokument s limitem velikosti 16 MB. $group s _id: "$customerId" a $push: "$$ROOT" na velké kolekci je standardní způsob, jak narazit na oba limity najednou. Pokud potřebuješ jen poslední tři objednávky každého zákazníka, použij top-N akumulátory ($topN, $firstN) ve verzi, která je má, místo abys vše nasypal do pole a pak ho ořezával.

Report podle data je stejná stage s jiným klíčem. Objednávky za den:

javascript
db.orders.aggregate([  { $match: { createdAt: { $gte: ISODate("2026-10-01"), $lt: ISODate("2026-11-01") } } },  { $group: {      _id: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },      orders: { $sum: 1 }  } },  { $sort: { _id: 1 } }])

Klíč je řetězec v UTC, pokud nepředáš timezone. Pokud jsou reporty pro lidi v jednom časovém pásmu, předej ho (timezone: "Asia/Tbilisi"), jinak objednávka z 01:00 místního času skončí v předchozím dni.

$project, $addFields, $unwind a $count#

Čtyři menší stage zajišťují většinu přetváření kolem $match a $group.

$project pole ponechá, zahodí nebo vypočítá. { $project: { _id: 0, customerId: 1, total: 1 } } vrátí jen tato dvě pole. $addFields (a jeho alias $set) přidá vypočtená pole a všechno ostatní ponechá, což je uprostřed pipeline obvykle to, co chceš.

$unwind rozbalí pole na jeden dokument za každý prvek. Takhle agreguješ přes items:

javascript
db.orders.aggregate([  { $match: { status: "paid" } },  { $unwind: "$items" },  { $group: {      _id: "$items.sku",      units: { $sum: "$items.qty" },      revenue: { $sum: { $multiply: ["$items.qty", "$items.price"] } }  } },  { $sort: { revenue: -1 } },  { $limit: 5 }])

$unwind dokumenty násobí, takže ho dej až za $match a hlídej, kolik z toho vznikne: kolekce milionu objednávek po deseti položkách se změní na deset milionů dokumentů, které procházejí další stage. Dokumenty, kde pole chybí nebo je prázdné, se zahodí, pokud nenapíšeš { $unwind: { path: "$items", preserveNullAndEmptyArrays: true } }.

$count je stage, která vydá jeden dokument s počtem vstupních dokumentů: { $count: "paidOrders" }. $sort, $limit a $skip fungují jako v find(), s jedním rozdílem, na kterém později záleží: $sort následovaný přímo $limit se optimalizuje na top-k řazení, které drží v paměti jen k dokumentů.

$lookup: spojování kolekcí#

$lookup je v MongoDB left outer join. Pro každý příchozí dokument načte odpovídající dokumenty z jiné kolekce a připojí je jako pole:

javascript
db.orders.aggregate([  { $match: { status: "paid" } },  { $lookup: {      from: "customers",      localField: "customerId",      foreignField: "_id",      as: "customer"  } },  { $unwind: "$customer" },  { $project: { total: 1, "customer.name": 1, "customer.plan": 1 } }])

as je vždy pole, i když se shoduje přesně jeden dokument, a proto následuje $unwind. Vynecháš-li ho, dostaneš customer: [ { ... } ], což jednou překvapí skoro každého. Pokud se neshoduje žádný dokument, pole je prázdné a $unwind výše by takovou objednávku zahodil. Přidej preserveNullAndEmptyArrays: true, pokud má objednávka chybějícího zákazníka přežít.

Tři pravidla drží $lookup rychlý:

  1. Indexuj cizí pole. Pro každý příchozí dokument spouští $lookup dotaz na druhou kolekci. Na _id je index vždy. Na customerId v orders (když spojuješ opačným směrem) není, a bez indexu každé vyhledání prochází celou kolekci: tisíc příchozích dokumentů proti sto tisícům cizích je sto milionů porovnání.
  2. Nejdřív zmenši vstup. Každý dokument, který dorazí do $lookup, stojí jeden dotaz. $match a $limit dej před něj, nikdy za něj.
  3. Spojuj ze směru od menší strany. Začít s několika dokumenty, které přežily filtr, a hledat přes index ve velké kolekci je levné. Opačně ne.

Když potřebuješ připojené dokumenty filtrovat nebo přetvářet, použij formu s pipeline a lokální pole předej přes let:

javascript
db.customers.aggregate([  { $match: { plan: "pro" } },  { $lookup: {      from: "orders",      let: { cid: "$_id" },      pipeline: [        { $match: { $expr: { $eq: ["$customerId", "$$cid"] }, status: "paid" } },        { $project: { _id: 0, total: 1, createdAt: 1 } },        { $sort: { createdAt: -1 } },        { $limit: 3 }      ],      as: "recentOrders"  } }])

Využití indexů uvnitř $lookup ve formě s pipeline je omezenější než v jednoduché formě. Rovnost přes $expr na poli může index využít, ale ověř si to přes explain(), nepředpokládej to; je to popsáno níže. U spojení, které děláš pořád, bývá odpovědí ne rychlejší $lookup, ale jiné schéma: vnořuj to, co čteš vždy společně, jak popisuje schéma a indexy v MongoDB. $lookup je skvělý nástroj pro reporty a drahý pro horkou cestu webového požadavku.

Připojené pole také žije uvnitř jednoho dokumentu, takže na něj platí limit dokumentu 16 MB. Když ke každému zákazníkovi připojíš všechny objednávky, které kdy zadal, bez $limit, dotaz jednou selže na zákazníkovi s nejvíc objednávkami.

Které stage využívají index#

Index mohou využít jen stage na začátku pipeline, a ne všechny:

StageVyužití indexu
$match na začátkuAno, stejně jako v find()
$sort na začátku nebo hned za $match s indexemAno, pokud pořadí zajišťuje index
$groupNe, ale $sort s $first nebo $last může využít index na klíči řazení
$lookupVyhledávání v cizí kolekci využívá její indexy
$project, $addFields, $unwindNe - dokumenty jen přetvářejí
Jakákoli stage po $group nebo $projectNe - už to nejsou dokumenty kolekce

Důvod je jednoduchý: index je struktura nad uloženými dokumenty. Jakmile stage změní tvar proudu, není na co index navázat. Proto je výchozí pořadí "match, sort, group, lookup" a pipeline, která začíná $project nebo $addFields a teprve potom filtruje, je varovný signál.

Pro pipeline, která filtruje na rovnost a řadí, platí stejná pravidla složených indexů jako pro find(): nejdřív pole s rovností, potom řazení, potom rozsah. Jak pole seřadit, vysvětluje rozbor pravidla ESR. Pro report uhrazených objednávek podle data výše:

javascript
db.orders.createIndex({ status: 1, createdAt: -1 })

obslouží $match na status i rozsah na createdAt a dodá dokumenty už seřazené podle data, pokud o to pipeline požádá.

Jak číst explain() u pipeline

Nehádej, jestli se index použil. Zeptej se:

javascript
db.orders.explain("executionStats").aggregate([  { $match: { status: "paid", createdAt: { $gte: ISODate("2026-10-01") } } },  { $group: { _id: "$customerId", revenue: { $sum: "$total" } } }])

Ve výstupu najdi vstupní stage. IXSCAN s názvem indexu znamená, že se index použil. COLLSCAN znamená, že se četl každý dokument. Porovnej totalDocsExamined s nReturned na první stage: prohlédnout 500 000 dokumentů, aby se vrátilo 800, znamená, že filtr není pro tvůj index selektivní, nebo že žádný index není. U pipeline s $lookup ukazuje výstup explain přístup k cizí kolekci zvlášť a COLLSCAN tam je chybějící index na foreignField.

Pokud se první stage čte jako $cursor s blokem queryPlanner, je to část předaná dotazovému enginu a jen tu může obsloužit index. Všechno, co je uvedeno za ní, běželo už ve vrstvě agregace.

Prosté explain() (bez úrovně podrobnosti) jen plánuje a pipeline nikdy nespustí, což je bezpečná volba proti produkční databázi. executionStats pipeline spouští, takže na velké kolekci ho pusť na kopii nebo s $limit, který bys jinak nepoužil.

Limit 100 MB a allowDiskUse#

Každá blokující stage v pipeline smí využít až 100 megabajtů RAM. Blokující jsou ty stage, které musí vidět celý vstup, než vydají výsledek: $group, $sort (když pořadí nezajišťuje index), $bucket, $bucketAuto, $setWindowFields nad velkými partitions a několik dalších. Stage, které dokumenty propouštějí průběžně, jako $match, $project a $addFields, se do limitu nepočítají. Limit platí na stage, ne na celou pipeline, takže pipeline s $group a za ním $sort může použít 100 MB na každou z nich.

Když stage limit překročí, chování závisí na verzi serveru a na volbě allowDiskUse:

  • V MongoDB 6.0 a novějším je serverové nastavení allowDiskUseByDefault rovno true, pokud ho nasazení nevypnulo, takže stage, která přeroste 100 MB, zapisuje dočasné soubory na disk a pokračuje, jen pomaleji.
  • V dřívějších verzích, nebo tam, kde je allowDiskUseByDefault rovno false, stage selže s chybou jako Sort exceeded memory limit of 104857600 bytes, but did not opt in to external sorting nebo Exceeded memory limit for $group, but didn't allow external sort. Předej allowDiskUse: true jako volbu, aby se mohla odkládat na disk.
javascript
db.orders.aggregate(  [    { $group: { _id: "$customerId", orders: { $push: "$_id" } } },    { $sort: { _id: 1 } }  ],  { allowDiskUse: true })

V ovladači jde stejná volba do argumentu options u aggregate(). Přesný zápis najdeš v dokumentaci svého ovladače, protože se řídí konvencemi jazyka.

allowDiskUse je záchranná síť, ne oprava. Mění tvrdé selhání na pomalý dotaz: stage zapisuje seřazené úseky do adresáře _tmp pod cestou k databázi a pak je slévá, což stojí diskové I/O a na malém tarifu to soutěží se vším ostatním, co server dělá. Potřebuje také volné místo v datovém adresáři. Pipeline, která odkládá na disk pravidelně, ti něco říká, a obvyklá náprava je v tomto pořadí:

  1. Posuň $match dřív nebo přidej index, který potřebuje, aby se k blokující stage dostalo méně dokumentů.
  2. Zahoď pole, která nepotřebuješ, přes $project před $group nebo $sort. Stage drží celé dokumenty a dokument o 4 KB s polem o 40 bajtech, které potřebuješ, stojí stokrát víc, než by měl.
  3. Dej $limit hned za $sort, aby se uchovalo jen několik prvních.
  4. Nahraď $push: "$$ROOT" a podobné malým akumulátorem, který drží jedno číslo.
  5. U reportu, který spouštíš denně nad miliony dokumentů, ho předpočítej do souhrnné kolekce přes $merge (nebo $out) podle plánu a čti souhrn.

Dvě omezení jsou oddělená od čísla 100 MB a allowDiskUse na ně nemá vliv. Jeden dokument nesmí přesáhnout 16 MB, ať je to uložený dokument, příliš velká skupina nebo výsledek $lookup. A $facet, který spouští několik dílčích pipeline nad stejným vstupem, drží svůj výstup v jednom dokumentu, takže celý jeho výsledek se musí vejít do 16 MB.

Limit 100 MB také neznamená, že server potřebuje 100 MB volné paměti pro každou stage: je to strop a malý $group využije pár kilobajtů. Jenže na tarifu s 1 GB paměti může několik uživatelů, kteří spustí velké reporty naráz, zabrat víc, než mají cache WiredTiger a operační systém k dispozici. Sekce o dimenzování v článku schéma a indexy v MongoDB vysvětluje, jak si cache a zbytek procesu dělí paměť.

Pipeline krok za krokem: od pomalé k rychlé#

Vezmi měsíční report tržeb se jménem a tarifem zákazníka, napsaný tak, jak se píše nejsnáz:

javascript
db.orders.aggregate([  { $lookup: { from: "customers", localField: "customerId",               foreignField: "_id", as: "customer" } },  { $unwind: "$customer" },  { $match: { status: "paid", createdAt: { $gte: ISODate("2026-10-01") } } },  { $group: { _id: "$customer.plan", revenue: { $sum: "$total" }, orders: { $sum: 1 } } },  { $sort: { revenue: -1 } }])

Vrací správnou odpověď a je špatně třemi způsoby. $match stojí za $lookup, takže se spojení provádí pro každou objednávku, která kdy byla. Pole v $match patří objednávkám, takže ho optimalizátor často posune dopředu, ale tohle přepsání bys měl udělat sám. A $lookup zkopíruje celý dokument zákazníka do každé objednávky před $group, který potřebuje jen plan.

Přepsáno:

javascript
db.orders.aggregate([  { $match: { status: "paid", createdAt: { $gte: ISODate("2026-10-01") } } },  { $group: { _id: "$customerId", revenue: { $sum: "$total" }, orders: { $sum: 1 } } },  { $lookup: { from: "customers", localField: "_id",               foreignField: "_id", as: "customer" } },  { $unwind: "$customer" },  { $group: { _id: "$customer.plan", revenue: { $sum: "$revenue" }, orders: { $sum: "$orders" } } },  { $sort: { revenue: -1 } }])

Filtr využívá index { status: 1, createdAt: -1 }. První $group zredukuje proud na jeden dokument na platícího zákazníka ještě před spojením, takže $lookup běží jednou na zákazníka, ne jednou na objednávku. Druhý $group je sečte podle tarifu. Spusť explain("executionStats") u obou verzí a rozdíl uvidíš v totalDocsExamined i v čase každé stage.

Bezpečné spouštění agregací na ostré databázi#

Pár návyků zajistí, že report neshodí celý web.

  • Zkus to nejdřív na omezeném vstupu. Při vývoji dej { $limit: 1000 } hned za $match a odstraň ho, až bude výsledek správný.
  • Nastav časový limit. maxTimeMS u příkazu zastaví pipeline, která se utrhla ze řetězu: db.orders.aggregate([...], { maxTimeMS: 20000 }). Report, který nedoběhne za dvacet sekund, by neměl být webový požadavek.
  • Najdi a ukonči, co běží. db.currentOp() vypíše operace a db.killOp(opid) jednu z nich zastaví, jak je popsáno v sekci o profileru v článku schéma a indexy v MongoDB.
  • Pro reporty použij vyhrazeného uživatele. Dej mu roli read na jednu databázi, aby se překlep v pipeline nemohl dostat k $out nebo $merge do živé kolekce. Jakou roli zvolit, popisuje článek uživatelé a role v MongoDB.
  • Před jakýmkoli `$out` nebo `$merge` udělej backup. $out po dokončení pipeline nahradí cílovou kolekci. Pokud ti na cíli záleží, nejdřív udělej mongodump.

Na RE:NODE se k tarifu MongoDB dodávají vygenerované přihlašovací údaje a sloty pro backup, které můžeš spouštět ručně nebo podle plánu na záložce Schedules, což je rozumný způsob, jak udělat backup před pipeline, která zapisuje. Paměť tarifu je pevný limit: server, kterému dojde, se zastaví a čistě znovu spustí, místo aby swapoval, takže velmi velký $group v paměti na malém tarifu je důvod nejdřív přehodnotit pipeline a až potom zvažovat upgrade. Tarify najdeš na stránce hostingu databází MongoDB.

Řešení potíží#

`Exceeded memory limit for $group, but didn't allow external sort.` $group překročil 100 MB. Předej { allowDiskUse: true } a potom udělej skutečnou opravu: filtruj dřív, zahoď nepoužívaná pole přes project, nesypej přes $push celé dokumenty.

`Sort exceeded memory limit of 104857600 bytes`. Stejný limit u $sort, za kterým není index. Přidej index odpovídající řazení nebo za něj dej $limit.

`BSONObj size: ... is invalid` nebo "document exceeds maximum size 16777216". Skupina nebo pole z $lookup přerostlo 16 MB. Omez, co sbíráš, spojuj užší pipeline nebo data přestavěj.

Pipeline nic nevrací a neohlásí chybu. Většinou pole, které už na dané stage neexistuje, $unwind nad chybějícím polem nebo datum uložené jako řetězec. Zkrať pipeline na první stage, zkontroluj výstup a přidávej stage po jedné.

`$lookup` je hodně pomalý. Spusť explain("executionStats") a hledej COLLSCAN na cizí kolekci. Zaindexuj foreignField a sniž počet příchozích dokumentů.

`$match` je pomalý, přestože index existuje. Pravděpodobně to není první stage, nebo je ve filtru nesoulad typů (číslo proti řetězci), nebo začíná negací či regulárním výrazem s úvodním zástupným znakem. Podívej se na hranice IXSCAN v explain().

Výsledky se mezi spuštěními liší. $group nevrací skupiny v žádném určitém pořadí a $first a $last závisejí na pořadí vstupu. Pokud výsledek závisí na pořadí, dej dřív $sort.

FAQ#

Je aggregation pipeline rychlejší než find()?

Sama o sobě ne. Pro jednoduchý filtr dělají find() a pipeline jen s $match stejnou práci. Pipeline je rychlejší než stejná úloha v kódu aplikace, protože seskupování a spojování se děje uvnitř serveru a po síti jde jen výsledek.

Využívá $match index?

Ano, když je to první stage, nebo před ním stojí jen stage, přes které ho optimalizátor umí posunout. $match za $group, za $project s přejmenováním polí nebo za $unwind pracuje s upravenými dokumenty a index kolekce využít nemůže.

Co dělá allowDiskUse?

Dovolí blokující stage zapisovat dočasné soubory na disk, když překročí limit paměti 100 MB, místo aby selhala. Dotaz doběhne, ale pomaleji. V MongoDB 6.0 a novějším to serverový výchozí stav už povoluje, pokud správce nezměnil allowDiskUseByDefault.

Můžu spojit víc než dvě kolekce?

Ano, řetězením stage $lookup, ale každá přidá dotaz na každý příchozí dokument, takže se cena násobí. Pokud jich na horkém dotazu řetězíš víc než dvě nebo tři, data nejspíš patří do jednoho dokumentu.

Čím se $lookup liší od SQL JOIN?

Je to left outer join, který vrací shody jako pole uvnitř každého dokumentu, ne jako další řádky. Provádí jedno vyhledání na příchozí dokument a pro úplné spojení dvou velmi velkých kolekcí se nehodí. SQL databáze jsou na to stavěné; pokud jsou tvá data převážně relační, přečti si nejdřív PostgreSQL, nebo MongoDB.

Kdy předpočítat místo agregování?

Když se tentýž drahý report čte mnohem častěji, než se data mění. Spouštěj pipeline podle plánu s $merge do souhrnné kolekce a aplikace ať čte tu. Dashboard, který při každém načtení stránky znovu agreguje milion dokumentů, je obvyklá chyba.

Pomohlo vám to?