sql >> Database >  >> NoSQL >> MongoDB

Groepeer en tel over een begin- en eindbereik

Het algoritme hiervoor is om in principe waarden tussen het interval van de twee waarden te "itereren". MongoDB heeft een aantal manieren om hiermee om te gaan, namelijk wat altijd al aanwezig was met mapReduce() en met nieuwe functies die beschikbaar zijn voor de aggregate() methode.

Ik ga uw selectie uitbreiden om met opzet een overlappende maand weer te geven, aangezien uw voorbeelden er geen hadden. Dit zal ertoe leiden dat de "HGV"-waarden verschijnen in "drie" maanden output.

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

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

De sleutel om dit te laten werken is de $range operator die waarden aanneemt voor een "start" en "end" en een "interval" om toe te passen. Het resultaat is een reeks waarden die vanaf het 'begin' worden genomen en worden opgehoogd totdat het 'einde' is bereikt.

We gebruiken dit met startDate en einddatum om de mogelijke datums tussen deze waarden te genereren. Je zult merken dat we hier wat rekenwerk moeten doen omdat de $range neemt alleen een 32-bits geheel getal, maar we kunnen de milliseconden weghalen van de tijdstempelwaarden, dus dat is goed.

Omdat we "maanden" willen, extraheren de toegepaste bewerkingen de maand- en jaarwaarden uit het gegenereerde bereik. We genereren het bereik eigenlijk als de "dagen" ertussen, omdat "maanden" moeilijk zijn om mee om te gaan in wiskunde. De daaropvolgende $reduce operatie duurt alleen de "afzonderlijke maanden" van het datumbereik.

Het resultaat van de eerste aggregatiepijplijnfase is daarom een ​​nieuw veld in het document dat een "array" is van alle verschillende maanden die worden gedekt tussen startDate en einddatum . Dit geeft een "iterator" voor de rest van de bewerking.

Met "iterator" bedoel ik dan wanneer we $unwind we krijgen een kopie van het originele document voor elke afzonderlijke maand die in het interval valt. Hierdoor kunnen de volgende twee $group stappen om eerst een groepering toe te passen op de gemeenschappelijke sleutel van "maand" en "type" om de tellingen te "totalen" via $sum , en vervolgens $group maakt de sleutel gewoon het "type" en plaatst de resultaten in een array via $push .

Dit geeft het resultaat op de bovenstaande gegevens:

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

Merk op dat de dekking van "maanden" alleen aanwezig is als er daadwerkelijke gegevens zijn. Hoewel het mogelijk is om nulwaarden over een bereik te produceren, vereist het nogal wat gekibbel om dit te doen en is niet erg praktisch. Als u nulwaarden wilt, is het beter om dat toe te voegen in de nabewerking in de client zodra de resultaten zijn opgehaald.

Als je je hart echt op de nulwaarden hebt ingesteld, moet je apart zoeken naar $min en $max waarden, en geef deze door aan de "brute force" van de pijplijn om de kopieën te genereren voor elke opgegeven mogelijke bereikwaarde.

Dus deze keer wordt het "bereik" extern gemaakt voor alle documenten, en gebruik je dan een $cond statement in de accumulator om te zien of de huidige gegevens binnen het geproduceerde gegroepeerde bereik vallen. Ook omdat de generatie "extern" is, hebben we de MongoDB 3.4-operator van $range echt niet nodig , dus dit kan ook op eerdere versies worden toegepast:

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

Die de consistente nulvullingen produceert voor alle mogelijke maanden op alle groeperingen:

{
        "_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 versies van MongoDB ondersteunen mapReduce, en het eenvoudige geval van de "iterator" zoals hierboven vermeld wordt afgehandeld door een for lus in de mapper. We kunnen output krijgen zoals gegenereerd tot aan de eerste $group van bovenaf door simpelweg te doen:

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

Wat produceert:

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

Het heeft dus niet de tweede groepering om tot arrays te worden samengesteld, maar we hebben wel dezelfde geaggregeerde basisuitvoer geproduceerd.




  1. MongoDb-queryvoorwaarde bij het vergelijken van 2 velden

  2. Hoe useMongoClient (Mongoose 4.11.0) in te stellen?

  3. Datums opvragen met Mongo Spring-gegevens versus Mongo's console (datums)

  4. JSON lezen, schrijven en opslaan met Node op Heroku