MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Come calcolare il totale parziale utilizzando l'aggregato?

In realtà più adatto a mapReduce rispetto al framework di aggregazione, almeno nella risoluzione iniziale dei problemi. Il framework di aggregazione non ha il concetto del valore di un documento precedente o del precedente valore "raggruppato" di un documento, ecco perché non può farlo.

D'altra parte, mapReduce ha un "ambito globale" che può essere condiviso tra fasi e documenti man mano che vengono elaborati. In questo modo otterrai il "totale corrente" per il saldo corrente alla fine della giornata di cui hai bisogno.

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 }
  }
)      

Verrà sommato per raggruppamento di date e quindi nella sezione "finalizza" verrà creata una somma cumulativa per ogni giorno.

   "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 lungo termine sarebbe meglio avere una raccolta separata con una voce per ogni giorno e modificare il saldo utilizzando $inc in un aggiornamento. Basta fare anche un $inc upsert all'inizio di ogni giornata per creare un nuovo documento che riporti il ​​saldo del giorno precedente:

// 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 }
);

Come NON farlo

Se è vero che dalla scrittura originale ci sono più operatori introdotti nel framework di aggregazione, ciò che viene chiesto qui non è ancora pratico da fare in una dichiarazione di aggregazione.

Si applica la stessa regola di base che il framework di aggregazione non può fa riferimento a un valore da un "documento" precedente, né può memorizzare una "variabile globale". "Hacking" questo per coercizione di tutti i risultati in un array:

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" } }
])

Questa non è né una soluzione performante né "sicura" considerando che set di risultati più grandi corrono la probabilità molto reale di violare il limite BSON di 16 MB. Come "regola d'oro" , tutto ciò che propone di inserire TUTTO il contenuto all'interno dell'array di un singolo documento:

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

allora questo è un difetto di base e quindi non una soluzione .

Conclusione

I modi molto più conclusivi per gestirlo in genere sarebbero la post-elaborazione sul cursore in esecuzione dei risultati:

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 });
})

Quindi in generale è sempre meglio:

  • Usa l'iterazione del cursore e una variabile di tracciamento per i totali. Il mapReduce sample è un esempio artificiale del processo semplificato di cui sopra.

  • Utilizza i totali preaggregati. Forse in combinazione con l'iterazione del cursore a seconda del processo di pre-aggregazione, sia che si tratti solo di un totale di intervallo o di un totale parziale "portato avanti".

Il framework di aggregazione dovrebbe davvero essere utilizzato per "aggregare" e nient'altro. Forzare le coercizioni sui dati tramite processi come la manipolazione in un array solo per elaborare come si desidera non è né saggio né sicuro e, soprattutto, il codice di manipolazione del client è molto più pulito ed efficiente.

Consenti ai database di fare le cose in cui sono bravi, poiché le tue "manipolazioni" sono invece gestite molto meglio nel codice.