MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Soma matriz aninhada em node.js mongodb


Vamos começar com um aviso básico, pois o corpo principal do que responde ao problema já foi respondido aqui em Encontre na matriz aninhada dupla MongoDB . E "para registro" o Double também se aplica ao Triplo ou Quadrupal ou QUALQUER nível de aninhamento como basicamente o mesmo princípio SEMPRE .

O outro ponto principal de qualquer resposta também é Não NEST Arrays , já que também é explicado nessa resposta (e eu repeti isso muitos times ), qualquer que seja o motivo pelo qual você "pensa" você tem para "aninhamento" na verdade não lhe dá as vantagens que você percebe que dará. Na verdade "aninhamento" está realmente apenas tornando a vida muito mais difícil.

Problemas aninhados


O principal equívoco de qualquer tradução de uma estrutura de dados de um modelo "relacional" é quase sempre interpretado como "adicionar um nível de matriz aninhada" para cada modelo associado. O que você está apresentando aqui não é exceção a esse equívoco, pois parece muito "normalizado" para que cada sub-matriz contenha os itens relacionados a seu pai.

O MongoDB é um banco de dados baseado em "documentos", então ele permite que você faça isso ou, de fato, qualquer conteúdo de estrutura de dados que você basicamente queira. No entanto, isso não significa que os dados em tal formato sejam fáceis de trabalhar ou mesmo práticos para o propósito real.

Vamos preencher o esquema com alguns dados reais para demonstrar:
{
  "_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
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Isso é um "pouco" diferente da estrutura da questão, mas para fins de demonstração, tem as coisas que precisamos observar. Principalmente, há um array no documento que possui itens com um sub-array, que por sua vez possui itens em um sub-array e assim por diante. A "normalização" aqui é claro pelos identificadores em cada "nível" como um "tipo de item" ou o que você realmente tem.

O problema principal é que você quer apenas "alguns" dos dados de dentro dessas matrizes aninhadas, e o MongoDB realmente quer apenas retornar o "documento", o que significa que você precisa fazer alguma manipulação para obter apenas aqueles "sub- Itens".

Mesmo na questão de "corretamente" selecionar o documento que corresponda a todos esses "subcritérios" requer o uso extensivo de $elemMatch para obter a combinação correta de condições em cada nível de elementos da matriz. Você não pode usar diretamente "Notação de ponto" devido à necessidade desses múltiplas condições . Sem o $elemMatch declarações você não obtém a "combinação" exata e apenas obtém documentos em que a condição era verdadeira em qualquer elemento de matriz.

Quanto a "filtrar o conteúdo do array" então essa é realmente a parte da diferença adicional:
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" }
  }}
])

Isso é melhor descrito como "bagunçado" e "envolvido". Nossa consulta inicial para seleção de documentos não é apenas com o $elemMatch mais do que um bocado, mas então temos o subsequente $filter e $map processamento para cada nível de array. Como mencionado anteriormente, este é o padrão, não importa quantos níveis realmente existam.

Você também pode fazer um $unwind e $match combinação em vez de filtrar as matrizes no local, mas isso causa sobrecarga adicional para $unwind antes que o conteúdo indesejado seja removido, portanto, em versões modernas do MongoDB, geralmente é uma prática melhor $filtro da matriz primeiro.

O ponto final aqui é que você deseja $group por elementos que estão realmente dentro do array, então você acaba precisando $unwind cada nível das matrizes de qualquer maneira antes disso.

O "agrupamento" real é geralmente direto usando o sales_date e preço propriedades para o primeiro acumulação e, em seguida, adicionar um estágio subsequente a $push o preço diferente valores para os quais você deseja acumular uma média em cada data como um segundo acumulação.

OBSERVAÇÃO :O manuseio real de datas pode variar em uso prático, dependendo da granularidade em que você as armazena. Nesta amostra, as datas já estão todas arredondadas para o início de cada "dia". Se você realmente precisa acumular valores "datetime" reais, provavelmente realmente deseja uma construção como esta ou semelhante:
{ "$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"
  }
  ...
}}

Usando $dateFromParts e outros operadores de agregação de data extrair as informações do "dia" e apresentar a data de volta nesse formulário para acumulação.

Começando a desnormalizar


O que deve ficar claro da "bagunça" acima é que trabalhar com matrizes aninhadas não é exatamente fácil. Essas estruturas geralmente nem eram possíveis de atualizar atomicamente em versões anteriores ao MongoDB 3.6, e mesmo se você nunca as atualizou ou viveu substituindo basicamente todo o array, elas ainda não são simples de consultar. Isto é o que você está sendo mostrado.

Onde você deve tem conteúdo de matriz em um documento pai, geralmente é aconselhável "achatar" e "desnormalizar" tais estruturas. Isso pode parecer contrário ao pensamento relacional, mas na verdade é a melhor maneira de lidar com esses dados por motivos de desempenho:
{
  "_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
     }
  ]
}

São todos os mesmos dados mostrados originalmente, mas em vez de aninhar na verdade, apenas colocamos tudo em um array achatado singular dentro de cada documento pai. Claro que isso significa duplicação de vários pontos de dados, mas a diferença na complexidade e desempenho da consulta deve ser evidente:
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" }
  }}
])

Agora, em vez de aninhar esses $elemMatch chamadas e da mesma forma para o $filter expressões, tudo é muito mais claro e fácil de ler e realmente muito simples no processamento. Há outra vantagem em que você pode até indexar as chaves dos elementos na matriz, conforme usado na consulta. Essa era uma restrição do aninhado modelo em que o MongoDB simplesmente não permitirá tal "Indexação multichave" em chaves de arrays dentro de arrays . Com um único array isso é permitido e pode ser usado para melhorar o desempenho.

Tudo depois da "filtragem de conteúdo de matriz" então permanece exatamente o mesmo, com a exceção de que são apenas nomes de caminho como "data.sales_date" ao contrário do prolixo "first_level.second_level.third_level.forth_level.sales_date" da estrutura anterior.

Quando NÃO incorporar


Finalmente, o outro grande equívoco é que TODAS as relações precisam ser traduzidos como incorporação em matrizes. Isso realmente nunca foi a intenção do MongoDB e você só deveria manter dados "relacionados" dentro do mesmo documento em uma matriz no caso em que isso significasse fazer uma única recuperação de dados em oposição a "junções".

O modelo clássico "Pedido/Detalhes" aqui normalmente se aplica onde no mundo moderno você deseja exibir "cabeçalho" para um "Pedido" com detalhes como endereço do cliente, total do pedido e assim por diante na mesma "tela" que os detalhes do diferentes itens de linha no "Pedido".

No início do RDBMS, a tela típica de 80 caracteres por 25 linhas simplesmente tinha essas informações de "cabeçalho" em uma tela, e as linhas de detalhes de tudo o que era comprado estavam em uma tela diferente. Então, naturalmente, havia algum nível de bom senso para armazená-los em tabelas separadas. À medida que o mundo se move para mais detalhes nessas "telas", você normalmente quer ver a coisa toda, ou pelo menos o "cabeçalho" e as primeiras tantas linhas de tal "ordem".

Daí por que esse tipo de arranjo faz sentido colocar em uma matriz, já que o MongoDB retorna um "documento" contendo os dados relacionados de uma só vez. Não há necessidade de solicitações separadas para telas renderizadas separadas e não há necessidade de "junções" nesses dados, pois já estão "pré-juntados" por assim dizer.

Considere se você precisar - AKA "Totalmente" Desnormalização


Portanto, nos casos em que você sabe que não está realmente interessado em lidar com a maioria dos dados em tais matrizes na maioria das vezes, geralmente faz mais sentido simplesmente colocar tudo em uma coleção própria com apenas outra propriedade em a fim de identificar o "pai" caso tal "junção" seja ocasionalmente necessária:
{
  "_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
}

Novamente, são os mesmos dados, mas desta vez em documentos completamente separados com uma referência ao pai, na melhor das hipóteses, no caso de você realmente precisar deles para outra finalidade. Observe que as agregações aqui não estão relacionadas aos dados pai e também fica claro onde o desempenho adicional e a complexidade removida vêm simplesmente armazenando em uma coleção separada:
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" }
  }}
])

Como tudo já é um documento, não há necessidade de "filtrar arrays" ou tem qualquer outra complexidade. Tudo o que você está fazendo é selecionar os documentos correspondentes e agregar os resultados, com exatamente as mesmas duas etapas finais que estiveram presentes o tempo todo.

Com o propósito de apenas obter os resultados finais, isso funciona muito melhor do que qualquer uma das alternativas acima. A consulta em questão está realmente preocupada apenas com os dados "detalhe", portanto, o melhor curso de ação é separar completamente os detalhes do pai, pois sempre fornecerá o melhor benefício de desempenho.

E o ponto geral aqui é onde o padrão de acesso real do resto do aplicativo NUNCA precisa retornar todo o conteúdo da matriz, então provavelmente não deveria ter sido incorporado de qualquer maneira. Aparentemente, a maioria das operações de "gravação" da mesma forma nunca precisa tocar o pai relacionado de qualquer maneira, e esse é outro fator decisivo onde isso funciona ou não.

Conclusão


A mensagem geral é novamente que, como regra geral, você nunca deve aninhar arrays. No máximo, você deve manter uma matriz "singular" com dados parcialmente desnormalizados dentro do documento pai relacionado, e onde os padrões de acesso restantes realmente não usam muito o pai e o filho em conjunto, os dados realmente devem ser separados.

A "grande" mudança é que todas as razões pelas quais você acha que normalizar dados é realmente bom, acaba sendo o inimigo de tais sistemas de documentos embutidos. Evitar "junções" é sempre bom, mas criar uma estrutura aninhada complexa para ter a aparência de dados "juntados" também nunca funciona para seu benefício.

O custo de lidar com o que você "pensa" é normalização geralmente acaba superando o armazenamento adicional e a manutenção de dados duplicados e desnormalizados em seu armazenamento eventual.

Observe também que todos os formulários acima retornam o mesmo conjunto de resultados. É bastante derivado, pois os dados de amostra para brevidade incluem apenas itens singulares ou, no máximo, onde há vários pontos de preço, a "média" ainda é 1 já que é isso que todos os valores são de qualquer maneira. Mas o conteúdo para explicar isso já é excessivamente longo, então é apenas "por exemplo":
{
        "_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
}