sql >> Base de Datos >  >> NoSQL >> MongoDB

Sumar matriz anidada en node.js mongodb

Comencemos con un descargo de responsabilidad básico en el sentido de que el cuerpo principal de lo que responde al problema ya se respondió aquí en Buscar en matriz doble anidada MongoDB . Y "para que conste" el Doble también se aplica a Triple o Cuádruple o CUALQUIERA nivel de anidamiento como básicamente el mismo principio SIEMPRE .

El otro punto principal de cualquier respuesta también es No NEST Arrays , ya que como se explica en esa respuesta también (y he repetido esto muchas veces), cualquiera que sea la razón por la que "piensas" tienes para "anidar" en realidad no le brinda las ventajas que cree que tendrá. De hecho "anidación" realmente está haciendo la vida mucho más difícil.

Problemas anidados

El concepto erróneo principal de cualquier traducción de una estructura de datos de un modelo "relacional" casi siempre se interpreta como "agregar un nivel de matriz anidado" para todos y cada uno de los modelos asociados. Lo que está presentando aquí no es una excepción a este concepto erróneo, ya que parece estar "normalizado" para que cada subarreglo contenga los elementos relacionados con su padre.

MongoDB es una base de datos basada en "documentos", por lo que prácticamente le permite hacer esto o, de hecho, cualquier contenido de estructura de datos que básicamente desee. Sin embargo, eso no significa que sea fácil trabajar con los datos en tal forma o que sea práctico para el propósito real.

Completemos el esquema con algunos datos reales para demostrar:

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

Eso es un "poco" diferente de la estructura en la pregunta, pero para fines de demostración, tiene las cosas que debemos observar. Principalmente hay una matriz en el documento que tiene elementos con una sub-matriz, que a su vez tiene elementos en una sub-matriz y así sucesivamente. La "normalización" aquí está, por supuesto, por los identificadores en cada "nivel" como un "tipo de elemento" o lo que sea que tenga.

El problema central es que solo quiere "algunos" de los datos de estas matrices anidadas, y MongoDB realmente solo quiere devolver el "documento", lo que significa que necesita hacer alguna manipulación para llegar a los "sub- artículos".

Incluso en el tema de "correctamente" seleccionar el documento que coincida con todos estos "subcriterios" requiere un uso extensivo de $elemMatch para obtener la combinación correcta de condiciones en cada nivel de los elementos de la matriz. No puede usar directamente "Notación de puntos" debido a la necesidad de aquellos múltiples condiciones . Sin $elemMatch declaraciones no obtiene la "combinación" exacta y solo obtiene documentos donde la condición era verdadera en cualquier elemento de matriz.

En cuanto a realmente "filtrar el contenido de la matriz" entonces esa es en realidad la parte de la diferencia 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" }
  }}
])

Esto se describe mejor como "desordenado" e "involucrado". No solo es nuestra consulta inicial para la selección de documentos con $elemMatch más que un bocado, pero luego tenemos el siguiente $filter y $map procesamiento para cada nivel de matriz. Como se mencionó anteriormente, este es el patrón sin importar cuántos niveles haya en realidad.

Alternativamente, podría hacer un $unwind y $match combinación en lugar de filtrar las matrices en su lugar, pero esto genera una sobrecarga adicional para $unwind antes de que se elimine el contenido no deseado, por lo que en las versiones modernas de MongoDB generalmente es una mejor práctica $filter de la matriz primero.

El lugar final aquí es que desea $group por elementos que realmente están dentro de la matriz, por lo que termina necesitando $unwind cada nivel de las matrices de todos modos antes de esto.

La "agrupación" real es generalmente sencilla utilizando sales_date y price propiedades para el primero acumulación, y luego agregar una etapa posterior a $push el diferente price valores para los que desea acumular un promedio dentro de cada fecha como un segundo acumulación.

NOTA :El manejo real de las fechas puede variar en el uso práctico dependiendo de la granularidad con la que las almacene. En esta muestra, las fechas ya están redondeadas al comienzo de cada "día". Si realmente necesita acumular valores reales de "fecha y hora", entonces probablemente realmente desee una construcción como esta o similar:

{ "$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 y otros operadores de agregación de fechas para extraer la información del "día" y presentar la fecha en ese formulario para la acumulación.

Comenzando a desnormalizarse

Lo que debería quedar claro del "desorden" anterior es que trabajar con arreglos anidados no es exactamente fácil. En general, tales estructuras ni siquiera eran posibles de actualizar atómicamente en versiones anteriores a MongoDB 3.6, e incluso si nunca las actualizó o vivió reemplazando básicamente toda la matriz, todavía no son fáciles de consultar. Esto es lo que se te está mostrando.

Donde debes tiene contenido de matriz dentro de un documento principal, generalmente se recomienda "aplanar" y "desnormalizar" tales estructuras. Esto puede parecer contrario al pensamiento relacional, pero en realidad es la mejor manera de manejar dichos datos por motivos de rendimiento:

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

Todos son los mismos datos que se mostraron originalmente, pero en lugar de anidar en realidad, solo ponemos todo en una matriz aplanada singular dentro de cada documento principal. Seguro que esto significa duplicación de varios puntos de datos, pero la diferencia en la complejidad y el rendimiento de la consulta debería 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" }
  }}
])

Ahora, en lugar de anidar esos $elemMatch llamadas y de manera similar para el $filter expresiones, todo es mucho más claro y fácil de leer y realmente bastante simple en el procesamiento. Hay otra ventaja en el sentido de que incluso puede indexar las claves de los elementos de la matriz tal como se utilizan en la consulta. Esa fue una restricción del anidado modelo donde MongoDB simplemente no permitirá tal "Indización de claves múltiples" en claves de matrices dentro de matrices . Con una sola matriz, esto está permitido y se puede utilizar para mejorar el rendimiento.

Todo después del "filtrado de contenido de matriz" luego permanece exactamente igual, con la excepción de que son solo nombres de ruta como "data.sales_date" a diferencia del extenso "first_level.second_level.third_level.forth_level.sales_date" de la estructura anterior.

Cuándo NO insertar

Finalmente, el otro gran error es que TODAS las relaciones debe traducirse como incrustación dentro de matrices. Esta nunca fue realmente la intención de MongoDB y solo tenía la intención de mantener los datos "relacionados" dentro del mismo documento en una matriz en el caso de que significara hacer una única recuperación de datos en lugar de "uniones".

El modelo clásico de "Pedido/Detalles" generalmente se aplica en el mundo moderno en el que desea mostrar el "encabezado" de un "Pedido" con detalles como la dirección del cliente, el total del pedido, etc. dentro de la misma "pantalla" que los detalles de diferentes elementos de línea en el "Pedido".

Hace mucho tiempo en el inicio del RDBMS, la pantalla típica de 80 caracteres por 25 líneas simplemente tenía esa información de "encabezado" en una pantalla, luego las líneas detalladas para todo lo comprado estaban en una pantalla diferente. Entonces, naturalmente, había cierto nivel de sentido común para almacenarlos en tablas separadas. A medida que el mundo avanza hacia más detalles en tales "pantallas", normalmente querrá ver todo, o al menos el "encabezado" y las primeras líneas de tal "orden".

Por eso tiene sentido poner este tipo de disposición en una matriz, ya que MongoDB devuelve un "documento" que contiene todos los datos relacionados a la vez. No hay necesidad de solicitudes separadas para pantallas procesadas separadas y no hay necesidad de "combinar" dichos datos, ya que ya están "pre-unidos".

Considere si lo necesita, también conocido como desnormalización "totalmente"

Entonces, en los casos en los que sabe que no está realmente interesado en manejar la mayoría de los datos en dichas matrices la mayor parte del tiempo, generalmente tiene más sentido simplemente ponerlo todo en una colección por sí solo con simplemente otra propiedad en para identificar al "padre" en caso de que se requiera ocasionalmente dicha "unión":

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

Nuevamente, son los mismos datos, pero esta vez en documentos completamente separados con una referencia al padre en el mejor de los casos en el caso de que realmente los necesite para otro propósito. Tenga en cuenta que las agregaciones aquí no se relacionan en absoluto con los datos principales y también está claro dónde entran el rendimiento adicional y la complejidad eliminada simplemente almacenando en una colección 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" }
  }}
])

Dado que todo ya es un documento, no hay necesidad de "filtrar matrices" o tener cualquiera de las otras complejidades. Todo lo que está haciendo es seleccionar los documentos coincidentes y agregar los resultados, con exactamente los mismos dos pasos finales que han estado presentes todo el tiempo.

Con el propósito de llegar a los resultados finales, esto funciona mucho mejor que cualquiera de las alternativas anteriores. La consulta en cuestión realmente solo se ocupa de los datos de "detalle", por lo tanto, el mejor curso de acción es separar completamente el detalle del padre, ya que siempre proporcionará el mejor beneficio de rendimiento.

Y el punto general aquí es que el patrón de acceso real del resto de la aplicación NUNCA necesita devolver todo el contenido de la matriz, entonces probablemente no debería haberse incrustado de todos modos. Aparentemente, la mayoría de las operaciones de "escritura" nunca deberían necesitar tocar el padre relacionado de todos modos, y ese es otro factor decisivo en el que esto funciona o no.

Conclusión

El mensaje general es nuevamente que, como regla general, nunca debe anidar matrices. Como máximo, debe mantener una matriz "singular" con datos parcialmente desnormalizados dentro del documento principal relacionado, y donde los patrones de acceso restantes realmente no usan el padre y el hijo en conjunto, entonces los datos realmente deberían estar separados.

El "gran" cambio es que todas las razones por las que cree que la normalización de datos es realmente buena, resultan ser el enemigo de tales sistemas de documentos incrustados. Evitar las "uniones" siempre es bueno, pero crear una estructura anidada compleja para tener la apariencia de datos "unidos" tampoco funciona realmente para su beneficio.

El costo de lidiar con lo que "piensas" que es la normalización generalmente termina superando el almacenamiento adicional y el mantenimiento de datos duplicados y desnormalizados dentro de tu almacenamiento final.

También tenga en cuenta que todos los formularios anteriores devuelven el mismo conjunto de resultados. Es bastante derivado en el sentido de que los datos de muestra por brevedad solo incluyen artículos singulares o, como máximo, donde hay múltiples puntos de precio, el "promedio" sigue siendo 1 ya que eso es lo que son todos los valores de todos modos. Pero el contenido para explicar esto ya es demasiado largo, por lo que en realidad es solo "por ejemplo":

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