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

Grupper og tæl over et start- og slutområde

Algoritmen til dette er grundlæggende at "iterere" værdier mellem intervallet mellem de to værdier. MongoDB har et par måder at håndtere dette på, idet det er det, der altid har været til stede med mapReduce() og med nye funktioner tilgængelige for aggregate() metode.

Jeg vil udvide dit valg for bevidst at vise en overlappende måned, da dine eksempler ikke havde en. Dette vil resultere i, at "HGV"-værdierne vises efter "tre" måneders 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"
}

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

Nøglen til at få dette til at fungere er $range operator som tager værdier for en "start" og og "slut" samt et "interval" for at anvende. Resultatet er en matrix af værdier taget fra "starten" og øget indtil "slutningen" er nået.

Vi bruger dette med startdato og endDate for at generere de mulige datoer mellem disse værdier. Du vil bemærke, at vi er nødt til at lave noget matematik her siden $range tager kun et 32-bit heltal, men vi kan tage millisekunderne væk fra tidsstempelværdierne, så det er okay.

Fordi vi ønsker "måneder", udtrækker de anvendte operationer måneds- og årsværdierne fra det genererede interval. Vi genererer faktisk rækkevidden som "dagene" imellem, da "måneder" er svære at håndtere i matematik. Den efterfølgende $reduce handling tager kun de "adskilte måneder" fra datointervallet.

Resultatet af den første aggregeringspipeline-fase er derfor et nyt felt i dokumentet, som er en "array" af alle de adskilte måneder, der er dækket mellem startDate og endDate . Dette giver en "iterator" for resten af ​​operationen.

Med "iterator" mener jeg, end når vi anvender $unwind vi får en kopi af det originale dokument for hver enkelt måned, der er dækket af intervallet. Dette tillader så følgende to $group trin for først at anvende en gruppering på den fælles nøgle "måned" og "type" for at "totale" tællingerne via $sum , og næste $group gør nøglen til bare "type" og placerer resultaterne i et array via $push .

Dette giver resultatet på ovenstå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
                }
        ]
}

Bemærk, at dækningen af ​​"måneder" kun er til stede, hvor der er faktiske data. Selvom det er muligt at producere nulværdier over et område, kræver det en del skænderier at gøre det og er ikke særlig praktisk. Hvis du vil have nulværdier, er det bedre at tilføje det i efterbehandlingen i klienten, når resultaterne er blevet hentet.

Hvis du virkelig har dit hjerte indstillet på nulværdierne, så bør du separat forespørge efter $min og $max værdier, og send disse ind for at "brute force" pipelinen til at generere kopierne for hver leveret mulig intervalværdi.

Så denne gang laves "rækken" eksternt til alle dokumenter, og du bruger så en $cond sætning i akkumulatoren for at se, om de aktuelle data er inden for det producerede grupperede interval. Da generationen er "ekstern", har vi heller ikke brug for MongoDB 3.4-operatoren for $range , så dette kan også anvendes på tidligere 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" }
    }
  }}
])

Hvilket producerer de konsistente nulfyldninger for alle mulige måneder på alle grupperinger:

{
        "_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 versioner af MongoDB understøtter mapReduce, og det simple tilfælde af "iterator" som nævnt ovenfor håndteres af en for sløjfe i mapperen. Vi kan få output som genereret op til den første $group ovenfra ved blot at gøre:

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 producerer:

{
        "_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å det har ikke den anden gruppering til at sammensætte til arrays, men vi producerede det samme grundlæggende aggregerede output.




  1. Mongodb count vs findone

  2. Er det ok at bruge Mongos objekt-id som dets unikke identifikator? Hvis ja, hvordan kan jeg konvertere den til en streng og slå den op for streng?

  3. Hvordan forespørger jeg refererede objekter i MongoDB?

  4. Sådan klones/kopieres en database i Azure Cosmos DB