MongoDB
 sql >> база данни >  >> NoSQL >> MongoDB

Сумиране на вложен масив в node.js mongodb

Нека започнем с основен отказ от отговорност, тъй като основната част от това, което отговаря на проблема, вече е получила отговор тук на Намиране в двойно вложен масив MongoDB . И "за протокола"Двойникът важи и заТройката или Quadrupal или ВСЯКАКВИ ниво на вмъкване като основно същия принципВИНАГИ .

Другата основна точка на всеки отговор също е Не поставяйте масиви , тъй като както е обяснено и в този отговор (и аз повторих това много пъти ), независимо от причината, която „мислите“ имате за "гнездене" всъщност не ви дава предимствата, които възприемате. Всъщност "гнездене" всъщност просто прави живота много по-труден.

Вложени проблеми

Основното погрешно схващане на всеки превод на структура от данни от "релационен" модел почти винаги се тълкува като "добавяне на ниво на вложен масив" за всеки свързан модел. Това, което представяте тук, не е изключение от това погрешно схващане, тъй като до голяма степен изглежда "нормализирано" така че всеки подмасив да съдържа свързаните елементи с неговия родител.

MongoDB е база данни, базирана на „документ“, така че до голяма степен ви позволява да правите това или всъщност всяко съдържание на структура от данни, което основно искате. Това обаче не означава, че данните в такава форма са лесни за работа или наистина практични за действителната цел.

Нека попълним схемата с някои действителни данни, за да демонстрираме:

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

Това е "малко" различно от структурата във въпроса, но за демонстрационни цели има нещата, които трябва да разгледаме. Основно в документа има масив, който има елементи с подмасив, който от своя страна има елементи в подмасив и т.н. "Нормализиране" тук разбира се според идентификаторите на всяко „ниво“ като „тип елемент“ или каквото всъщност имате.

Основният проблем е, че вие ​​просто искате „някои“ от данните от тези вложени масиви, а MongoDB наистина иска просто да върне „документа“, което означава, че трябва да направите някои манипулации, за да стигнете до онези, които съответстват на „под- елементи".

Дори по въпроса за "правилно" избирането на документа, който отговаря на всички тези „подкритерии“, изисква широко използване на $elemMatch за да получите правилната комбинация от условия на всяко ниво от елементи на масива. Не можете да използвате направо "Dot Notation" поради необходимостта от тези множество условия . Без $elemMatch изявления, не получавате точната „комбинация“ и просто получавате документи, където условието е вярно на всеки елемент от масив.

Що се отнася до действителното "филтриране на съдържанието на масива" тогава това всъщност е частта от допълнителната разлика:

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

Това най-добре се описва като „объркано“ и „замесено“. Първоначалната ни заявка за избор на документ е не само с $elemMatch повече от пълна хапка, но след това имаме последващия $filter и $map обработка за всяко ниво на масив. Както споменахме по-рано, това е моделът, без значение колко нива всъщност има.

Можете алтернативно да направите $unwind и $match комбинация вместо филтриране на масивите на място, но това причинява допълнителни разходи за $unwind преди нежеланото съдържание да бъде премахнато, така че в съвременните версии на MongoDB обикновено е по-добре да $filter първо от масива.

Крайното място тук е, че искате да $group по елементи, които всъщност са вътре в масива, така че в крайна сметка трябва да $unwind всяко ниво на масивите така или иначе преди това.

Действителното „групиране“ след това обикновено е лесно с помощта на sales_date и цена имоти за прив натрупване и след това добавяне на следващ етап към $push различната цена стойности, за които искате да натрупате средна стойност в рамките на всяка дата като секунда натрупване.

ЗАБЕЛЕЖКА :Действителното боравене с фурми може да варира в практическата употреба в зависимост от степента на детайлност, в която ги съхранявате. В тази извадка всички дати вече са закръглени до началото на всеки „ден“. Ако наистина трябва да натрупате реални стойности за "datetime", тогава вероятно наистина искате конструкция като тази или подобна:

{ "$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 и други оператори за агрегиране на дати за извличане на информацията за „деня“ и представяне на датата обратно в тази форма за натрупване.

Започване на денормализиране

Това, което трябва да стане ясно от "бъркотията" по-горе е, че работата с вложени масиви не е съвсем лесна. Такива структури обикновено дори не беше възможно да се актуализират атомарно в издания преди MongoDB 3.6 и дори ако никога не сте ги актуализирали или сте живели с подмяната на целия масив, те все още не са лесни за заявка. Това е, което ви се показва.

Къдетотрябва имат съдържание на масив в рамките на родителски документ, обикновено се препоръчва да се "изравни" и "денормализиране" такива структури. Това може да изглежда в противоречие с релационното мислене, но всъщност е най-добрият начин за обработка на такива данни от съображения за ефективност:

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

Това са същите данни, както са показани първоначално, но вместо вложени ние всъщност просто поставяме всичко в единичен сплескан масив във всеки родителски документ. Разбира се, това означава дублиране на различни точки от данни, но разликата в сложността на заявката и производителността трябва да е очевидна:

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

Сега вместо да влагате тези $elemMatch обаждания и по подобен начин за $filter изрази, всичко е много по-ясно и лесно за четене и наистина доста просто за обработка. Има друго предимство в това, че всъщност можете дори да индексирате ключовете на елементите в масива, както се използват в заявката. Това беше ограничение на вложените модел, при който MongoDB просто няма да позволи такова "Многоключово индексиране" върху ключове на масиви в масиви . С единичен масив това е разрешено и може да се използва за подобряване на производителността.

Всичко след "филтриране на съдържание на масив" след това остава абсолютно същото, с изключение, че това са само имена на пътища като "data.sales_date" за разлика от дълготрайния "first_level.second_level.third_level.forth_level.sales_date" от предишната структура.

Кога НЕ трябва да се вгражда

И накрая, другото голямо погрешно схващане е, че ВСИЧКИ отношения трябва да се преведе като вграждане в масиви. Това наистина никога не е било намерението на MongoDB и вие сте имали за цел да запазите „свързани“ данни в рамките на един и същи документ в масив в случай, че това означава да направите еднократно извличане на данни, за разлика от „съединения“.

Класическият модел „Поръчка/подробности“ тук обикновено се прилага, когато в съвременния свят искате да покажете „заглавка“ за „Поръчка“ с подробности като адрес на клиента, обща сума на поръчката и така нататък в рамките на същия „екран“ като подробностите за различни договорени позиции в „Поръчка“.

Още в началото на RDBMS, типичният екран с 80 знака на 25 реда просто имаше такава информация за „заглавка“ на един екран, след което редовете с подробности за всичко закупено бяха на различен екран. Така че естествено имаше някакво ниво на здрав разум да се съхраняват тези в отделни таблици. С напредването на света към повече детайли на такива „екрани“ обикновено искате да видите цялото нещо или поне „заглавката“ и първите толкова много редове от такъв „ред“.

Ето защо този вид подреждане има смисъл да се постави в масив, тъй като MongoDB връща „документ“, съдържащ всички свързани данни наведнъж. Няма нужда от отделни заявки за отделни изобразени екрани и няма нужда от „съединявания“ на такива данни, тъй като те вече са „предварително обединени“, така да се каже.

Помислете дали имате нужда от това - още известно като „Напълно“ денормализиране

Така че в случаите, в които до голяма степен знаете, че всъщност не се интересувате да работите с повечето от данните в такива масиви през повечето време, обикновено има по-голям смисъл просто да ги поставите в една колекция сама по себе си само с друго свойство в за да се идентифицира "родител", ако такова "присъединяване" се изисква от време на време:

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

Отново това са същите данни, но този път в напълно отделни документи с препратка към родителя в най-добрия случай, когато може действително да ви трябват за друга цел. Обърнете внимание, че всички агрегирания тук изобщо не са свързани с родителските данни и също така е ясно къде идва допълнителната производителност и премахнатата сложност чрез просто съхраняване в отделна колекция:

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

Тъй като всичко вече е документ, няма нужда от "филтриране на масиви" или имат някоя от другите сложности. Всичко, което правите, е да изберете съвпадащите документи и да обедините резултатите с абсолютно същите две последни стъпки, които са присъствали през цялото време.

За целите на самото достигане до крайните резултати, това се представя много по-добре от всяка от горните алтернативи. Въпросната заявка наистина се занимава само с „подробните“ данни, следователно най-добрият курс на действие е да отделите напълно детайлите от родителя, тъй като винаги ще осигури най-добрата полза от производителността.

И общата точка тук е къде е действителният модел на достъп на останалата част от приложението НИКОГА трябва да върне цялото съдържание на масива, тогава вероятно не е трябвало да бъде вграден така или иначе. Привидно повечето операции за "запис" така или иначе никога не трябва да докосват свързания родител и това е друг решаващ фактор, когато това работи или не.

Заключение

Общото послание отново е, че като общо правило никога не трябва да влагате масиви. Най-много трябва да запазите "единствен" масив с частично денормализирани данни в рамките на свързания родителски документ и когато останалите модели за достъп наистина не използват родителя и детето в тандем много, тогава данните наистина трябва да бъдат разделени.

„Голямата“ промяна е, че всички причини, поради които смятате, че нормализирането на данни е всъщност добро, се оказват враг на такива вградени системи за документи. Избягването на „съединения“ винаги е добре, но създаването на сложна вложена структура, за да има вид на „съединени“ данни, никога не работи във ваша полза.

Разходите за справяне с това, което „мислите“ за нормализиране, обикновено завършва с надхвърляне на допълнителното съхранение и поддръжка на дублирани и денормализирани данни в рамките на вашето евентуално хранилище.

Също така имайте предвид, че всички формуляри по-горе връщат един и същ набор от резултати. Това е доста производно, тъй като примерните данни за краткост включват само единични артикули или най-много там, където има няколко ценови точки, „средната стойност“ все още е 1 тъй като това са всички стойности. Но съдържанието за обяснение на това вече е изключително дълго, така че всъщност е само „по пример“:

{
        "_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. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Извеждане на основен HTML изглед?

  2. Mongodb - Как да намеря низ в множество полета?

  3. свързване към локален mongodb от докер контейнер

  4. Как да проектираме актуализирани стойности само с помощта на findOneAndUpdate във вграден масив Mongoose?

  5. Проектиране на схема на база данни MongoDB