MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Gruppieren und zählen Sie über einen Start- und Endbereich

Der Algorithmus dafür besteht im Grunde darin, Werte zwischen dem Intervall der beiden Werte zu "iterieren". MongoDB hat mehrere Möglichkeiten, damit umzugehen, was mit mapReduce() und mit neuen Funktionen, die für aggregate() Methode.

Ich werde Ihre Auswahl erweitern, um absichtlich einen überlappenden Monat anzuzeigen, da Ihre Beispiele keinen hatten. Dies führt dazu, dass die "HGV"-Werte in "drei" Monaten der Ausgabe erscheinen.

{
        "_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"
}

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

Der Schlüssel, damit dies funktioniert, ist der $range Operator, der Werte für einen "Start" und ein "Ende" sowie ein "Intervall" zur Anwendung nimmt. Das Ergebnis ist ein Array von Werten, die vom "Start" genommen und inkrementiert werden, bis das "Ende" erreicht ist.

Wir verwenden dies mit startDate und endDate um die möglichen Daten zwischen diesen Werten zu generieren. Sie werden feststellen, dass wir hier etwas rechnen müssen, seit $range nimmt nur eine 32-Bit-Ganzzahl, aber wir können die Millisekunden von den Zeitstempelwerten wegnehmen, also ist das in Ordnung.

Da wir "Monate" wollen, extrahieren die angewendeten Operationen die Monats- und Jahreswerte aus dem generierten Bereich. Wir generieren den Bereich eigentlich als die „Tage“ dazwischen, da „Monate“ in der Mathematik schwer zu handhaben sind. Der nachfolgende $reduce Der Vorgang dauert nur die "eindeutigen Monate" aus dem Datumsbereich.

Das Ergebnis der ersten Phase der Aggregationspipeline ist daher ein neues Feld im Dokument, das ein "Array" aller unterschiedlichen Monate ist, die zwischen startDate liegen und endDate . Dies ergibt einen "Iterator" für den Rest der Operation.

Mit „Iterator“ meine ich, wenn wir $unwind Wir erhalten eine Kopie des Originaldokuments für jeden einzelnen Monat, der im Intervall abgedeckt wird. Dies erlaubt dann die folgenden zwei $group Schritte, um zunächst eine Gruppierung auf den gemeinsamen Schlüssel von "Monat" und "Typ" anzuwenden, um die Zählungen über $sum , und als nächstes $group macht den Schlüssel nur zum "Typ" und legt die Ergebnisse in einem Array über ab $push .

Dies ergibt das Ergebnis für die obigen Daten:

{
        "_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
                }
        ]
}

Beachten Sie, dass die Abdeckung von "Monaten" nur vorhanden ist, wenn tatsächliche Daten vorhanden sind. Obwohl es möglich ist, Nullwerte über einen Bereich zu erzeugen, erfordert dies ziemlich viel Hin und Her und ist nicht sehr praktisch. Wenn Sie Nullwerte wünschen, ist es besser, diese in der Nachbearbeitung im Client hinzuzufügen, sobald die Ergebnisse abgerufen wurden.

Wenn Sie wirklich auf Nullwerte setzen, sollten Sie separat nach $min und $max Werte, und übergeben Sie diese per "Brute Force" an die Pipeline, um die Kopien für jeden bereitgestellten möglichen Bereichswert zu generieren.

Diesmal wird also die "Range" extern zu allen Dokumenten gemacht, und Sie verwenden dann einen $cond -Anweisung in den Akkumulator, um zu sehen, ob die aktuellen Daten innerhalb des erzeugten gruppierten Bereichs liegen. Da die Generierung "extern" ist, brauchen wir den MongoDB 3.4-Operator von $range wirklich nicht , sodass dies auch auf frühere Versionen angewendet werden kann:

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

Was die konsistenten Nullfüllungen für alle möglichen Monate in allen Gruppierungen erzeugt:

{
        "_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
                }
        ]
}

MapReduce

Alle Versionen von MongoDB unterstützen mapReduce, und der einfache Fall des oben erwähnten „Iterators“ wird von einem for behandelt Schleife im Mapper. Wir können die Ausgabe so erhalten, wie sie bis zur ersten $group generiert wurde von oben, indem Sie einfach Folgendes tun:

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

Was produziert:

{
        "_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
}

Es hat also nicht die zweite Gruppierung, um Arrays zu verbinden, aber wir haben die gleiche grundlegende aggregierte Ausgabe erzeugt.