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

¿Cómo calcular el total acumulado usando agregado?

En realidad, es más adecuado para mapReduce que el marco de agregación, al menos en la resolución inicial del problema. El marco de agregación no tiene concepto del valor de un documento anterior, o el valor "agrupado" anterior de un documento, por eso no puede hacer esto.

Por otro lado, mapReduce tiene un "alcance global" que se puede compartir entre etapas y documentos a medida que se procesan. Esto le dará el "total acumulado" del saldo actual al final del día que necesita.

db.collection.mapReduce(
  function () {
    var date = new Date(this.dateEntry.valueOf() -
      ( this.dateEntry.valueOf() % ( 1000 * 60 * 60 * 24 ) )
    );

    emit( date, this.amount );
  },
  function(key,values) {
      return Array.sum( values );
  },
  { 
      "scope": { "total": 0 },
      "finalize": function(key,value) {
          total += value;
          return total;
      },
      "out": { "inline": 1 }
  }
)      

Eso sumará por agrupación de fechas y luego en la sección "finalizar" hace una suma acumulativa de cada día.

   "results" : [
            {
                    "_id" : ISODate("2015-01-06T00:00:00Z"),
                    "value" : 50
            },
            {
                    "_id" : ISODate("2015-01-07T00:00:00Z"),
                    "value" : 150
            },
            {
                    "_id" : ISODate("2015-01-09T00:00:00Z"),
                    "value" : 179
            }
    ],

A más largo plazo, sería mejor tener una colección separada con una entrada para cada día y modificar el saldo usando $inc en una actualización. Solo haz también un $inc upsert al comienzo de cada día para crear un nuevo documento arrastrando el saldo del día anterior:

// increase balance
db.daily(
    { "dateEntry": currentDate },
    { "$inc": { "balance": amount } },
    { "upsert": true }
);

// decrease balance
db.daily(
    { "dateEntry": currentDate },
    { "$inc": { "balance": -amount } },
    { "upsert": true }
);

// Each day
var lastDay = db.daily.findOne({ "dateEntry": lastDate });
db.daily(
    { "dateEntry": currentDate },
    { "$inc": { "balance": lastDay.balance } },
    { "upsert": true }
);

Cómo NO hacer esto

Si bien es cierto que desde la escritura original se han introducido más operadores en el marco de agregación, lo que se pregunta aquí todavía no es práctico hacer en una declaración de agregación.

Se aplica la misma regla básica de que el marco de agregación no puede hacer referencia a un valor de un "documento" anterior, ni puede almacenar una "variable global". "Hackear" esto por coerción de todos los resultados en una matriz:

db.collection.aggregate([
  { "$group": {
    "_id": { 
      "y": { "$year": "$dateEntry" }, 
      "m": { "$month": "$dateEntry" }, 
      "d": { "$dayOfMonth": "$dateEntry" } 
    }, 
    "amount": { "$sum": "$amount" }
  }},
  { "$sort": { "_id": 1 } },
  { "$group": {
    "_id": null,
    "docs": { "$push": "$$ROOT" }
  }},
  { "$addFields": {
    "docs": {
      "$map": {
        "input": { "$range": [ 0, { "$size": "$docs" } ] },
        "in": {
          "$mergeObjects": [
            { "$arrayElemAt": [ "$docs", "$$this" ] },
            { "amount": { 
              "$sum": { 
                "$slice": [ "$docs.amount", 0, { "$add": [ "$$this", 1 ] } ]
              }
            }}
          ]
        }
      }
    }
  }},
  { "$unwind": "$docs" },
  { "$replaceRoot": { "newRoot": "$docs" } }
])

Esa no es una solución eficaz ni "segura" teniendo en cuenta que los conjuntos de resultados más grandes tienen una probabilidad muy real de infringir el límite de BSON de 16 MB. Como "regla de oro" , cualquier cosa que proponga poner TODO el contenido dentro de la matriz de un solo documento:

{ "$group": {
  "_id": null,
  "docs": { "$push": "$$ROOT" }
}}

entonces eso es un defecto básico y por lo tanto no una solución .

Conclusión

Las formas mucho más concluyentes de manejar esto normalmente serían el procesamiento posterior en el cursor de ejecución de los resultados:

var globalAmount = 0;

db.collection.aggregate([
  { $group: {
    "_id": { 
      y: { $year:"$dateEntry"}, 
      m: { $month:"$dateEntry"}, 
      d: { $dayOfMonth:"$dateEntry"} 
    }, 
    amount: { "$sum": "$amount" }
  }},
  { "$sort": { "_id": 1 } }
]).map(doc => {
  globalAmount += doc.amount;
  return Object.assign(doc, { amount: globalAmount });
})

Entonces, en general, siempre es mejor:

  • Utilice la iteración del cursor y una variable de seguimiento para los totales. El mapReduce muestra es un ejemplo artificial del proceso simplificado anterior.

  • Use totales agregados previamente. Posiblemente en conjunto con la iteración del cursor dependiendo de su proceso de agregación previa, ya sea solo un total de intervalo o un total acumulado "transferido".

El marco de agregación realmente debería usarse para "agregar" y nada más. Forzar coerciones en los datos a través de procesos como manipularlos en una matriz solo para procesarlos como usted desea no es inteligente ni seguro y, lo que es más importante, el código de manipulación del cliente es mucho más limpio y eficiente.

Deje que las bases de datos hagan las cosas en las que son buenas, ya que sus "manipulaciones" se manejan mucho mejor en el código.