sql >> Databasteknik >  >> NoSQL >> MongoDB

Gruppera och räkna över ett start- och slutintervall

Algoritmen för detta är att i princip "iterera" värden mellan intervallet för de två värdena. MongoDB har ett par sätt att hantera detta, eftersom det är det som alltid har funnits med mapReduce() och med nya funktioner tillgängliga för aggregate() metod.

Jag kommer att utöka ditt urval för att medvetet visa en överlappande månad eftersom dina exempel inte hade någon. Detta kommer att resultera i att "HGV"-värdena visas efter "tre" månaders produktion.

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

Aggregerat – Kräver 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" }
    }
  }}
])

Nyckeln till att få detta att fungera är $range operator som tar värden för en "start" och och "slut" samt ett "intervall" att tillämpa. Resultatet är en matris med värden tagna från "start" och inkrementerade tills "slut" nås.

Vi använder detta med startDate och endDate för att generera möjliga datum mellan dessa värden. Du kommer att notera att vi måste göra lite matematik här eftersom $range tar bara ett 32-bitars heltal, men vi kan ta millisekunderna bort från tidsstämpelvärdena så det är okej.

Eftersom vi vill ha "månader" extraherar de tillämpade operationerna månads- och årvärdena från det genererade intervallet. Vi genererar faktiskt intervallet som "dagarna" däremellan eftersom "månader" är svåra att hantera i matematik. Den efterföljande $reduce operationen tar bara de "särskilda månaderna" från datumintervallet.

Resultatet av det första aggregeringspipelinesteget är därför ett nytt fält i dokumentet som är en "array" av alla distinkta månader som täcks mellan startDate och endDate . Detta ger en "iterator" för resten av operationen.

Med "iterator" menar jag än när vi tillämpar $unwind vi får en kopia av originaldokumentet för varje enskild månad som omfattas av intervallet. Detta tillåter sedan följande två $group steg för att först tillämpa en gruppering på den gemensamma nyckeln "månad" och "typ" för att "summera" räkningarna via $sum , och nästa $group gör nyckeln till bara "typ" och placerar resultaten i en array via $push .

Detta ger resultatet på ovanstående data:

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

Observera att täckningen av "månader" endast finns där det finns faktiska data. Även om det är möjligt att producera nollvärden över ett intervall, kräver det en hel del gräl för att göra det och är inte särskilt praktiskt. Om du vill ha nollvärden är det bättre att lägga till det i efterbearbetningen i klienten när resultaten har hämtats.

Om du verkligen har ditt hjärta inställt på nollvärdena bör du separat fråga efter $min och $max värden, och skicka in dessa för att "brute force" pipelinen generera kopiorna för varje tillhandahållet möjligt intervallvärde.

Så den här gången görs "intervallet" externt till alla dokument, och du använder sedan en $cond uttalande i ackumulatorn för att se om aktuell data ligger inom det grupperade intervallet som produceras. Eftersom generationen är "extern" behöver vi verkligen inte MongoDB 3.4-operatören för $range , så detta kan även tillämpas på tidigare versioner:

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

Vilket ger de konsekventa nollfyllningarna för alla möjliga månader på alla grupperingar:

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

Alla versioner av MongoDB stöder mapReduce, och det enkla fallet med "iterator" som nämnts ovan hanteras av en för slinga i mapparen. Vi kan få utdata som genereras upp till den första $gruppen från ovan genom att helt enkelt göra:

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

Som producerar:

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

Så den har inte den andra gruppen att sammansätta till arrayer, men vi producerade samma grundläggande aggregerade utdata.




  1. Hur väntar man på att posten på mongoose list ska skjutas?

  2. Memcache v/s redis för att upprätthålla ihållande sessioner?

  3. Om mongodb aggregate inte använder index för $lookup, varför ökar min prestanda när jag använder index?

  4. kan jag skicka mongodb-frågan som en sträng i php