Lad os starte med en grundlæggende ansvarsfraskrivelse, idet hoveddelen af, hvad der besvarer problemet, allerede er blevet besvaret her på Find i Double Nested Array MongoDB . Og "for ordens skyld" Dobbelt gælder også for Triple eller Quadrupal eller ENHVER Indlejringsniveau som grundlæggende det samme princip ALTID .
Det andet hovedpunkt i ethvert svar er også Don't NEST Arrays , da som det også er forklaret i det svar (og jeg har gentaget dette mange gange ), uanset hvilken grund du "tror" du har til "nesting" faktisk ikke giver dig de fordele, som du opfatter det vil. Faktisk "nesting" er egentlig bare at gøre livet langt sværere.
Indlejrede problemer
Den største misforståelse af enhver oversættelse af en datastruktur fra en "relationel" model fortolkes næsten altid som "tilføj et indlejret array-niveau" for hver tilknyttede model. Det, du præsenterer her, er ingen undtagelse fra denne misforståelse, da den meget ser ud til at være "normaliseret" så hvert underarray indeholder de relaterede elementer til dets overordnede.
MongoDB er en "dokument" baseret database, så den giver dig stort set mulighed for at gøre dette eller faktisk et hvilket som helst datastrukturindhold, du dybest set ønsker. Det betyder dog ikke, at data i en sådan form er nemme at arbejde med eller faktisk er praktiske til det faktiske formål.
Lad os udfylde skemaet med nogle faktiske data for at demonstrere:
{
"_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
}
]
}
]
}
]
}
]
}
Det er lidt anderledes end strukturen i spørgsmålet, men til demonstrationsformål har det de ting, vi skal se på. Hovedsageligt er der et array i dokumentet, som har elementer med et sub-array, som igen har elementer i et sub-array og så videre. "normalisering" her er selvfølgelig ved identifikatorerne på hvert "niveau" som en "varetype" eller hvad du faktisk har.
Kerneproblemet er, at du bare vil have "nogle" af dataene inde fra disse indlejrede arrays, og MongoDB vil egentlig bare returnere "dokumentet", hvilket betyder, at du skal lave noget manipulation for bare at komme til de matchende "sub- varer".
Selv om spørgsmålet om "korrekt" at vælge det dokument, der matcher alle disse "underkriterier", kræver omfattende brug af $elemMatch
for at få den korrekte kombination af betingelser på hvert niveau af array-elementer. Du kan ikke bruge direkte "Priknotation"
på grund af behovet for disse flere betingelser
. Uden $elemMatch
udsagn får du ikke den nøjagtige "kombination" og får bare dokumenter, hvor betingelsen var sand på enhver array-element.
Hvad angår faktisk "filtrering af array-indholdet" så er det faktisk den del af yderligere forskel:
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" }
}}
])
Dette beskrives bedst som "rodet" og "involveret". Ikke kun er vores indledende forespørgsel om dokumentvalg med $elemMatch
mere end en mundfuld, men så har vi det efterfølgende $filter
og $map
behandling for hvert array-niveau. Som tidligere nævnt er dette mønsteret, uanset hvor mange niveauer der faktisk er.
Du kan alternativt lave en $unwind
og $match
kombination i stedet for at filtrere arrays på plads, men dette forårsager yderligere overhead til $unwind
før det uønskede indhold fjernes, så i moderne udgivelser af MongoDB er det generelt bedre praksis at $filter
fra arrayet først.
Slutstedet her er, at du vil $group
af elementer, der faktisk er inde i arrayet, så du ender med at skulle $unwind
hvert niveau af arrays alligevel før dette.
Den faktiske "gruppering" er så generelt ligetil ved at bruge sales_date
og pris
egenskaber for den første akkumulering og derefter tilføje en efterfølgende fase til $push
den forskellige pris
værdier, du vil akkumulere et gennemsnit for inden for hver dato som et sekund ophobning.
BEMÆRK :Den faktiske håndtering af dadler kan meget vel variere i praktisk brug afhængigt af, hvor detaljeret du opbevarer dem. I denne prøve er datoerne alle allerede afrundet til begyndelsen af hver "dag". Hvis du faktisk har brug for at akkumulere rigtige "datetime"-værdier, så vil du sandsynligvis virkelig have en konstruktion som denne eller lignende:
{ "$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"
}
...
}}
Brug af $dateFromParts
og andre datoaggregationsoperatører
at udtrække "dag"-oplysningerne og præsentere datoen tilbage i den form for akkumulering.
Begynder at denormalisere
Hvad der burde være klart fra "rodet" ovenfor er, at det ikke er ligefrem let at arbejde med indlejrede arrays. Sådanne strukturer var generelt ikke engang mulige at atomært opdatere i udgivelser før MongoDB 3.6, og selvom du aldrig engang opdaterede dem eller levede med at erstatte stort set hele arrayet, er de stadig ikke nemme at forespørge på. Dette er, hvad du bliver vist.
Hvor du skal har matrixindhold i et overordnet dokument, anbefales det generelt at "fladder" og "denormalisere" sådanne strukturer. Dette kan virke i modstrid med relationel tænkning, men det er faktisk den bedste måde at håndtere sådanne data på af præstationsmæssige årsager:
{
"_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
}
]
}
Det er alle de samme data som oprindeligt vist, men i stedet for indlejring vi har faktisk bare lagt alt ind i et enkelt fladt array i hvert overordnet dokument. Det betyder selvfølgelig duplikering af forskellige datapunkter, men forskellen i forespørgselskompleksitet og ydeevne burde være indlysende:
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" }
}}
])
Nu i stedet for at indlejre disse $elemMatch
opkald og lignende for $filter
udtryk, alt er meget klarere og let at læse og egentlig ret simpelt i behandlingen. Der er en anden fordel ved, at du faktisk endda kan indeksere nøglerne til elementerne i arrayet som brugt i forespørgslen. Det var en begrænsning for de indlejrede model, hvor MongoDB simpelthen ikke tillader sådan "Multikey-indeksering" på taster til arrays i arrays . Med et enkelt array er dette tilladt og kan bruges til at forbedre ydeevnen.
Alt efter "matrixindholdsfiltrering" så forbliver det nøjagtigt det samme, med undtagelsen er det bare stinavne som "data.sales_date"
i modsætning til den langvarige "first_level.second_level.third_level.forth_level.sales_date"
fra den tidligere struktur.
Hvornår skal man IKKE integrere
Endelig er den anden store misforståelse, at ALL Relations skal oversættes som indlejring i arrays. Dette har aldrig været hensigten med MongoDB, og det var kun meningen, at du skulle opbevare "relaterede" data i det samme dokument i et array i det tilfælde, hvor det betød en enkelt hentning af data i modsætning til "joins".
Den klassiske "Ordre/Detaljer"-model her gælder typisk, hvor man i den moderne verden ønsker at vise "header" for en "Ordre" med detaljer som kundeadresse, ordretotal og så videre inden for samme "skærm" som detaljerne på forskellige linjeposter på "Ordre".
Helt tilbage i starten af RDBMS havde den typiske skærm på 80 tegn gange 25 linjer simpelthen sådanne "header"-oplysninger på én skærm, så var detaljelinjerne for alt købt på en anden skærm. Så naturligvis var der en vis grad af sund fornuft at gemme dem i separate tabeller. Efterhånden som verden bevægede sig til flere detaljer på sådanne "skærme", vil du typisk gerne se det hele, eller i det mindste "headeren" og de første så mange linjer i sådan en "ordre".
Derfor er det fornuftigt at sætte denne slags arrangement i et array, da MongoDB returnerer et "dokument", der indeholder de relaterede data på én gang. Intet behov for separate anmodninger om separate gengivet skærmbilleder og intet behov for "joins" på sådanne data, da de som det var allerede er "pre-joined".
Overvej om du har brug for det - AKA "Fuldstændig" Denormalize
Så i tilfælde, hvor du stort set ved, at du faktisk ikke er interesseret i at håndtere de fleste af dataene i sådanne arrays det meste af tiden, giver det generelt mere mening blot at lægge det hele i én samling alene med blot en anden ejendom i for at identificere "forælderen", hvis en sådan "tilslutning" lejlighedsvis kræves:
{
"_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
}
Igen er det de samme data, men netop denne gang i helt separate dokumenter med en henvisning til forælderen i bedste fald i det tilfælde, hvor du rent faktisk har brug for dem til et andet formål. Bemærk, at aggregeringerne her alle slet ikke relaterer til de overordnede data, og det er også tydeligt, hvor den ekstra ydeevne og fjernede kompleksitet kommer ind ved blot at gemme i en separat samling:
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" }
}}
])
Da alt allerede er et dokument, er det ikke nødvendigt at "filtrere arrays" eller har nogen af de andre kompleksiteter. Alt du gør er at vælge de matchende dokumenter og samle resultaterne med nøjagtig de samme to sidste trin, som har været til stede hele tiden.
Med det formål bare at komme til de endelige resultater, fungerer dette langt bedre end begge ovenstående alternativer. Den pågældende forespørgsel drejer sig egentlig kun om "detaljerede" data, derfor er den bedste fremgangsmåde at adskille detaljerne fra forælderen fuldstændigt, da det altid vil give den bedste ydeevne.
Og det overordnede punkt her er, hvor det faktiske adgangsmønster for resten af applikationen ALDRIG skal returnere hele array-indholdet, så burde det nok ikke have været indlejret alligevel. Tilsyneladende skulle de fleste "skrive"-operationer på samme måde aldrig behøve at røre den relaterede forælder alligevel, og det er en anden afgørende faktor, hvor dette virker eller ikke gør.
Konklusion
Den generelle besked er igen, at du som hovedregel aldrig bør indlejre arrays. Du bør højst beholde en "enkelt" array med delvist denormaliserede data i det relaterede overordnede dokument, og hvor de resterende adgangsmønstre overhovedet ikke bruger forælderen og barnet i tandem meget, så burde dataene virkelig adskilles.
Den "store" ændring er, at alle grundene til, at du synes, at normalisering af data faktisk er godt, viser sig at være fjenden af sådanne indlejrede dokumentsystemer. At undgå "joins" er altid godt, men at skabe kompleks indlejret struktur for at få udseendet af "joined" data virker heller aldrig til din fordel.
Omkostningerne ved at beskæftige sig med det, du "tror" er normalisering, ender sædvanligvis med at overgå den ekstra lagring og vedligeholdelse af duplikerede og denormaliserede data i dit eventuelle lager.
Bemærk også, at alle formularer ovenfor returnerer det samme resultatsæt. Det er ret afledt, idet prøvedataene for kortheds skyld kun inkluderer enkeltstående varer, eller højst, hvor der er flere prispunkter, er "gennemsnittet" stadig 1
da det er hvad alle værdierne alligevel er. Men indholdet til at forklare dette er allerede overordentlig langt, så det er egentlig bare "ved eksempel":
{
"_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
}