MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Regrouper et compter sur une plage de début et de fin

L'algorithme pour cela consiste essentiellement à "itérer" les valeurs entre l'intervalle des deux valeurs. MongoDB a plusieurs façons de gérer cela, étant ce qui a toujours été présent avec mapReduce() et avec de nouvelles fonctionnalités disponibles pour aggregate() méthode.

Je vais développer votre sélection pour montrer délibérément un mois qui se chevauche puisque vos exemples n'en avaient pas. Cela se traduira par les valeurs "HGV" apparaissant dans "trois" mois de sortie.

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

Agrégat - Nécessite 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 clé pour que cela fonctionne est la $range opérateur qui prend des valeurs pour un "début" et une "fin" ainsi qu'un "intervalle" à appliquer. Le résultat est un tableau de valeurs prises à partir du "début" et incrémentées jusqu'à ce que la "fin" soit atteinte.

Nous l'utilisons avec startDate et endDate pour générer les dates possibles entre ces valeurs. Vous remarquerez que nous devons faire quelques calculs ici puisque la $range ne prend qu'un entier 32 bits, mais nous pouvons retirer les millisecondes des valeurs d'horodatage, donc ça va.

Parce que nous voulons des "mois", les opérations appliquées extraient les valeurs de mois et d'année de la plage générée. Nous générons en fait la plage car les "jours" entre les deux car les "mois" sont difficiles à gérer en mathématiques. Le suivant $reduce l'opération ne prend que les "mois distincts" de la plage de dates.

Le résultat de la première étape du pipeline d'agrégation est donc un nouveau champ dans le document qui est un "tableau" de tous les mois distincts couverts entre startDate et endDate . Cela donne un "itérateur" pour le reste de l'opération.

Par "itérateur", je veux dire que lorsque nous appliquons $unwind nous obtenons une copie du document original pour chaque mois distinct couvert dans l'intervalle. Cela permet alors les deux $group étapes pour d'abord appliquer un regroupement sur la clé commune de "mois" et "type" afin de "totaliser" les décomptes via $sum , et ensuite $group rend la clé juste le "type" et place les résultats dans un tableau via $push .

Cela donne le résultat sur les données ci-dessus :

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

Notez que la couverture des "mois" n'est présente que lorsqu'il existe des données réelles. Bien qu'il soit possible de produire des valeurs nulles sur une plage, cela nécessite un peu de préparation et n'est pas très pratique. Si vous voulez des valeurs nulles, il est préférable de l'ajouter en post-traitement dans le client une fois les résultats récupérés.

Si vous avez vraiment à cœur les valeurs zéro, vous devez interroger séparément $min et $max valeurs, et transmettez-les pour "forcer brutalement" le pipeline en générant les copies pour chaque valeur de plage possible fournie.

Donc cette fois la "plage" est faite en externe à tous les documents, et vous utilisez alors un $cond instruction dans l'accumulateur pour voir si les données actuelles sont dans la plage groupée produite. De plus, puisque la génération est "externe", nous n'avons vraiment pas besoin de l'opérateur MongoDB 3.4 de $range , cela peut donc s'appliquer également aux versions antérieures :

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

Ce qui produit des remplissages à zéro cohérents pour tous les mois possibles sur tous les regroupements :

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

Toutes les versions de MongoDB prennent en charge mapReduce, et le cas simple de "l'itérateur" comme mentionné ci-dessus est géré par un for boucle dans le mappeur. Nous pouvons obtenir la sortie telle que générée jusqu'au premier $group d'en haut en faisant simplement :

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

Qui produit :

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

Il n'a donc pas le deuxième groupement pour composer des tableaux, mais nous avons produit la même sortie agrégée de base.