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

Requête agrégée Mongodb, ou trop complexe ?

Bien que cela aurait dû être plus clair dans votre question, votre échantillon de sortie de la source suggère que vous recherchez :

  • Nombre total de messages par "uid"
  • Nombre distinct de valeurs dans "to"
  • Nombre distinct de valeurs dans "de"
  • Résumé des décomptes par "heure" pour chaque "uid"

Tout cela est possible dans une seule déclaration d'agrégation, et il suffit d'une gestion minutieuse des listes distinctes, puis de quelques manipulations pour mapper les résultats pour chaque heure sur une période de 24 heures.

La meilleure approche ici est aidée par les opérateurs introduits dans MongoDB 3.2 :

db.collection.aggregate([
    // First group by hour within "uid" and keep distinct "to" and "from"
    { "$group": {
        "_id": {
            "uid": "$uid",
            "time": { "$hour": "$timestamp" }
        },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "count": { "$sum": 1 }
    }},

    // Roll-up to "uid" and keep each hour in an array
    { "$group": {
        "_id": "$_id.uid",
        "total": { "$sum": "$count" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { 
            "$push": {
                "index": "$_id.time",
                "count": "$count"
            }
        }
     }},

     // Getting distinct "to" and "from" requires a double unwind of arrays
     { "$unwind": "$to" },
     { "$unwind": "$to" },
     { "$unwind": "$from" },
     { "$unwind": "$from" },

     // And then adding back to sets for distinct
     { "$group": {
        "_id": "$_id",
        "total": { "$first": "$total" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { "$first": "$temp_hours" }
     }},

     // Map out for each hour and count size of distinct lists
     { "$project": {
        "count": "$total",
        "from_count": { "$size": "$from" },
        "to_count": { "$size": "$to" },
        "hours": {
            "$map": {
                "input": [
                     00,01,02,03,04,05,06,07,08,09,10,11,
                     12,13,14,15,16,17,18,19,20,21,22,23
                 ],
                 "as": "el",
                 "in": {
                      "$ifNull": [
                          { "$arrayElemAt": [
                              { "$map": {
                                  "input": { "$filter": {
                                     "input": "$temp_hours",
                                     "as": "tmp",
                                     "cond": {
                                         "$eq": [ "$$el", "$$tmp.index" ]
                                     }
                                  }},
                                 "as": "out",
                                 "in": "$$out.count"
                              }},
                              0
                          ]},
                          0
                      ]
                 }
            }
        }
     }},

     // Optionally sort in "uid" order
     { "$sort": { "_id": 1 } }
 ])

Avant MongoDB 3.2, vous devez vous impliquer un peu plus pour mapper le contenu du tableau à toutes les heures de la journée :

db.collection.aggregate([

    // First group by hour within "uid" and keep distinct "to" and "from"
    { "$group": {
        "_id": {
            "uid": "$uid",
            "time": { "$hour": "$timestamp" }
        },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "count": { "$sum": 1 }
    }},

    // Roll-up to "uid" and keep each hour in an array
    { "$group": {
        "_id": "$_id.uid",
        "total": { "$sum": "$count" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { 
            "$push": {
                "index": "$_id.time",
                "count": "$count"
            }
        }
     }},

     // Getting distinct "to" and "from" requires a double unwind of arrays
     { "$unwind": "$to" },
     { "$unwind": "$to" },
     { "$unwind": "$from" },
     { "$unwind": "$from" },

     // And then adding back to sets for distinct, also adding the indexes array
     { "$group": {
        "_id": "$_id",
        "total": { "$first": "$total" },
        "from": { "$addToSet": "$from" },
        "to": { "$addToSet": "$to" },
        "temp_hours": { "$first": "$temp_hours" },
        "indexes": { "$first": { "$literal": [
                     00,01,02,03,04,05,06,07,08,09,10,11,
                     12,13,14,15,16,17,18,19,20,21,22,23
        ] } }
     }},

     // Denormalize both arrays
     { "$unwind": "$temp_hours" },
     { "$unwind": "$indexes" },

     // Marry up the index entries and keep either the value or 0
     // Note you are normalizing the double unwind to distinct index
     { "$group": {
         "_id": {
             "_id": "$_id",
             "index": "$indexes"
         },
         "total": { "$first": "$total" }, 
         "from": { "$first": "$from" },
         "to": { "$first": "$to" },
         "count": {
             "$max": {
                 "$cond": [
                     { "$eq": [ "$indexes", "$temp_hours.index" ] },
                     "$temp_hours.count",
                     0
                 ]
             }
         }
     }},

     // Sort to keep index order - !!Important!!         
     { "$sort": { "_id": 1 } },

     // Put the hours into the array and get sizes for other results
     { "$group": {
         "_id": "$_id._id",
         "count": { "$first": "$total" },
         "from_count": { "$first": { "$size": "$from" } },
         "to_count": { "$first": { "$size": "$to" } },
         "hours": { "$push": "$count" }
     }},

     // Optionally sort in "uid" order
     { "$sort": { "_id": 1 } }
])

Pour décomposer cela, les deux approches suivent ici les mêmes étapes de base, la seule vraie différence se produisant sur la cartographie des "heures" pour la période de 24 heures.

Dans la première agrégation $group étape, l'objectif est d'obtenir des résultats par heure présente dans les données et pour chaque valeur "uid". L'opérateur d'agrégation de date simple de $hour permet d'obtenir cette valeur dans le cadre de la clé de regroupement.

Le $addToSet les opérations sont une sorte de "mini-groupe" en elles-mêmes, ce qui permet de conserver les "ensembles distincts" pour chacune des valeurs "vers" et "depuis" tout en continuant à regrouper par heure.

Le prochain $group est plus "organisationnel", car les "comptes" enregistrés pour chaque heure sont conservés dans un tableau tout en regroupant toutes les données pour être simplement regroupées par "uid". Cela vous donne essentiellement toutes les "données" dont vous avez vraiment besoin pour le résultat, mais bien sûr le $addToSet les opérations ici ne font qu'ajouter des "tableaux dans des tableaux" des ensembles distincts déterminés par heure.

Afin d'obtenir ces valeurs sous forme de listes vraiment distinctes pour chaque "uid" et uniquement, il est nécessaire de déconstruire chaque tableau en utilisant $unwind puis enfin regrouper en tant que "ensembles" distincts. Le même $addToSet compacte ceci, et le $first les opérations prennent simplement les "premières" valeurs des autres champs, qui sont déjà toutes identiques pour les données cibles "par uid". Nous en sommes satisfaits, alors gardez-les tels quels.

La ou les étapes finales ici sont essentiellement de nature "cosmétique" et peuvent également être réalisées dans le code côté client. Puisqu'il n'y a pas de données présentes pour chaque intervalle d'une heure, elles doivent être mappées dans un tableau de valeurs représentant chaque heure. Les deux approches ici varient selon les capacités des opérateurs disponibles entre les versions.

Dans la version MongoDB 3.2, il y a $filter et $arrayElemAt opérateurs qui vous permettent effectivement de créer la logique pour "transposer" une source d'entrée de toutes les positions d'index possibles (24 heures) dans les valeurs qui sont déjà déterminées pour les décomptes de ces heures dans les données disponibles. Il s'agit essentiellement d'une "recherche directe" des valeurs déjà enregistrées pour chaque heure disponible pour voir si elle existe, où elle le compte est transposée dans le tableau complet. Là où il n'est pas présent, une valeur par défaut de 0 est utilisé à la place.

Sans ces opérateurs, faire cette "mise en correspondance" signifie essentiellement dénormaliser les deux tableaux (les données enregistrées et les 24 positions complètes) afin de comparer et de transposer. C'est ce qui se passe dans la deuxième approche avec une simple comparaison des valeurs "d'index" pour voir s'il y avait un résultat pour cette heure. Le $max L'opérateur ici est principalement utilisé à cause des deux $unwind instructions, où chaque valeur enregistrée à partir des données source va être reproduite pour chaque position d'index possible. Cela "compacte" jusqu'aux seules valeurs souhaitées par "heure d'index".

Dans cette dernière approche, il devient alors important de $sort sur le regroupement _id évaluer. C'est parce qu'il contient la position "index", et cela sera nécessaire lors du déplacement de ce contenu dans un tableau que vous vous attendez à ordonner. Qui est bien sûr le dernier $group étape ici où les positions ordonnées sont mises dans un tableau avec $push .

Revenons aux "listes distinctes", les $size L'opérateur est utilisé dans tous les cas pour déterminer la "longueur" et donc le "nombre" de valeurs distinctes dans les listes pour "vers" et "depuis". C'est la seule véritable contrainte sur MongoDB 2.6 au moins, mais peut être remplacée par un simple "déroulage" de chaque tableau individuellement, puis un regroupement sur le _id déjà présent afin de compter les entrées du tableau dans chaque ensemble. C'est un processus basique, mais comme vous devriez voir le $size est la meilleure option ici pour les performances globales.

Pour finir, vos données de conclusion sont un peu décalées, car il est possible que l'entrée avec "ddd" dans "from" était également destinée à être la même dans "to", mais est plutôt enregistrée comme "bbb". Cela modifie le nombre distinct du troisième groupe "uid" pour "to" d'une entrée. Mais bien sûr, les résultats logiques étant donné les données source sont solides :

{ "_id" : 1000000, "count" : 3, "from_count" : 2, "to_count" : 2, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0 ] }
{ "_id" : 2000000, "count" : 2, "from_count" : 1, "to_count" : 1, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0 ] }
{ "_id" : 3000000, "count" : 5, "from_count" : 5, "to_count" : 4, "hours" : [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0 ] }

N.B La source a également une faute de frappe avec le délimiteur intercalé avec : au lieu d'une virgule juste après l'horodatage sur toutes les lignes.