sql >> Databasteknik >  >> NoSQL >> MongoDB

Summa kapslad array i node.js mongodb

Låt oss börja med en grundläggande ansvarsfriskrivning genom att huvuddelen av vad som svarar på problemet redan har besvarats här på Find i Double Nested Array MongoDB . Och "för ordens skull" Dubbel gäller även Trippel eller Quadrupal eller NÅGON nivå av häckning som i princip samma princip ALLTID .

Den andra huvudpoängen med alla svar är också NEST inte arrayer , eftersom som förklaras i det svaret också (och jag har upprepat detta många gånger ), vilken anledning du än "tror" du har för "kapsla" faktiskt inte ger dig de fördelar som du uppfattar att det kommer att göra. Faktum är att "kapsla" är egentligen bara att göra livet mycket svårare.

Inkapslade problem

Den huvudsakliga missuppfattningen av varje översättning av en datastruktur från en "relationell" modell tolkas nästan alltid som "lägg till en kapslad arraynivå" för varje tillhörande modell. Det du presenterar här är inget undantag från denna missuppfattning eftersom den i hög grad verkar vara "normaliserad" så att varje undermatris innehåller de relaterade objekten till dess överordnade.

MongoDB är en "dokument"-baserad databas, så den låter dig göra detta eller i själva verket vilket datastrukturinnehåll du i princip vill ha. Det betyder dock inte att uppgifterna i en sådan form är lätta att arbeta med eller faktiskt är praktiska för det faktiska syftet.

Låt oss fylla i schemat med några faktiska data för att visa:

{
  "_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 är lite annorlunda än strukturen i frågan men för demonstrationsändamål har den de saker vi behöver titta på. Främst finns det en array i dokumentet som har objekt med en sub-array, som i sin tur har objekt i en sub-array och så vidare. "normalisering" här är naturligtvis av identifierarna på varje "nivå" som en "artikeltyp" eller vad du faktiskt har.

Kärnproblemet är att du bara vill ha "en del" av data från dessa kapslade arrayer, och MongoDB vill egentligen bara returnera "dokumentet", vilket betyder att du behöver göra lite manipulation för att bara komma till de matchande "sub- objekt".

Även i frågan om "korrekt" att välja dokument som matchar alla dessa "underkriterier" kräver omfattande användning av $elemMatch för att få den korrekta kombinationen av villkor på varje nivå av arrayelement. Du kan inte använda direkt "Pricknotation" på grund av behovet av de flera villkor . Utan $elemMatch påståenden du inte får den exakta "kombinationen" och bara få dokument där villkoret var sant på alla array-element.

Vad gäller faktiskt "filtrering av arrayinnehållet" då är det faktiskt den del av ytterligare skillnad:

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" }
  }}
])

Detta beskrivs bäst som "stökigt" och "inblandat". Inte bara är vår första fråga för dokumentval med $elemMatch mer än en munsbit, men sedan har vi det efterföljande $filter och $map bearbetning för varje arraynivå. Som nämnts tidigare är detta mönstret oavsett hur många nivåer det faktiskt finns.

Du kan alternativt göra en $unwind och $match kombination istället för att filtrera arrayerna på plats, men detta orsakar ytterligare overhead till $unwind innan det oönskade innehållet tas bort, så i moderna versioner av MongoDB är det generellt sett bättre praxis att $filter från arrayen först.

Slutplatsen här är att du vill $group av element som faktiskt finns i arrayen, så du måste $unwind varje nivå i arrayerna ändå före detta.

Själva "grupperingen" är då i allmänhet okomplicerad med försäljningsdatum och pris egenskaper för den första ackumulering och sedan lägga till ett efterföljande steg till $push det olika priset värden som du vill samla ett genomsnitt för inom varje datum som en sekund ackumulering.

OBS :Den faktiska hanteringen av dadlar kan mycket väl variera i praktisk användning beroende på hur detaljerat du lagrar dem. I detta exempel är alla datumen bara avrundade till början av varje "dag". Om du faktiskt behöver ackumulera riktiga "datetime"-värden, så vill du förmodligen verkligen ha en konstruktion som denna eller liknande:

{ "$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"
  }
  ...
}}

Använder $dateFromParts och andra operatorer för datumaggregation för att extrahera "dag"-informationen och presentera datumet tillbaka i den formen för ackumulering.

Börjar avnormalisera

Vad som borde framgå av "röran" ovan är att det inte är helt lätt att arbeta med kapslade arrayer. Sådana strukturer var i allmänhet inte ens möjliga att atomiskt uppdatera i utgåvor före MongoDB 3.6, och även om du aldrig ens uppdaterade dem eller levde med att ersätta i princip hela arrayen, är de fortfarande inte enkla att fråga efter. Det här är vad du visas.

Där du måste har matrisinnehåll i ett överordnat dokument rekommenderas det generellt att "platta ut" och "avnormalisera" sådana strukturer. Detta kan verka i strid med relationstänkande, men det är faktiskt det bästa sättet att hantera sådan data av prestationsskäl:

{
  "_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 är samma data som ursprungligen visades, men istället för att kapsla vi lägger faktiskt bara allt i en singularis tillplattad array inom varje överordnat dokument. Visst betyder detta dubblering av olika datapunkter, men skillnaden i frågekomplexitet och prestanda bör vara självklar:

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 istället för att kapsla dessa $elemMatch anrop och liknande för $filter uttryck, allt är mycket tydligare och lättläst och egentligen ganska enkelt i bearbetningen. Det finns en annan fördel i att du faktiskt till och med kan indexera nycklarna för elementen i arrayen som används i frågan. Det var en begränsning för de kapslade modell där MongoDB helt enkelt inte tillåter sådan "Multikey-indexering" på nycklar för arrayer inom arrayer . Med en enda array är detta tillåtet och kan användas för att förbättra prestandan.

Allt efter "matrisinnehållsfiltrering" sedan förblir exakt densamma, med undantaget är det bara sökvägsnamn som "data.sales_date" i motsats till den långrandiga "first_level.second_level.third_level.forth_level.sales_date" från den tidigare strukturen.

När ska man INTE bädda in

Slutligen är den andra stora missuppfattningen att ALLA relationer måste översättas som inbäddning i arrayer. Detta var verkligen aldrig avsikten med MongoDB och det var bara meningen att du skulle hålla "relaterad" data i samma dokument i en array i fallet där det innebar att du gjorde en enda hämtning av data i motsats till "joins".

Den klassiska "Order/Details"-modellen här gäller vanligtvis där man i den moderna världen vill visa "header" för en "Order" med detaljer som kundadress, ordersumma och så vidare inom samma "skärm" som detaljerna för olika rader på "Beställning".

Långt tillbaka i början av RDBMS hade den typiska skärmen på 80 tecken gånger 25 rader helt enkelt sådan "header"-information på en skärm, sedan var detaljraderna för allt köpt på en annan skärm. Så naturligtvis fanns det en viss nivå av sunt förnuft att lagra dem i separata tabeller. När världen utvecklades till mer detaljer på sådana "skärmar" vill du vanligtvis se hela, eller åtminstone "huvudet" och de första så många raderna i en sådan "ordning".

Därför är denna typ av arrangemang vettigt att lägga in i en array, eftersom MongoDB returnerar ett "dokument" som innehåller relaterade data på en gång. Inget behov av separata förfrågningar om separata renderade skärmar och inget behov av "joins" på sådan data eftersom den redan är "föransluten" så att säga.

Tänk på om du behöver det - AKA "Fullständigt" avnormalisera

Så i fall där du i stort sett vet att du inte är intresserad av att hantera det mesta av data i sådana arrayer för det mesta, är det generellt sett mer meningsfullt att helt enkelt lägga allt i en samling för sig med bara en annan egenskap i för att identifiera "föräldern" om sådan "anslutning" ibland skulle krävas:

{
  "_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
}

Återigen är det samma data, men just den här gången i helt separata dokument med en hänvisning till föräldern i bästa fall i det fall du faktiskt kan behöva den för ett annat ändamål. Observera att aggregeringarna här inte alls relaterar till överordnade data och det är också tydligt var den extra prestandan och den borttagna komplexiteten kommer in genom att helt enkelt lagra 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" }
  }}
])

Eftersom allt redan är ett dokument finns det inget behov av att "filtrera ner arrayer" eller har någon av de andra komplexiteten. Allt du gör är att välja de matchande dokumenten och sammanställa resultaten, med exakt samma två sista steg som har funnits hela tiden.

I syfte att bara komma till slutresultaten presterar detta mycket bättre än något av alternativen ovan. Frågan i fråga handlar egentligen bara om "detaljerad" data, därför är det bästa sättet att separera detaljerna från föräldern helt eftersom det alltid kommer att ge den bästa prestandafördelen.

Och den övergripande punkten här är var det faktiska åtkomstmönstret för resten av applikationen ALDRIG måste returnera hela arrayinnehållet, så borde det förmodligen inte ha varit inbäddat ändå. De flesta "skriv"-operationer borde till synes aldrig behöva röra den relaterade föräldern ändå, och det är en annan avgörande faktor var detta fungerar eller inte.

Slutsats

Det allmänna budskapet är återigen att du som en allmän regel aldrig bör kapsla arrayer. Som mest bör du hålla en "singular" array med delvis denormaliserade data inom det relaterade överordnade dokumentet, och där de återstående åtkomstmönstren verkligen inte använder föräldern och barnet i tandem mycket alls, då borde data verkligen separeras.

Den "stora" förändringen är att alla anledningar till att man tycker att normalisering av data faktiskt är bra, visar sig vara fienden till sådana inbäddade dokumentsystem. Att undvika "joins" är alltid bra, men att skapa komplexa kapslade strukturer för att se ut som "joined" data fungerar aldrig riktigt till din fördel heller.

Kostnaden för att hantera vad du "tror" är normalisering slutar vanligtvis med att den extra lagringen och underhållet av duplicerade och denormaliserade data inom din eventuella lagring slutar.

Observera också att alla formulär ovan returnerar samma resultatuppsättning. Det är ganska härledd genom att exempeldata för korthetens skull bara inkluderar enstaka artiklar, eller som mest där det finns flera prispunkter är "genomsnittet" fortfarande 1 eftersom det är vad alla värden är i alla fall. Men innehållet för att förklara detta är redan oerhört långt så det är egentligen bara "genom exempel":

{
        "_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
}



  1. MongoDB $toInt

  2. Hur man går igenom kapslade dokument rekursivt i MongoDB

  3. MongoDB $indexOfBytes

  4. Bästa sättet att fråga alla dokument från en mongodb-samling på ett reaktivt sätt utan översvämning av RAM