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

Agrupe y cuente usando el marco de agregación

Parece que empezaste con esto, pero te perdiste en algunos de los otros conceptos. Hay algunas verdades básicas cuando se trabaja con matrices en documentos, pero empecemos donde lo dejó:

db.sample.aggregate([
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 }
    }}
])

Entonces eso solo usará el $group tubería para recopilar sus documentos sobre los diferentes valores del campo "estado" y luego también producir otro campo para "recuento" que, por supuesto, "cuenta" las ocurrencias de la clave de agrupación al pasar un valor de 1 al $sum operador para cada documento encontrado. Esto lo coloca en un punto muy parecido al que describe:

{ "_id" : "done", "count" : 2 }
{ "_id" : "canceled", "count" : 1 }

Esa es la primera etapa de esto y bastante fácil de entender, pero ahora necesita saber cómo obtener valores de una matriz. Entonces podrías sentirte tentado una vez que entiendas la "notación de puntos" concepto correctamente para hacer algo como esto:

db.sample.aggregate([
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$devices.cost" }
    }}
])

Pero lo que encontrará es que el "total" será de hecho 0 para cada uno de esos resultados:

{ "_id" : "done", "count" : 2, "total" : 0 }
{ "_id" : "canceled", "count" : 1, "total" : 0 }

¿Por qué? Bueno, las operaciones de agregación de MongoDB como esta en realidad no atraviesan los elementos de la matriz cuando se agrupan. Para hacer eso, el marco de agregación tiene un concepto llamado $unwind . El nombre es relativamente autoexplicativo. Una matriz incrustada en MongoDB es muy similar a tener una asociación de "uno a muchos" entre fuentes de datos vinculadas. Entonces, qué $unwind hace es exactamente ese tipo de resultado de "unión", donde los "documentos" resultantes se basan en el contenido de la matriz y la información duplicada para cada padre.

Entonces, para actuar sobre los elementos de la matriz, debe usar $unwind primero. Esto debería llevarte lógicamente a un código como este:

db.sample.aggregate([
    { "$unwind": "$devices" },
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$devices.cost" }
    }}
])

Y luego el resultado:

{ "_id" : "done", "count" : 4, "total" : 700 }
{ "_id" : "canceled", "count" : 2, "total" : 350 }

Pero eso no está del todo bien, ¿verdad? Recuerda lo que acabas de aprender de $unwind y ¿cómo se une una desnormalizada con la información de los padres? Entonces ahora eso está duplicado para cada documento ya que ambos tenían dos miembros de matriz. Entonces, mientras que el campo "total" es correcto, el "recuento" es el doble de lo que debería ser en cada caso.

Se debe tener un poco más de cuidado, así que en lugar de hacer esto en un solo $group etapa, se hace en dos:

db.sample.aggregate([
    { "$unwind": "$devices" },
    { "$group": {
        "_id": "$_id",
        "status": { "$first": "$status" },
        "total": { "$sum": "$devices.cost" }
    }},
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$total" }
    }}
])

Que ahora obtiene el resultado con los totales correctos:

{ "_id" : "canceled", "count" : 1, "total" : 350 }
{ "_id" : "done", "count" : 2, "total" : 700 }

Ahora los números son correctos, pero todavía no es exactamente lo que estás pidiendo. Creo que debería detenerse allí, ya que el tipo de resultado que espera realmente no es adecuado para un solo resultado de la agregación por sí sola. Está buscando que el total esté "dentro" del resultado. Realmente no pertenece allí, pero con datos pequeños está bien:

db.sample.aggregate([
    { "$unwind": "$devices" },
    { "$group": {
        "_id": "$_id",
        "status": { "$first": "$status" },
        "total": { "$sum": "$devices.cost" }
    }},
    { "$group": {
        "_id": "$status",
        "count": { "$sum": 1 },
        "total": { "$sum": "$total" }
    }},
    { "$group": {
        "_id": null,
        "data": { "$push": { "count": "$count", "total": "$total" } },
        "totalCost": { "$sum": "$total" }
    }}
])

Y un formulario de resultado final:

{
    "_id" : null,
    "data" : [
            {
                    "count" : 1,
                    "total" : 350
            },
            {
                    "count" : 2,
                    "total" : 700
            }
    ],
    "totalCost" : 1050
}

Pero, "No hagas eso" . MongoDB tiene un límite de documentos en respuesta de 16 MB, que es una limitación de la especificación BSON. En los resultados pequeños, puede hacer este tipo de ajuste de conveniencia, pero en el esquema más amplio, desea los resultados en la forma anterior y en una consulta separada o en vivo con la iteración de los resultados completos para obtener el total de todos los documentos.

Parece que está utilizando una versión de MongoDB inferior a 2.6, o está copiando la salida de un shell de RoboMongo que no es compatible con las características de la última versión. Desde MongoDB 2.6, aunque los resultados de la agregación pueden ser un "cursor" en lugar de una sola matriz BSON. Por lo tanto, la respuesta general puede ser mucho mayor que 16 MB, pero solo cuando no se está compactando en un solo documento como resultado, como se muestra en el último ejemplo.

Esto sería especialmente cierto en los casos en los que estaba "paginando" los resultados, con líneas de resultados de 100 a 1000, pero solo deseaba que se devolviera un "total" en una respuesta API cuando solo devuelve una "página" de 25 resultados en una vez.

De todos modos, eso debería brindarle una guía razonable sobre cómo obtener el tipo de resultados que espera de su formulario de documento común. Recuerda $unwind para procesar matrices y, en general, $group varias veces para obtener totales en diferentes niveles de agrupación de sus agrupaciones de documentos y colecciones.