sql >> Database >  >> NoSQL >> MongoDB

Som geneste array in node.js mongodb

Laten we beginnen met een fundamentele disclaimer in die zin dat het belangrijkste deel van wat het probleem oplost hier al is beantwoord op Find in dubbel geneste array MongoDB . En "voor de goede orde" de Double geldt ook voor Triple of Viermaal of ELKE niveau van nesten als in principe hetzelfde principe ALTIJD .

Het andere hoofdpunt van elk antwoord is ook Niet-NEST-arrays , aangezien zoals ook in dat antwoord wordt uitgelegd (en ik heb dit vele herhaald tijden ), welke reden u ook "denkt" je hebt voor "nesting" geeft u eigenlijk niet de voordelen die u denkt dat het zal doen. In feite "nesten" maakt het leven echt veel moeilijker.

Geneste problemen

De belangrijkste misvatting van elke vertaling van een gegevensstructuur van een "relationeel" model wordt vrijwel altijd geïnterpreteerd als "voeg een genest array-niveau toe" voor elk bijbehorend model. Wat u hier presenteert, vormt geen uitzondering op deze misvatting, aangezien het erg "genormaliseerd" lijkt te zijn zodat elke sub-array de gerelateerde items aan zijn bovenliggende matrix bevat.

MongoDB is een op "document" gebaseerde database, dus het stelt je vrijwel in staat om dit te doen of in feite elke datastructuur-inhoud die je in principe wilt. Dat betekent echter niet dat de gegevens in een dergelijke vorm gemakkelijk zijn om mee te werken of zelfs praktisch zijn voor het eigenlijke doel.

Laten we het schema invullen met enkele actuele gegevens om te demonstreren:

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

Dat is een "beetje" anders dan de structuur in de vraag, maar voor demonstratiedoeleinden heeft het de dingen waar we naar moeten kijken. Er is voornamelijk een array in het document met items met een subarray, die op zijn beurt items in een subarray heeft, enzovoort. De "normaliseren" hier is natuurlijk door de identifiers op elk "niveau" als een "itemtype" of wat je ook daadwerkelijk hebt.

Het kernprobleem is dat je gewoon "sommige" gegevens uit deze geneste arrays wilt, en MongoDB wil eigenlijk gewoon het "document" retourneren, wat betekent dat je wat manipulatie moet doen om alleen bij die overeenkomende "sub- artikelen".

Zelfs over de kwestie van "correct" het selecteren van het document dat aan al deze "subcriteria" voldoet, vereist uitgebreid gebruik van $elemMatch om de juiste combinatie van voorwaarden op elk niveau van array-elementen te krijgen. U kunt "Dot Notation" niet rechtstreeks gebruiken vanwege de behoefte aan die meerdere voorwaarden . Zonder de $elemMatch verklaringen krijgt u niet de exacte "combinatie" en krijgt u alleen documenten waarin de voorwaarde waar was op elke array-element.

Wat betreft het feitelijk "het uitfilteren van de array-inhoud" dan is dat eigenlijk het deel van het extra verschil:

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

Dit is het best te omschrijven als "rommelig" en "betrokken". Niet alleen is onze eerste vraag voor documentselectie met de $elemMatch meer dan een mondvol, maar dan hebben we de volgende $filter en $map verwerking voor elk array-niveau. Zoals eerder vermeld, is dit het patroon, ongeacht hoeveel niveaus er daadwerkelijk zijn.

U kunt ook een $unwind doen en $match combinatie in plaats van de aanwezige arrays te filteren, maar dit veroorzaakt wel extra overhead voor $unwind voordat de ongewenste inhoud wordt verwijderd, dus in moderne versies van MongoDB is het over het algemeen beter om $filter eerst uit de array.

De eindplaats hier is dat u $group door elementen die zich daadwerkelijk in de array bevinden, dus uiteindelijk moet je $unwind elk niveau van de arrays daarvoor.

De eigenlijke "groepering" is dan over het algemeen eenvoudig met behulp van de sales_date en prijs eigenschappen voor de eerste accumulatie, en vervolgens een volgende fase toe te voegen aan $push de verschillende prijs waarden waarvoor u binnen elke datum een ​​gemiddelde wilt berekenen als seconde accumulatie.

OPMERKING :De daadwerkelijke verwerking van dadels kan in praktisch gebruik variëren, afhankelijk van de granulariteit waarop u ze opslaat. In dit voorbeeld zijn de datums allemaal net afgerond naar het begin van elke "dag". Als je echt echte "datetime"-waarden moet verzamelen, dan wil je waarschijnlijk echt een constructie als deze of iets dergelijks:

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

$dateFromParts gebruiken en andere operators voor datumaggregatie om de "dag" -informatie te extraheren en de datum in die vorm weer te geven voor accumulatie.

Begint te denormaliseren

Wat duidelijk moet zijn uit de "puinhoop" hierboven, is dat het werken met geneste arrays niet bepaald eenvoudig is. Dergelijke structuren waren over het algemeen niet eens mogelijk om atomair bij te werken in releases vóór MongoDB 3.6, en zelfs als je ze nooit hebt bijgewerkt of je hebt geleefd met het vervangen van in wezen de hele array, zijn ze nog steeds niet eenvoudig te doorzoeken. Dit is wat je te zien krijgt.

Waar je moet array-inhoud binnen een bovenliggend document hebben, wordt over het algemeen aangeraden om te "afvlakken" en "denormaliseren" dergelijke structuren. Dit lijkt misschien in strijd met relationeel denken, maar het is eigenlijk de beste manier om met dergelijke gegevens om te gaan vanwege prestatieredenen:

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

Dat zijn allemaal dezelfde gegevens als oorspronkelijk getoond, maar in plaats van nesten we hebben eigenlijk alles in een enkelvoudig afgeplatte array geplaatst binnen elk bovenliggend document. Natuurlijk betekent dit duplicatie van verschillende gegevenspunten, maar het verschil in complexiteit en prestatie van query's zou duidelijk moeten zijn:

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 in plaats van die $elemMatch calls en gelijkaardig voor de $filter uitdrukkingen, alles is veel duidelijker en gemakkelijk te lezen en echt heel eenvoudig in verwerking. Er is nog een voordeel dat u zelfs de sleutels van de elementen in de array kunt indexeren zoals die in de query worden gebruikt. Dat was een beperking van de geneste model waar MongoDB dergelijke "Multikey indexing" op sleutels van arrays binnen arrays . Met een enkele array is dit toegestaan ​​en kan het worden gebruikt om de prestaties te verbeteren.

Alles na de "array content filtering" blijft dan precies hetzelfde, met de uitzondering dat het alleen padnamen zijn zoals "data.sales_date" in tegenstelling tot de langdradige "first_level.second_level.third_level.forth_level.sales_date" van de vorige structuur.

Wanneer NIET insluiten

Ten slotte is de andere grote misvatting dat ALLE relaties moeten worden vertaald als insluiten in arrays. Dit was echt nooit de bedoeling van MongoDB en het was altijd de bedoeling dat je "gerelateerde" gegevens binnen hetzelfde document in een array bewaart in het geval dat het betekende dat je een enkele keer gegevens moest ophalen in plaats van "joins".

Het klassieke "Order/Details"-model is hier meestal van toepassing wanneer u in de moderne wereld "header" voor een "Order" wilt weergeven met details zoals klantadres, ordertotaal enzovoort binnen hetzelfde "scherm" als de details van verschillende regelitems op de "Order".

Lang geleden in het begin van het RDBMS, had het typische scherm van 80 tekens bij 25 regels gewoon zulke "kop" -informatie op het ene scherm, en de detailregels voor alles wat gekocht was op een ander scherm. Dus natuurlijk was er een zekere mate van gezond verstand om die in aparte tabellen op te slaan. Naarmate de wereld meer details op dergelijke "schermen" kreeg, wil je meestal het hele ding zien, of in ieder geval de "kop" en de eerste regels van zo'n "bestelling".

Vandaar dat dit soort opstelling zinvol is om in een array te plaatsen, aangezien MongoDB een "document" retourneert dat de gerelateerde gegevens in één keer bevat. Geen behoefte aan afzonderlijke verzoeken voor afzonderlijke weergegeven schermen en geen behoefte aan "joins" op dergelijke gegevens, omdat deze als het ware al "pre-joined" zijn.

Overweeg of je het nodig hebt - AKA "Volledig" Denormaliseren

Dus in gevallen waarin je vrijwel zeker weet dat je niet echt geïnteresseerd bent in het omgaan met de meeste gegevens in dergelijke arrays, is het over het algemeen logischer om alles eenvoudig in één verzameling op zichzelf te plaatsen met alleen een andere eigenschap in om de "ouder" te identificeren mocht een dergelijke "toetreding" af en toe nodig zijn:

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

Nogmaals, het zijn dezelfde gegevens, maar deze keer in volledig afzonderlijke documenten met op zijn best een verwijzing naar de ouder in het geval dat u het misschien voor een ander doel nodig heeft. Merk op dat de aggregaties hier allemaal helemaal geen betrekking hebben op de bovenliggende gegevens en het is ook duidelijk waar de extra prestaties en verwijderde complexiteit binnenkomen door ze simpelweg op te slaan in een aparte verzameling:

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

Aangezien alles al een document is, is het niet nodig om "arrays te filteren" of een van de andere complexiteiten hebben. Het enige dat u hoeft te doen, is de overeenkomende documenten selecteren en de resultaten samenvoegen, met precies dezelfde twee laatste stappen die altijd al aanwezig waren.

Om alleen maar tot de uiteindelijke resultaten te komen, presteert dit veel beter dan beide bovenstaande alternatieven. De betreffende query houdt zich eigenlijk alleen bezig met de "detail"-gegevens, daarom is de beste manier om de details volledig te scheiden van de bovenliggende gegevens, omdat dit altijd het beste prestatievoordeel oplevert.

En het algemene punt hier is waar het daadwerkelijke toegangspatroon van de rest van de applicatie NOOIT moet de volledige array-inhoud retourneren, dan had deze waarschijnlijk toch niet moeten worden ingesloten. Schijnbaar zouden de meeste "schrijf"-bewerkingen op dezelfde manier nooit de verwante ouder hoeven te raken, en dat is een andere beslissende factor waar dit wel of niet werkt.

Conclusie

De algemene boodschap is opnieuw dat je als algemene regel nooit arrays mag nesten. U zou hoogstens een "enkelvoud" array met gedeeltelijk gedenormaliseerde gegevens binnen het gerelateerde bovenliggende document moeten houden, en waar de resterende toegangspatronen de ouder en het kind niet veel tegelijk gebruiken, dan zouden de gegevens echt moeten worden gescheiden.

De "grote" verandering is dat alle redenen waarom je denkt dat het normaliseren van gegevens eigenlijk goed is, de vijand blijken te zijn van dergelijke embedded documentsystemen. Het is altijd goed om "joins" te vermijden, maar het creëren van een complexe geneste structuur om het uiterlijk van "joined" data te laten lijken, werkt ook nooit echt in uw voordeel.

De kosten van het omgaan met wat u 'denkt' dat normalisatie is, gaat meestal de extra opslag en het onderhoud van gedupliceerde en gedenormaliseerde gegevens binnen uw uiteindelijke opslag te boven.

Merk ook op dat alle bovenstaande formulieren dezelfde resultatenset retourneren. Het is behoorlijk afgeleid omdat de voorbeeldgegevens voor de beknoptheid alleen afzonderlijke items bevatten, of hoogstens waar er meerdere prijspunten zijn, is het "gemiddelde" nog steeds 1 aangezien dat is wat alle waarden zijn hoe dan ook. Maar de inhoud om dit uit te leggen is al buitengewoon lang, dus het is eigenlijk gewoon "door voorbeeld":

{
        "_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. System.Type opslaan met MongoDb

  2. Conceptdocumenten in Mongo

  3. Mongoideus niet in vraag

  4. MongoDB C#-stuurprogramma 2.0:het resultaat van MapReduceAsync verkrijgen?