MongoDB est une base de données NoSQL relativement simple à prendre en main et très riche fonctionnellement. Elle permet d’adresser les problématiques de temps réel dans un contexte Big Data (mise en cluster, haute disponibilité, tolérance aux pannes).

Pour ce quatrième volet, nous allons aborder les possibilités d’agrégation.

Agrégation simple

Pour illustrer l’agrégation, nous allons travailler sur une collection zips qui contient les villes des états-unis. Le niveau de granularité est le code postal (ZIP: Zoning Improvement Plan). Outre ce code ZIP, chaque document contient la population de la zone considérée, les coordonnées GPS, la ville et l’état de rattachement.

Nous pouvons commencer par calculer la population de chaque état en utilisant la fonction aggregate ,le mot clé $group pour regrouper sur les états, le mot clé $sum pour sommer les éléments de population.

db.zips.aggregate({$group:{_id:"$state",population:{$sum:"$pop"}}})

Examinons plus en détail la syntaxe.

$group indique un regroupement. La clé de regroupement est indiquée par id.

Dans notre cas, on lui passe le code de l’état $state(le $ précise qu’il faut reprendre le champ state de notre collection zip.

$sum: " $pop “ indique que l’on va additionner les populations de chaque code postal.

On peut faire des additions. On peut aussi faire des moyennes.

MongoDB propose plusieurs opérateurs d’aggrégation ($min, $max, $first, $last, $push, $addToSet, $stdDevPop, $stdDevSamp).

Vous trouverez plus de précision sur ces opérateurs dans la documentation MongoDB.

Calculer, c’est bien. Trier c’est mieux !

Comment faire si nous souhaitons trouver les villes les plus peuplées des états-unis ? On aimerait calculer la population de chaque ville puis trier les villes par ordre décroissant de leur population.

C’est possible grâce au mot clé $sort.

db.zips.aggregate({$group:{_id:"$city",population:{$sum:"$pop"}}},{$sort:{population:-1}})

Dans notre cas, la fonction aggregate prend en deuxième argument le tri des données qui sont traités séquentiellement. On parle ici d’étape ou de stage dans le traitement d’agrégation.

Rajoutons maintenant un filtre pour ne conserver que les villes de Californie. On pourrait le placer n’importe où dans notre chaîne de traitement. Mais le plus efficace est de le mettre en début de chaîne pour limiter les calculs ultérieurs.

Cela se fait avec le mot clé $match.

db.zips.aggregate({$match:{state:"CA"}},{$group:{_id:"$city",population:{$sum:"$pop"}}},{$sort:{population:-1}})

Dans nos exemples précédents, nous avons toujours utilisé un champ de la collection (dans notre cas, « pop ») pour faire nos calculs (somme ou moyenne). Ce n’est pas une obligation.

La commande suivante permet ainsi de compter le nombre de codes postaux par état.

db.zips.aggregate({$group:{_id:"$state",nb_zip:{$sum:1}}},{$sort:{nb_zip:-1}})

Cette approche nous permet ainsi d’avoir l’équivalent d’un count en SQL.

Le pipeline d’agrégation

Lorsque nous avons fait notre requête d’agrégation sur les villes les plus peuplées de Californie, nous avons en fait créer un enchaînement de 3 étapes (filtre, agrégation, tri). MongoDB appelle cette approche le pipeline d’agrégation.

Les étapes peuvent être multiples. Imaginons qu’au lieu de compter les codes postaux par état, nous désirions comptabiliser le nombre de villes par état. Comment faire ?

On va simplement faire un premier regroupement par état et ville (pour éviter de regrouper des villes situées dans des états différents mais portant le même nom) puis comptabiliser ensuite le nombre de villes par état.

La première étape consiste donc à opérer un regroupement sur 2 champs.

db.zips.aggregate({$group:{_id:{state:"$state",city:"$city"}}})

Puis on regroupe maintenant les villes par état. Le champ « état » s’appelle maintenant _id.state.

db.zips.aggregate({$group:{_id:{state:"$state",city:"$city"}}},{$group:{_id:"$_id.state",nb_villes:{$sum:1}}})

Amusons nous un peu. Imaginons maintenant que nous souhaitions afficher les états ayant le plus de villes à plus de 100 000 habitants. On va intercaler dans notre traitement un filtre sur le nombre d’habitants et rajouter à la fin un tri par ordre décroissant. On n’oublie pas dans la première étape de calculer la population de chaque ville puisqu’on va en avoir besoin dans notre filtre à l’étape 2.

db.zips.aggregate({$group:{_id:{state:"$state",city:"$city"},population:{$sum:"$pop"}}},{$match:{population:{$gt:100000}}},{$group:{_id:"$_id.state",nb_villes:{$sum:1}}},{$sort:{nb_villes:-1}})

Encore d’autres possibilités

Nous avons vu les types d’étape suivant:

  • $group: groupement des données
  • $match: filtre des données
  • $sort: tri des données

MongoDB propose de nombreux autres types. On peut citer par exemple sans cela soit exhaustif:

  • $project: pour sélectionner les champs que l’on souhaite conserver dans le résultat
  • $limit: pour limiter les résultats
  • $skip: pour sauter n résultats
  • $unwind: pour opérer une transposition sur un champ contenant un tableau
  • $out: pour enregistrer le résultat dans une nouvelle collection

Mais aussi des limitations

La documentation précise que les étapes d’agrégation ne doivent pas consommer plus de 100 Mo en mémoire sous peine de générer une erreur. Cette limitation pouvant être rapidement contraignante, on peut l’outrepasser en passant le paramètre allowDiskUse à true. Le passage de paramètre se fait en fin de commande après les stages. Pour différencier les stages des paramètres, il faut veiller à mettre les stages dans un tableau.

db.zips.aggregate([{$group:{_id:{state:"$state",city:"$city"},population:{$sum:"$pop"}}},{$match:{"_id.state":"CA"}},{$sort:{population:-1}}],{allowDiskUse:true})

Bien évidemment, si cela permet de dépasser la limitation, les performances vont s’en ressentir.

Dans tous les cas, n’oublions pas que MongoDB est une solution scalable permettant de répondre à un grand nombre de requêtes concurrentes. Pour des besoins d’agrégation sur un historique profond, privilégiez plutôt Hadoop ou Spark.