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

Raggruppa e conta su un intervallo iniziale e finale

L'algoritmo per questo consiste sostanzialmente nell'"iterare" i valori tra l'intervallo dei due valori. MongoDB ha un paio di modi per affrontare questo problema, essendo ciò che è sempre stato presente con mapReduce() e con le nuove funzionalità disponibili per aggregate() metodo.

Espanderò la tua selezione per mostrare deliberatamente un mese sovrapposto poiché i tuoi esempi non ne avevano uno. Ciò comporterà la visualizzazione dei valori "HGV" in "tre" mesi di produzione.

{
        "_id" : 1,
        "startDate" : ISODate("2017-01-01T00:00:00Z"),
        "endDate" : ISODate("2017-02-25T00:00:00Z"),
        "type" : "CAR"
}
{
        "_id" : 2,
        "startDate" : ISODate("2017-02-17T00:00:00Z"),
        "endDate" : ISODate("2017-03-22T00:00:00Z"),
        "type" : "HGV"
}
{
        "_id" : 3,
        "startDate" : ISODate("2017-02-17T00:00:00Z"),
        "endDate" : ISODate("2017-04-22T00:00:00Z"),
        "type" : "HGV"
}

Aggregato - Richiede MongoDB 3.4

db.cars.aggregate([
  { "$addFields": {
    "range": {
      "$reduce": {
        "input": { "$map": {
          "input": { "$range": [ 
            { "$trunc": { 
              "$divide": [ 
                { "$subtract": [ "$startDate", new Date(0) ] },
                1000
              ]
            }},
            { "$trunc": {
              "$divide": [
                { "$subtract": [ "$endDate", new Date(0) ] },
                1000
              ]
            }},
            60 * 60 * 24
          ]},
          "as": "el",
          "in": {
            "$let": {
              "vars": {
                "date": {
                  "$add": [ 
                    { "$multiply": [ "$$el", 1000 ] },
                    new Date(0)
                  ]
                },
                "month": {
                }
              },
              "in": {
                "$add": [
                  { "$multiply": [ { "$year": "$$date" }, 100 ] },
                  { "$month": "$$date" }
                ]
              }
            }
          }
        }},
        "initialValue": [],
        "in": {
          "$cond": {
            "if": { "$in": [ "$$this", "$$value" ] },
            "then": "$$value",
            "else": { "$concatArrays": [ "$$value", ["$$this"] ] }
          }
        }
      }
    }
  }},
  { "$unwind": "$range" },
  { "$group": {
    "_id": {
      "type": "$type",
      "month": "$range"
    },
    "count": { "$sum": 1 }
  }},
  { "$sort": { "_id": 1 } },
  { "$group": {
    "_id": "$_id.type",
    "monthCounts": { 
      "$push": { "month": "$_id.month", "count": "$count" }
    }
  }}
])

La chiave per farlo funzionare è il $range operatore che accetta i valori di "inizio" e "fine" nonché un "intervallo" da applicare. Il risultato è un array di valori presi dall'"inizio" e incrementati fino al raggiungimento della "fine".

Lo usiamo con startDate e endDate per generare le possibili date tra quei valori. Noterai che dobbiamo fare un po' di matematica qui poiché $range richiede solo un numero intero a 32 bit, ma possiamo togliere i millisecondi dai valori del timestamp, quindi va bene.

Poiché vogliamo "mesi", le operazioni applicate estraggono i valori del mese e dell'anno dall'intervallo generato. In realtà generiamo l'intervallo poiché i "giorni" intermedi poiché i "mesi" sono difficili da gestire in matematica. Il successivo $reduce l'operazione richiede solo i "mesi distinti" dall'intervallo di date.

Il risultato quindi della prima fase della pipeline di aggregazione è un nuovo campo nel documento che è un "array" di tutti i mesi distinti coperti tra startDate e endDate . Questo fornisce un "iteratore" per il resto dell'operazione.

Per "iteratore" intendo di quando applichiamo $unwind otteniamo una copia del documento originale per ogni mese distinto coperto dall'intervallo. Ciò consente quindi i seguenti due $group fasi per applicare prima un raggruppamento alla chiave comune di "mese" e "tipo" per "totale" i conteggi tramite $sum e poi $group rende la chiave solo il "tipo" e inserisce i risultati in un array tramite $push .

Questo dà il risultato sui dati di cui sopra:

{
        "_id" : "HGV",
        "monthCounts" : [
                {
                        "month" : 201702,
                        "count" : 2
                },
                {
                        "month" : 201703,
                        "count" : 2
                },
                {
                        "month" : 201704,
                        "count" : 1
                }
        ]
}
{
        "_id" : "CAR",
        "monthCounts" : [
                {
                        "month" : 201701,
                        "count" : 1
                },
                {
                        "month" : 201702,
                        "count" : 1
                }
        ]
}

Si noti che la copertura dei "mesi" è presente solo dove sono presenti dati effettivi. Sebbene sia possibile produrre valori zero su un intervallo, richiede un bel po' di discussioni per farlo e non è molto pratico. Se vuoi valori zero, allora è meglio aggiungerlo in post-elaborazione nel client una volta che i risultati sono stati recuperati.

Se hai davvero il cuore puntato sui valori zero, dovresti eseguire una query separatamente per $min e $max valori e passarli alla "forza bruta" della pipeline per generare le copie per ogni possibile valore di intervallo fornito.

Quindi questa volta l'"intervallo" viene creato esternamente a tutti i documenti, quindi si utilizza un $cond istruzione nell'accumulatore per vedere se i dati correnti rientrano nell'intervallo raggruppato prodotto. Inoltre, poiché la generazione è "esterna", non abbiamo davvero bisogno dell'operatore MongoDB 3.4 di $range , quindi questo può essere applicato anche alle versioni precedenti:

// Get min and max separately 
var ranges = db.cars.aggregate(
 { "$group": {
   "_id": null,
   "startRange": { "$min": "$startDate" },
   "endRange": { "$max": "$endDate" }
 }}
).toArray()[0]

// Make the range array externally from all possible values
var range = [];
for ( var d = new Date(ranges.startRange.valueOf()); d <= ranges.endRange; d.setUTCMonth(d.getUTCMonth()+1)) {
  var v = ( d.getUTCFullYear() * 100 ) + d.getUTCMonth()+1;
  range.push(v);
}

// Run conditional aggregation
db.cars.aggregate([
  { "$addFields": { "range": range } },
  { "$unwind": "$range" },
  { "$group": {
    "_id": {
      "type": "$type",
      "month": "$range"
    },
    "count": { 
      "$sum": {
        "$cond": {
          "if": {
            "$and": [
              { "$gte": [
                "$range",
                { "$add": [
                  { "$multiply": [ { "$year": "$startDate" }, 100 ] },
                  { "$month": "$startDate" }
                ]}
              ]},
              { "$lte": [
                "$range",
                { "$add": [
                  { "$multiply": [ { "$year": "$endDate" }, 100 ] },
                  { "$month": "$endDate" }
                ]}
              ]}
            ]
          },
          "then": 1,
          "else": 0
        }
      }
    }
  }},
  { "$sort": { "_id": 1 } },
  { "$group": {
    "_id": "$_id.type",
    "monthCounts": { 
      "$push": { "month": "$_id.month", "count": "$count" }
    }
  }}
])

Che produce riempimenti zero coerenti per tutti i mesi possibili su tutti i raggruppamenti:

{
        "_id" : "HGV",
        "monthCounts" : [
                {
                        "month" : 201701,
                        "count" : 0
                },
                {
                        "month" : 201702,
                        "count" : 2
                },
                {
                        "month" : 201703,
                        "count" : 2
                },
                {
                        "month" : 201704,
                        "count" : 1
                }
        ]
}
{
        "_id" : "CAR",
        "monthCounts" : [
                {
                        "month" : 201701,
                        "count" : 1
                },
                {
                        "month" : 201702,
                        "count" : 1
                },
                {
                        "month" : 201703,
                        "count" : 0
                },
                {
                        "month" : 201704,
                        "count" : 0
                }
        ]
}

Riduci mappa

Tutte le versioni di MongoDB supportano mapReduce e il semplice caso dell'"iteratore" come menzionato sopra è gestito da un for loop nel mappatore. Possiamo ottenere l'output generato fino al primo $group dall'alto semplicemente facendo:

db.cars.mapReduce(
  function () {
    for ( var d = this.startDate; d <= this.endDate;
      d.setUTCMonth(d.getUTCMonth()+1) )
    { 
      var m = new Date(0);
      m.setUTCFullYear(d.getUTCFullYear());
      m.setUTCMonth(d.getUTCMonth());
      emit({ id: this.type, date: m},1);
    }
  },
  function(key,values) {
    return Array.sum(values);
  },
  { "out": { "inline": 1 } }
)

Che produce:

{
        "_id" : {
                "id" : "CAR",
                "date" : ISODate("2017-01-01T00:00:00Z")
        },
        "value" : 1
},
{
        "_id" : {
                "id" : "CAR",
                "date" : ISODate("2017-02-01T00:00:00Z")
        },
        "value" : 1
},
{
        "_id" : {
                "id" : "HGV",
                "date" : ISODate("2017-02-01T00:00:00Z")
        },
        "value" : 2
},
{
        "_id" : {
                "id" : "HGV",
                "date" : ISODate("2017-03-01T00:00:00Z")
        },
        "value" : 2
},
{
        "_id" : {
                "id" : "HGV",
                "date" : ISODate("2017-04-01T00:00:00Z")
        },
        "value" : 1
}

Quindi non ha il secondo raggruppamento da comporre in array, ma abbiamo prodotto lo stesso output aggregato di base.