TP MongoDB: prise en main et pipeline d’agrégation
L’objectif de ce TP est de mettre en pratique le requêtage via une algèbre en utilisant MongoDB.
Dans une première partie, on prendra en main le shell MongoDB ainsi que les requêtes de base. Dans une seconde partie, on prendra en main quelques opérateurs d’aggrégation. Dans la troisième partie on combinera les opérations d’aggrégation dans des pipelines plus complexes.
Pensez à rédiger un compte-rendu au format Markdown dans lequel vous indiquerez vos remarques ainsi que les différentes requêtes que vous aurez exécuté.
1. Connexion
Pour faire le TP, trois choix possibles: utiliser le serveur mongodb commun, installer mongodb sur votre propre machine via des packages ou bien démarrer un serveur MongoDB via docker. Dans les deux dernier cas, il faudra importer les données contenues dans le fichier mif04-mongodb.zip à télécharger.
Pour effectuer les requêtes, on pourra utiliser le shell mongosh
, le shell mongo
ou bien MongoDB Compass.
1.1 Connexion au serveur commun
Un serveur MongoDB est disponible sur bd-pedago.univ-lyon1.fr
. Ce serveur
n’est disponible que depuis le réseau du campus: depuis les machine de TP ou
depuis le Wifi Eduroam. Il faut donc activer le VPN pour y avoir accès depuis
l’extérieur du campus ou depuis des réseaux Wifi comme Eduspot ou UCBL-Portail.
La base à laquelle on peut se connecter pour le TP est mif04
avec le compte
mif04
. Le mot de passe sera indiqué via tomuss.
On pourra adapter la commande suivante pour se connecter:
mongosh -u mif04 -p "remplacez-moi" mongodb://bd-pedago.univ-lyon1.fr/mif04
Changer
remplacez-moi
par le mot de passe qui aura été communiqué
Changer
mongosh
parmongo
si la première commande n’est pas disponible. Simongo
n’est pas non plus disponible, télécharger mongosh-1.1.7-linux-x64.tgz (utiliser/tmp
au besoin car l’archive fait environ 60 Mo)
Pour MongoDB Compass, utiliser la chaîne de connexion suivante, en remplaçant remplacez-moi
par le mot de passe:
mongodb://mif04:remplacez-moi@bd-pedago.univ-lyon1.fr:27017/mif04?authSource=mif04&readPreference=primary&appname=MongoDB%20Compass&directConnection=true&ssl=false
1.2 Installation du serveur via un système de packages
Installer MongoDB Community Edition via un système de packages, voir la documentation d’installation.
Importer ensuite les données via la commande suivante (à exécuter dans le répertoire où le zip a été extrait):
for i in *.json; do mongoimport -c $(basename $i .json) --file=$i --type=json --jsonArray; done
On peut ensuite simplement lancer mongosh
pour se connecter.
Pour MongoDB Compass, laisser la chaîne de connexion vide.
1.3 Démarrage via docker.
Cette option nécessite d’avoir déjà Docker installé (Windows/MacOS: utiliser Docker Desktop, Linux: installer via des packages).
La commande suivante permet de lancer un serveur MongoDB en tâche de fond:
docker run --name mongodb -d -p 27017:27017 mongo:5.0.5-focal
Pour arrêter le serveur:
docker stop mongodb
Pour redémarrer un serveur arrêté:
docker start mongodb
Pour détruire le serveur:
docker rm --force mongodb
Pour importer les données (à lancer depuis le répertoire le zip a été décompressé):
for i in *.json; do cat $i | docker exec -i mongodb mongoimport --type=json --jsonArray -c $(basename $i .json) ; done
Pour lancer le shell:
docker exec -it mongodb mongosh
Pour MongoDB Compass, laisser la chaîne de connexion vide.
1.4 Vérification de la connexion
Dans le shell mongo, vérifiez que vous êtes bien connecté en lançant la commande:
show collections
qui doit produire une sortie du type:
grades
neighborhoods
restaurants
zips
Cette sortie indique les collections disponibles dans la base courante.
Dans MongoDB Compass, ces 4 collections doivent apparaître dans la base mif04
ou dans la base test
.
2. Premières requêtes
Pour ces requêtes, on utilisera les collections grades
et zips
.
La syntaxe dans mongosh
est db.
collection.
methode où collection est le nom de la collection à requêter (l’équivalent d’un FROM
en SQL) et méthode est la manière de requêter la collection.
db.collection.findOne()
: renvoie un document de la collection (doc).Pour chacune des collections
grades
etzip
, récupérer un document, puis en déduire un type pour les éléments de cette collection.db.collection.find({})
: récupère les documents de la collection sans les filtrer (doc).Récupérer quelques documents de grades et vérifier qu’ils sont conformes au type de la question précédente.
db.collection.find({}).count()
: compte le nombre de résultats de la requête.Donner le nombre de résultats de la requête précédente (résultat attendu: 280).
db.collection.find({
field:
value})
: récupère les documents dont le champ field a la valeur value. Il est possible de spécifier plusieurs champs.Récupérer les documents dont le
class_id
vaut20
dans la collectiongrades
(7 résultats).db.collection.find({
field: {
op:
value} })
: exprime une condition de sélection sur le champ field: op est la fonctionde comparaison et value la valeur à laquelle on veut comparer la valeur du champ (doc des opérateur de sélection).Récupérer les documents dont le
class_id
est inférieur ou égal à20
. L’opérateur inférieur ou égal se note$lte
. (188 résultats)db.collection.find({ $expr: {
arbre de syntaxe de l’expression en json} } )
: exprime une condition générique. La syntaxe est la représentation en json de l’expression (doc). Les champs sont représentés par la syntaxe$champ
ou$champ1.souschamp2
. Les arguments sont souvent donnés sous forme d’une liste (mais il faut vérifier la documentation de chaque opérateur). Par exemple,{ $lte: [ "$class_id", 20 ] }
est une expression indiquant que le champclass_id
(via"$class_id"
) est inférieur ($lte
) à20
.Récupérer les documents pour lesquels le
student_id
est supérieur ou égal auclass_id
(188 résultats).-
Récupérer les documents dont le
class_id
est compris entre10
et20
(100 résultats). Faire une version avec un double filtre sur le champclass_id
(via la syntaxe{
field: {
op1:
value1,
op2:
value2} }
), puis une autre version avec un$expr
contenant un$and
. db.collection.find({
condition}, {
projection spec} )
: permet de choisir les champs de sortie de la requête (doc). Il est possible de supprimer des champs, voir de créer de nouveaux champs en utilisant une expression comme ci-dessus.Donner tous les documents de la collection avec un champ supplémentaire qui est la somme des champs
class_id
etstudent_id
.
3. Prise en main de quelques étapes d’agrégation
En MongoDB, les requêtes complexes s’effectuent entre autres via le pipeline d’agrégation. Ce pipeline est constituées d’étapes de transformation successives appliquées à une collection et passées sous forme de liste à db.
collection.aggregate
. Dans cette partie, on va appréhender les effets de quelques opérateurs pris individuellement. Syntaxiquement, on spécifiera les requêtes comme suit:
db.grades.aggregate([
{ /* operateur */ : { /* specification */ }},
])
Pour compter le nombre de résultats, on pourra précéder comme suit:
db.grades.aggregate([
{ /* operateur */ : { /* specification */ }},
{ $count: "count" },
])
{ $project: {
projection spec}}
: applique une projection spécifiée comme pour le deuxième argument dedb.collection.find()
(doc).Reprendre la requête précédente, mais en utilisant le pipeline d’agrégation avec un
$project
.{ $match: {
find spec} }
: applique un filtre similaire au premier argument dedb.collection.find()
(doc).Dans la collection
zips
récupérer les documents ayant une population supérieure ou égale à10000
(7634 résultats).{ $sort: {
sort spec} }
: trie la collection selon les champs spécifiés (doc). Pour chaque champ, on indique s’il est trié par ordre croissant ou décroissant. Les champs sont listés par ordre d’apparition dans l’ordre lexicographique utilisé. Par exemple, sur la collectionzips
,{ state: 1, pop: -1 }
inquera de trier par état (state
), puis par population (pop
) décroissante.Dans la collection
grades
, renvoyer les documents triés parclass_id
, puis parstudent_id
(par ordre croissant pour les 2 champs).{ $unwind:
chemin}
: chemin indique un champ, éventuellement dans un document imbriqué (via la syntaxe commençant avec$
). Ce champ contient un tableau. L’opérateur produit un document pour chaque élément du tableau. Les documents produits sont identique au document de départ, sauf pour le champ désigné qui, au lieu du tableau, contient un élément de ce tableau.Dans la collection
grades
, produire un document pour chaque élément du tableau contenu dans le champscores
(1241 résultats).{ $group: { _id:
spec id,
champ1: {
agg1:
expr1}, ...} }
(doc): regroupe les documents selon la valeur spec id, qui est une expression utilisant la syntaxe des expressions d’agrégation (chemins avec$
, opérateurs d’expressions, etc). On peut fabriquer des documents, ce qui permet de représenter des n-uplets pour utiliser des combinaisons de valeurs comme clés. Pour chaque groupe, produit un document avec la valeur de_id
, ainsi que chaque champ supplémentaire (champ1, etc) avec la valeur calculée par l’opérateur d’agrégation correspondant (agg1, etc) à qui on aura passé le tableau des valeurs calculées par les expressions (expr1, etc). Les fonctions d’agrégation utilisables sont indiquées dans la documentation. Par exemple{ $group: { _id: "$state", total: { $sum: "$pop" }}}
va créer un document pour chaque valeur du champstate
avec un champtotal
qui sera la somme des champspop
des documents du groupe.Dans la collection
zips
, donner pour chaque ville la population minimale (16584 résultats).{ $lookup: { from:
coll, localField:
champ local, foreignField:
champ ext, as:
champ out} }
(doc): effectue une jointure aveccoll
. La jointure se fait sur une égalité entre le champ champ local pour les documents venant du pipeline courant et le champ champ ext pour les documents venant de la collection coll. Produit un document pour chaque document de la collection du pipeline, avec un champ supplémentaire (champ out) contenant le tableau des documents de coll satisfaisant le critère de jointure.Effectuer une jointure entre
grades
etzips
sur les champsstudent_id
etpop
. Quel est le type du résultat ?
4. Pipelines d’agrégation
Dans cette partie, on va créer des pipelines d’aggrégation en enchaînant plusieurs opérateurs. Il faudra prévoir l’enchaînement des transformations à effectuer, et pour chaque transformation du pipeline, bien prévoir le type des documents du résultat afin de pourvoir configurer correctement les transformations suivantes.
Exemple: donner, par ordre de population décroissante, les états ayant plus de 10 000 000 d’habitants:
db.zips.aggregate([
{ $group: { _id: "$state",
population: { $sum: "$pop" } } },
{ $match: { population: { $gte: 10000000 } } },
{ $sort: { population: -1 }},
])
Répondre aux questions suivantes via une requête utilisant un pipeline d’agrégation.
- Calculer le nombre de notes de chaque type.
Indice: Commencer par extraire les notes individuellement avec
$unwind
. Indice: On peut calculer un nombre d’occurrences en sommant la valeur1
sur un groupe. - Donner pour chaque matière, la meilleure note d’examen.
- Donner les dix villes les plus peuplées. Indice: On pourra utiliser l’étape $limit.
- Donner la population moyenne des villes pour chaque état.
- Donner pour chaque matière, les étudiants ayant une note d’examen supérieure à 50. Indice: On pourra utiliser l’opérateur d’agrégation $push dans un $group pour constituer la liste.
- Donner pour chaque étudiant et chaque matière sa note générale. La note générale est la moyenne de chaque type de note. S’il y a plusieurs notes d’un même type, on prendra d’abord la moyenne de ce type avant de l’utiliser pour calculer la moyenne avec les autres types. Indice: Commencer par calculer pour chaque étudiant, chaque matière et chaque type de note sa moyenne.
- Donner, pour chaque matière le type d’épreuve la mieux réussie. Le type d’épreuve la mieux réussie est celui ayant la meilleure moyenne, calculée sur l’ensemble des notes de ce type pour cette matière. Indice: Un moyen de faire consiste à calculer la meilleure moyenne (via $max), tout en conservant l’ensemble des moyennes dans un tableau via $push.