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

Como calcular o total em execução usando agregado?


Na verdade, mais adequado para mapReduce do que o framework de agregação, pelo menos na solução inicial do problema. A estrutura de agregação não tem conceito do valor de um documento anterior, ou o valor anterior "agrupado" de um documento, por isso não pode fazer isso.

Por outro lado, mapReduce possui um "escopo global" que pode ser compartilhado entre etapas e documentos à medida que são processados. Isso lhe dará o "total corrente" para o saldo atual no final do dia que você precisa.
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 }
  }
)      

Isso somará por agrupamento de datas e, em seguida, na seção "finalizar", fará uma soma cumulativa de cada dia.
   "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 longo prazo, seria melhor ter uma coleção separada com uma entrada para cada dia e alterar o saldo usando $inc em uma atualização. Faça também um $inc upsert no início de cada dia para criar um novo documento transportando o saldo do dia 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 }
);

Como NÃO fazer isso


Embora seja verdade que desde a escrita original há mais operadores introduzidos no framework de agregação, o que está sendo perguntado aqui ainda não é prático fazer em uma declaração de agregação.

A mesma regra básica se aplica que a estrutura de agregação não pode referenciar um valor de um "documento" anterior, nem pode armazenar uma "variável global". "Hackeando" isso por coerção de todos os resultados em uma 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" } }
])

Essa não é uma solução de alto desempenho ou "segura" considerando que conjuntos de resultados maiores executam a probabilidade muito real de violar o limite de 16 MB de BSON. Como uma "regra de ouro" , qualquer coisa que proponha colocar TODO o conteúdo dentro do array de um único documento:
{ "$group": {
  "_id": null,
  "docs": { "$push": "$$ROOT" }
}}

então isso é uma falha básica e, portanto, não é uma solução .

Conclusão


As maneiras muito mais conclusivas de lidar com isso normalmente seriam o pós-processamento no cursor de execução dos 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 });
})

Então, em geral, é sempre melhor:

  • Use a iteração do cursor e uma variável de rastreamento para totais. O mapReduce sample é um exemplo inventado do processo simplificado acima.

  • Use totais pré-agregados. Possivelmente em conjunto com a iteração do cursor, dependendo do seu processo de pré-agregação, seja apenas intervalo total ou um total de execução "transportado".

A estrutura de agregação deve realmente ser usada para "agregar" e nada mais. Forçar coerções nos dados por meio de processos, como manipular em uma matriz apenas para processar como você deseja, não é sábio nem seguro e, o mais importante, o código de manipulação do cliente é muito mais limpo e eficiente.

Deixe os bancos de dados fazerem as coisas em que são bons, pois suas "manipulações" são muito melhor tratadas no código.