web-dev-qa-db-fra.com

Résultat de groupe par intervalle de 15 minutes dans MongoDb

J'ai une collection "status" comme cette structure -

{
    _id: ObjectId("545a0b63b03dbcd1238b4567"),
    status: 1004,
    comment: "Rem dolor ipsam placeat omnis non. Aspernatur nobis qui nisi similique.",
    created_at: ISODate("2014-11-05T11:34:59.804Z")
},
{
    _id: ObjectId("545a0b66b03dbcd1238b4568"),
    status: 1001,
    comment: "Sint et eos vero ipsa voluptatem harum. Hic unde voluptatibus et blanditiis quod modi.",
    created_at: ISODate("2014-11-05T11:35:02.814Z")
}
....
....

J'ai besoin d'obtenir un résultat groupé par intervalle de 15 minutes à partir de cette collection.

40
Hein Zaw Htet

Il y a plusieurs façons de faire ça.

Le premier est avec Opérateurs d'agrégation de dates , qui vous permettent de disséquer les valeurs de "date" dans les documents. Spécifiquement pour le "groupement" comme objectif principal:

db.collection.aggregate([
  { "$group": {
    "_id": {
      "year": { "$year": "$created_at" },
      "dayOfYear": { "$dayOfYear": "$created_at" },
      "hour": { "$hour": "$created_at" },
      "interval": {
        "$subtract": [ 
          { "$minute": "$created_at" },
          { "$mod": [{ "$minute": "$created_at"}, 15] }
        ]
      }
    }},
    "count": { "$sum": 1 }
  }}
])

La deuxième façon consiste à utiliser une petite astuce lorsqu'un objet de date est soustrait (ou toute autre opération mathématique directe) d'un autre objet de date, puis le résultat est une valeur numérique représentant les millisecondes d'horodatage Epoch entre les deux objets. Donc, en utilisant simplement la date Epoch, vous obtenez la représentation Epoch millisecondes. Ensuite, utilisez les mathématiques de date pour l'intervalle:

db.collection.aggregate([
    { "$group": {
        "_id": {
            "$subtract": [
                { "$subtract": [ "$created_at", new Date("1970-01-01") ] },
                { "$mod": [ 
                    { "$subtract": [ "$created_at", new Date("1970-01-01") ] },
                    1000 * 60 * 15
                ]}
            ]
        },
        "count": { "$sum": 1 }
    }}
])

Cela dépend donc du type de format de sortie que vous souhaitez pour l'intervalle de regroupement. Les deux représentent fondamentalement la même chose et ont suffisamment de données pour reconstruire en tant qu'objet "date" dans votre code.

Vous pouvez mettre tout ce que vous voulez dans la partie "opérateur de regroupement" après le regroupement _id. J'utilise simplement l'exemple de base "count" au lieu de toute déclaration réelle de vous-même sur ce que vous voulez vraiment faire.


MongoDB 4.x et versions ultérieures

Il y a eu quelques ajouts aux opérateurs d'agrégation de dates depuis l'écriture originale, mais à partir de MongoDB 4.0, il y aura de véritables "coulées réelles de types" par opposition aux astuces mathématiques de base effectuées ici avec la conversion de date BSON.

Par exemple, nous pouvons utiliser $toLong et $toDate comme nouveaux assistants ici:

db.collection.aggregate([
  { "$group": {
    "_id": {
      "$toDate": {
        "$subtract": [
          { "$toLong": "$created_at" },
          { "$mod": [ { "$toLong": "$created_at" }, 1000 * 60 * 15 ] }
        ]
      }
    },
    "count": { "$sum": 1 }
  }}
])

C'est un peu plus court et ne nécessite pas de définir une date BSON externe pour la valeur "Epoch" comme constante dans la définition du pipeline, donc c'est assez cohérent pour toutes les implémentations de langage.

Ce ne sont que deux des méthodes "d'aide" pour la conversion de type qui sont toutes liées à $convert , qui est une forme "plus longue" de l'implémentation permettant une gestion personnalisée sur null ou une erreur de conversion.

Il est même possible avec une telle conversion d'obtenir les informations Date à partir du ObjectId de la clé primaire, car ce serait une source fiable de date de "création":

db.collection.aggregate([
  { "$group": {
    "_id": {
      "$toDate": {
        "$subtract": [
          { "$toLong": { "$toDate": "$_id" }  },
          { "$mod": [ { "$toLong": { "$toDate": "$_id" } }, 1000 * 60 * 15 ] }
        ]
      }
    },
    "count": { "$sum": 1 }
  }}
])

Les "types de casting" avec ce type de conversion peuvent donc être un outil assez puissant.

Avertissement - ObjectId les valeurs sont limitées à la précision à la seconde uniquement pour la valeur de temps interne qui fait partie de leurs données permettant la $toDate conversion. Le "temps" réellement inséré dépend très probablement du pilote utilisé. Lorsque precision est requis, il est toujours recommandé d'utiliser un champ BSON Date discret au lieu de s'appuyer sur des valeurs ObjectId.

105
Neil Lunn

J'aime l'autre réponse ici, et surtout pour l'utilisation des mathématiques de date au lieu des opérateurs de date d'agrégation qui, bien qu'utiles, peuvent également être un peu obscurs.

La seule chose que je veux ajouter ici est que vous pouvez également renvoyer un objet Date à partir du cadre d'agrégation par cette approche, par opposition à l'horodatage "numérique". C'est juste un peu de calcul supplémentaire sur les mêmes principes, en utilisant $add :

db.collection.aggregate([
    { "$group": {
        "_id": {
            "$add": [
                { "$subtract": [
                    { "$subtract": [ "$current_date", new Date(0) ] },
                    { "$mod": [ 
                        { "$subtract": [ "$current_date", new Date(0) ] },
                        1000 * 60 * 15
                    ]}
                ] },
                new Date(0)
            ]
        },
        "count": { "$sum": 1 }
    }}
])

Les structures Date(0) en JavaScript représentent ici la même date "Epoch" sous une forme plus courte, car 0 milliseconde d'Epoch est Epoch. Mais le point principal est que lorsque "l'ajout" à un autre objet de date BSON est effectué avec un identificateur numérique, alors l'inverse de la condition décrite est vrai et le résultat final est en fait maintenant un Date.

Tous les pilotes retourneront le type natif Date dans leur langue par cette approche.

15
Blakes Seven

Un peu plus beau pour mongodb.version () <3.0

db.collection.aggregate([
    {$match: {created_at:{$exists:1}}},
    {$group: {
        _id: {$add:[
            {$dayOfYear: "$created_at" },
            {$multiply: [{$year: "$created_at"}, 1000]}
        ]},
        count: {$sum: 1 }
    }},
    {$sort:{_id:-1}}
])
7
Stierlitz

Un autre moyen utile:

db.collection.aggregate([
  {$group: {
    _id: { 
      overallTime: { 
        $dateToString: { format: "%Y-%m-%dT%H", date: "$created_at" } 
      },
      interval: { $trunc: { $divide: [{ $minute: "$created_at" }, 15 ]}}
    },
  }},
])

Et plus facile pour les intervalles min, heure, jour:

var format = "%Y-%m-%dT%H:%M"; // 1 min
var format = "%Y-%m-%dT%H"; // 1 hour
var format = "%Y-%m-%d"; // 1 day

db.collection.aggregate([
  {$group: {
    _id: { $dateToString: { format: format, date: "$created_at" } },
  }},
])
4
Sergey Reutskiy

La réponse de @Neil Lunn à https://stackoverflow.com/a/26814496/8474325 pour MongoDb 4.x vers le haut est fantastique. Mais il y a une petite erreur dans le code où il utilise ObjectId pour l'agrégation. La ligne { "$toDate": "_id" } doit être remplacé par { "$toDate": "$_id" } pour que le code fonctionne.

Voici le code corrigé.

db.collection.aggregate([
    { "$group": {
      "_id": {
          "$toDate": {
              "$subtract": [
                  { "$toLong": { "$toDate": "$_id" }  },
                  { "$mod": [ { "$toLong": { "$toDate": "$_id" } }, 1000 * 60 * 15 ] }
              ]
          }
      },
      "count": { "$sum": 1 }
   }}
])
1
sanair96