Začněme základním prohlášením, že hlavní část odpovědí na problém již byla zodpovězena zde na Najít v Double Nested Array MongoDB . A "pro pořádek" Double platí také pro Trojitý nebo Quadrupal nebo JAKÉKOLI úroveň vnoření jako v podstatě stejný princip VŽDY .
Dalším hlavním bodem jakékoli odpovědi je také Don't NEST Arrays , protože jak je vysvětleno v této odpovědi (a toto mnoho jsem opakoval krát ), z jakéhokoli důvodu "myslíte" máte pro "vnoření" ve skutečnosti vám nedává výhody, které si myslíte, že ano. Ve skutečnosti "vnoření" ve skutečnosti jen dělá život mnohem těžším.
Vnořené problémy
Hlavní mylná představa jakéhokoli překladu datové struktury z "relačního" modelu je téměř vždy interpretována jako "přidat úroveň vnořeného pole" pro každý související model. To, co zde prezentujete, není výjimkou z této mylné představy, protože se velmi zdá být "normalizované" takže každé dílčí pole obsahuje položky související s jeho nadřazeným prvkem.
MongoDB je databáze založená na „dokumentech“, takže vám v podstatě umožňuje dělat toto nebo ve skutečnosti jakýkoli obsah datové struktury, který v podstatě chcete. To však neznamená, že s daty v takové podobě je snadné pracovat nebo jsou skutečně praktické pro skutečný účel.
Vyplňte schéma některými skutečnými daty pro demonstraci:
{
"_id": 1,
"first_level": [
{
"first_item": "A",
"second_level": [
{
"second_item": "A",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-11-01"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-11-02"),
"quantity": 1
},
]
},
{
"third_item": "B",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
]
}
]
},
{
"second_item": "A",
"third_level": [
{
"third_item": "B",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
]
}
]
}
]
},
{
"first_item": "A",
"second_level": [
{
"second_item": "B",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
]
}
]
}
]
}
]
},
{
"_id": 2,
"first_level": [
{
"first_item": "A",
"second_level": [
{
"second_item": "A",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 2,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
}
]
}
]
}
]
},
{
"_id": 3,
"first_level": [
{
"first_item": "A",
"second_level": [
{
"second_item": "B",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
}
]
}
]
}
]
}
To se "trochu" liší od struktury v otázce, ale pro demonstrační účely obsahuje věci, na které se musíme podívat. V dokumentu je hlavně pole, které má položky s dílčím polem, které má zase položky v dílčím poli a tak dále. "normalizace" zde je samozřejmě podle identifikátorů na každé "úrovni" jako "typ položky" nebo cokoli, co ve skutečnosti máte.
Základním problémem je, že chcete jen „některá“ data z těchto vnořených polí, a MongoDB skutečně chce vrátit „dokument“, což znamená, že musíte provést nějakou manipulaci, abyste se dostali k odpovídajícím „sub- položky".
I v otázce "správně" výběr dokumentu, který odpovídá všem těmto „podkritériím“, vyžaduje rozsáhlé používání $elemMatch
za účelem získání správné kombinace podmínek na každé úrovni prvků pole. Nelze použít přímo nahoru "Dot Notation"
kvůli potřebě těchto více podmínek
. Bez $elemMatch
prohlášení, nezískáte přesnou „kombinaci“ a pouze získáte dokumenty, u kterých byla podmínka pravdivá na jakémkoli prvek pole.
Pokud jde o skutečné "filtrování obsahu pole" pak je to vlastně část dalšího rozdílu:
db.collection.aggregate([
{ "$match": {
"first_level": {
"$elemMatch": {
"first_item": "A",
"second_level": {
"$elemMatch": {
"second_item": "A",
"third_level": {
"$elemMatch": {
"third_item": "A",
"forth_level": {
"$elemMatch": {
"sales_date": {
"$gte": new Date("2018-11-01"),
"$lt": new Date("2018-12-01")
}
}
}
}
}
}
}
}
}
}},
{ "$addFields": {
"first_level": {
"$filter": {
"input": {
"$map": {
"input": "$first_level",
"in": {
"first_item": "$$this.first_item",
"second_level": {
"$filter": {
"input": {
"$map": {
"input": "$$this.second_level",
"in": {
"second_item": "$$this.second_item",
"third_level": {
"$filter": {
"input": {
"$map": {
"input": "$$this.third_level",
"in": {
"third_item": "$$this.third_item",
"forth_level": {
"$filter": {
"input": "$$this.forth_level",
"cond": {
"$and": [
{ "$gte": [ "$$this.sales_date", new Date("2018-11-01") ] },
{ "$lt": [ "$$this.sales_date", new Date("2018-12-01") ] }
]
}
}
}
}
}
},
"cond": {
"$and": [
{ "$eq": [ "$$this.third_item", "A" ] },
{ "$gt": [ { "$size": "$$this.forth_level" }, 0 ] }
]
}
}
}
}
}
},
"cond": {
"$and": [
{ "$eq": [ "$$this.second_item", "A" ] },
{ "$gt": [ { "$size": "$$this.third_level" }, 0 ] }
]
}
}
}
}
}
},
"cond": {
"$and": [
{ "$eq": [ "$$this.first_item", "A" ] },
{ "$gt": [ { "$size": "$$this.second_level" }, 0 ] }
]
}
}
}
}},
{ "$unwind": "$first_level" },
{ "$unwind": "$first_level.second_level" },
{ "$unwind": "$first_level.second_level.third_level" },
{ "$unwind": "$first_level.second_level.third_level.forth_level" },
{ "$group": {
"_id": {
"date": "$first_level.second_level.third_level.forth_level.sales_date",
"price": "$first_level.second_level.third_level.forth_level.price",
},
"quantity_sold": {
"$avg": "$first_level.second_level.third_level.forth_level.quantity"
}
}},
{ "$group": {
"_id": "$_id.date",
"prices": {
"$push": {
"price": "$_id.price",
"quanity_sold": "$quantity_sold"
}
},
"quanity_sold": { "$avg": "$quantity_sold" }
}}
])
Nejlépe se to dá popsat jako „nepořádný“ a „zúčastněný“. Nejen, že je náš počáteční dotaz na výběr dokumentu pomocí $elemMatch
víc než sousto, ale pak tu máme následný $filter
a $map
zpracování pro každou úroveň pole. Jak již bylo zmíněno dříve, toto je vzor bez ohledu na to, kolik úrovní ve skutečnosti je.
Alternativně můžete provést $unwind
a $match
kombinaci namísto filtrování polí na místě, ale to způsobuje další režii na $unwind
před odstraněním nežádoucího obsahu, takže v moderních verzích MongoDB je obecně lepší postup $filter
nejprve z pole.
Konečným místem je, že chcete $group
prvky, které jsou ve skutečnosti uvnitř pole, takže nakonec budete muset $unwind
každá úroveň polí před tímto.
Vlastní "seskupení" je pak obecně jednoduché pomocí sales_date
a cena
vlastnosti pro první akumulaci a poté přidání další fáze do $push
jiná cena
hodnoty, pro které chcete shromáždit průměr pro každé datum jako sekundu akumulace.
POZNÁMKA :Skutečná manipulace s datly se může při praktickém použití lišit v závislosti na granularitě, ve které je ukládáte. V tomto příkladu jsou všechna data již zaokrouhlena na začátek každého "dne". Pokud skutečně potřebujete shromáždit skutečné hodnoty „datetime“, pak pravděpodobně opravdu chcete konstrukci jako je tato nebo podobná:
{ "$group": {
"_id": {
"date": {
"$dateFromParts": {
"year": { "$year": "$first_level.second_level.third_level.forth_level.sales_date" },
"month": { "$month": "$first_level.second_level.third_level.forth_level.sales_date" },
"day": { "$dayOfMonth": "$first_level.second_level.third_level.forth_level.sales_date" }
}
}.
"price": "$first_level.second_level.third_level.forth_level.price"
}
...
}}
Pomocí $dateFromParts
a další operátoři data agregace
extrahovat informace o „dnech“ a předložit datum zpět v této formě pro akumulaci.
Zahájení denormalizace
Z výše uvedeného „nepořádku“ by mělo být jasné, že práce s vnořenými poli není zrovna jednoduchá. Takové struktury obecně nebylo ani možné atomicky aktualizovat ve verzích před MongoDB 3.6, a i když jste je nikdy neaktualizovali nebo jste žili s výměnou v podstatě celého pole, stále není snadné je dotazovat. To je to, co se vám zobrazuje.
Kde musíte mají-li obsah pole v nadřazeném dokumentu, obecně se doporučuje "sloučit" a "denormalizovat" takové struktury. To se může zdát v rozporu se vztahovým myšlením, ale ve skutečnosti je to nejlepší způsob, jak s takovými daty nakládat z důvodů výkonu:
{
"_id": 1,
"data": [
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-01"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-02"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
]
},
{
"_id": 2,
"data": [
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 2,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
},
{
"_id": 3,
"data": [
{
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
}
To jsou všechna stejná data, jaká byla původně zobrazena, ale namísto vnoření ve skutečnosti jsme vše vložili do singulárního zploštělého pole v každém nadřazeném dokumentu. Jistě to znamená duplikaci různých datových bodů, ale rozdíl ve složitosti dotazu a výkonu by měl být samozřejmý:
db.collection.aggregate([
{ "$match": {
"data": {
"$elemMatch": {
"first_item": "A",
"second_item": "A",
"third_item": "A",
"sales_date": {
"$gte": new Date("2018-11-01"),
"$lt": new Date("2018-12-01")
}
}
}
}},
{ "$addFields": {
"data": {
"$filter": {
"input": "$data",
"cond": {
"$and": [
{ "$eq": [ "$$this.first_item", "A" ] },
{ "$eq": [ "$$this.second_item", "A" ] },
{ "$eq": [ "$$this.third_item", "A" ] },
{ "$gte": [ "$$this.sales_date", new Date("2018-11-01") ] },
{ "$lt": [ "$$this.sales_date", new Date("2018-12-01") ] }
]
}
}
}
}},
{ "$unwind": "$data" },
{ "$group": {
"_id": {
"date": "$data.sales_date",
"price": "$data.price",
},
"quantity_sold": { "$avg": "$data.quantity" }
}},
{ "$group": {
"_id": "$_id.date",
"prices": {
"$push": {
"price": "$_id.price",
"quantity_sold": "$quantity_sold"
}
},
"quantity_sold": { "$avg": "$quantity_sold" }
}}
])
Nyní namísto vnořování těchto $elemMatch
volání a podobně pro $filter
výrazů, vše je mnohem přehlednější a dobře čitelné a opravdu celkem jednoduché na zpracování. Další výhodou je, že můžete dokonce indexovat klíče prvků v poli, jak je použito v dotazu. To bylo omezení vnořených model, kde MongoDB prostě nedovolí takové "Multikey indexování" na klíčích polí v polích . S jediným polem je to povoleno a lze to použít ke zlepšení výkonu.
Vše po "filtrování obsahu pole" pak zůstává úplně stejný, s výjimkou jsou to jen názvy cest jako "data.sales_date"
na rozdíl od dlouhého "first_level.second_level.third_level.forth_level.sales_date"
z předchozí struktury.
Kdy NEVLOŽIT
A konečně další velkou mylnou představou je, že VŠECHNY vztahy je třeba přeložit jako vložení do polí. Toto ve skutečnosti nikdy nebylo záměrem MongoDB a měli jste vždy uchovávat „související“ data v rámci stejného dokumentu v poli v případě, kdy to znamenalo provést jediné načtení dat na rozdíl od „připojení“.
Klasický model „Objednávka/Podrobnosti“ se zde obvykle používá tam, kde v moderním světě chcete zobrazit „záhlaví“ „Objednávky“ s podrobnostmi, jako je adresa zákazníka, celková částka objednávky atd. na stejné „obrazovce“ jako podrobnosti různé řádkové položky v "Objednávce".
V počátcích RDBMS měla typická obrazovka o velikosti 80 znaků na 25 řádků jednoduše takové „záhlaví“ informace na jedné obrazovce, pak byly řádky podrobností pro vše zakoupené na jiné obrazovce. Takže přirozeně existovala určitá míra zdravého rozumu ukládat je do samostatných tabulek. Jak se svět na takových „obrazovkách“ posouval do detailů, obvykle chcete vidět celou věc, nebo alespoň „záhlaví“ a prvních tolik řádků takového „pořadu“.
Proto má smysl vkládat tento druh uspořádání do pole, protože MongoDB vrací „dokument“ obsahující související data všechna najednou. Není potřeba samostatných požadavků na samostatné vykreslené obrazovky a není potřeba „připojení“ k takovým datům, protože jsou již „předpřipojená“, jak to bylo.
Zvažte, zda to potřebujete - AKA "Plně" Denormalizovat
Takže v případech, kdy v podstatě víte, že se většinu času nezajímáte o většinu dat v takových polích, je obecně smysluplnější dát je všechna do jedné samostatné kolekce s pouze jinou vlastností v k identifikaci „rodiče“, pokud by takové „spojení“ bylo občas vyžadováno:
{
"_id": 1,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"_id": 2,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-01"),
"quantity": 1
},
{
"_id": 3,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-02"),
"quantity": 1
},
{
"_id": 4,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"_id": 5,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 6,
"parent_id": 1,
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 7,
"parent_id": 2,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 2,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 8,
"parent_id": 2,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"_id": 9,
"parent_id": 2,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 10,
"parent_id": 3,
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
Opět se jedná o stejná data, ale tentokrát ve zcela samostatných dokumentech s odkazem na nadřazeného v nejlepším případě v případě, že byste je skutečně potřebovali k jinému účelu. Všimněte si, že všechny zde uvedené agregace se vůbec nevztahují k nadřazeným datům a je také jasné, kde přichází další výkon a odstraněná složitost pouhým uložením do samostatné kolekce:
db.collection.aggregate([
{ "$match": {
"first_item": "A",
"second_item": "A",
"third_item": "A",
"sales_date": {
"$gte": new Date("2018-11-01"),
"$lt": new Date("2018-12-01")
}
}},
{ "$group": {
"_id": {
"date": "$sales_date",
"price": "$price"
},
"quantity_sold": { "$avg": "$quantity" }
}},
{ "$group": {
"_id": "$_id.date",
"prices": {
"$push": {
"price": "$_id.price",
"quantity_sold": "$quantity_sold"
}
},
"quantity_sold": { "$avg": "$quantity_sold" }
}}
])
Protože vše je již dokument, není třeba "filtrovat pole" nebo mají jakoukoli jinou složitost. Vše, co děláte, je výběr odpovídajících dokumentů a agregace výsledků s přesně stejnými dvěma závěrečnými kroky, které byly po celou dobu přítomny.
Za účelem pouhého získání konečných výsledků to funguje mnohem lépe než kterákoli z výše uvedených alternativ. Dotyčný dotaz se ve skutečnosti týká pouze „podrobných“ dat, proto je nejlepším postupem zcela oddělit detail od nadřazeného prvku, protože to vždy poskytne nejlepší výkon.
A celkovým bodem je, kde skutečný přístupový vzor zbytku aplikace NIKDY potřebuje vrátit celý obsah pole, pak by pravděpodobně stejně nemělo být vloženo. Zdá se, že většina operací „zápisu“ by se stejně nikdy neměla dotknout příbuzného rodiče, a to je další rozhodující faktor, kde to funguje nebo ne.
Závěr
Obecnou zprávou je opět to, že jako obecné pravidlo byste nikdy neměli vnořovat pole. Nanejvýš byste měli v souvisejícím nadřazeném dokumentu ponechat „singulární“ pole s částečně denormalizovanými daty a tam, kde zbývající přístupové vzory opravdu příliš nepoužívají rodiče a potomka v tandemu, pak by data skutečně měla být oddělena.
„Velkou“ změnou je, že všechny důvody, proč si myslíte, že normalizace dat je vlastně dobrá, se ukázaly být nepřítelem takových systémů vestavěných dokumentů. Vyhýbat se „spojení“ je vždy dobré, ale vytváření složité vnořené struktury, která bude mít vzhled „spojených“ dat, také nikdy nebude ve váš prospěch.
Náklady na vypořádání se s tím, co si "myslíte" je normalizace, obvykle skončí mimo dodatečné ukládání a údržbu duplicitních a denormalizovaných dat v rámci vašeho případného úložiště.
Všimněte si také, že všechny výše uvedené formuláře vracejí stejnou sadu výsledků. Je to docela odvozené v tom, že vzorová data pro stručnost zahrnují pouze jednotlivé položky, nebo pokud existuje více cenových bodů, "průměr" je stále 1
protože to jsou stejně všechny hodnoty. Ale obsah, který to vysvětluje, je už tak nesmírně dlouhý, takže je to opravdu jen "příkladem":
{
"_id" : ISODate("2018-11-01T00:00:00Z"),
"prices" : [
{
"price" : 1,
"quantity_sold" : 1
}
],
"quantity_sold" : 1
}
{
"_id" : ISODate("2018-11-02T00:00:00Z"),
"prices" : [
{
"price" : 1,
"quantity_sold" : 1
}
],
"quantity_sold" : 1
}
{
"_id" : ISODate("2018-11-03T00:00:00Z"),
"prices" : [
{
"price" : 1,
"quantity_sold" : 1
},
{
"price" : 2,
"quantity_sold" : 1
}
],
"quantity_sold" : 1
}