Méthodes d’optimisation des index MongoDB :

Rida kejji
8 min readNov 29, 2021

--

Cet article est issue de la formation de performance M201 proposée par Mongodb University.

Mise en place d’une collection de test mongoDB :

Pour pouvoir tester mes index mongodb j’ai mis en place un cluster Atlas (ça prend deux secondes et c’est gratuit voir le lien suivant. )

Pour exécuter les requêtes mongodb j’utilise mongodb Compass parce que ça donne un visuel mais n’importe quel CLI fairai l’affaire.

J’utilise pour mes tests qui vont suivre la collection suivante. Que j’importe dans mon cluster atlas.

Note : Pour détailler les requête envoyé j’utilise la fonction explain(), elle peut prendre les paramètres suivants :

  • querryPlanner(par défaut) : permet d’avoir des prédiction de la requête sans pour l’exécuter
  • executionStats/ allPlansExcecution : qui permet d’exécuter et de donner des statistiques sur l’exécution. Cela permet aussi d’avoir la meilleur stratégie d’index; si on a plusieurs index possible pour une requête la fonction compare les stats de ces derniers et retourne un “winningPlan” et des “rejectedPlans”.

Les propriétés d’utilisation des compound index (index composés ) :

Quand on crée un index avec plusieurs champs les conditions d’utilisation des index deviennent plus compliquées qu’avec un seul champ en index.

L’erreur est de voir le compound index comme étant un tableau a plusieurs dimensions. Il devrait être représenté sous forme d’un seul tableau à une dimension (ne pas oublier que sa vraie forme est une B-tree) :

On a 4 situations possibles :

1- On site les 4 champs de l’index :

Par exemple :

db.people.find({job:"Analyst", employer: "Apple", last_name :"Kejji", first_name :"Rida"}).explain("executionStats")

Le serveur va scanner qu’un seul index et un seul document :

2- Requête avec un préfixe de l’index :

Si on envoie une requête avec seulement le job et l’employer comme :

db.people.find({job:"Analyst", employer :"Apple"}).explain("executionStats")

L’index sera quand même utilisé, c’est le principe des préfixes d’index. une partie de l’index sera donc utilisé et on aura les résultats suivants :

3- On requête en sautant des champs :

Par contre il est impératif de respecter l’ordre de déclaration de l’index et de ne pas oublier un champ. Par exemple si on envoie la requête suivante (en sautant le champ employer) :

db.people.find({job:"Analyst", last_name :"Kejji", first_name :"Rida"}).explain("executionStats")

L’index sera utilisé et 1 seul document sera scanné et retourné. Néanmoins l’index n’aura été utilisé que partiellement, on aura donc scanné plusieurs index keys avant de retrouver la bonne donnée.

4- On requête sans citer le premier champ de l’index :

Si jamais on envoie une requête qui ne contient pas le champ job du tout, alors l’index ne sera pas du tout utilisé et on aurait un collection scan.

Note : c’est pour ça qu’il faut toujours mettre les champs obligatoires en premier et ensuite les champs optionnels.

Le trie avec des index composés :

On peut trier en utilisant un compound index si et seulement si on respect

  • L’ordre des champs déclaré dans l’index (comme pour filtrer/chercher)
  • Le sens du trie de l’index ou alors son inverse complet. C-à-d, que si on déclare un index comme celui-ci ({a: 1, b:-1, c:1}). On peut faire une requête de trie ({a: 1, b:-1, c:1}) ou ({a: -1, b: 1, c: -1}) seulement. Si on trie avec ({a: 1, b:-1, c:-1}) par exemple, le trie sera fait en in-memory (la collection sera copiée du disque vers la RAM pour être triée ce qui est très chronophage).

La loi de ESR pour le filtre et le trie des compound index :

On reprenant l’index déclaré au début et la collection “people” de mongodb. Si on filtre la collection avant de la trier comme ceci :

expRunTotal.find({email : "teresabrown@yahoo.com"}).sort({job :1})

On aura les stats d’exécution suivants :

Même si la première commande de find() ne nous retourne qu’un seul élément le serveur va parcourir tous les index pour trier les documents. Ceci se produit car le serveur essaye d’éviter au maximum de faire des in-memory sort.

Pour pouvoir utiliser le même index pour le filtre et le trie des collections il faut respecter l’ordre de l’index et de ses préfixes.

La règle de ESR indique qu’il faut toujours faire des conditions d’égalité avant les conditions de trie, les conditions de trie avant les conditions de range ($gte/ $lte) et les conditions d’égalité avant les conditions de range.

La démonstration dans cette vidéo réalisée par des ingénieurs de MongoDB. Exemple :

Ici en mettant le champ de trie avant le champ de range on est obligé de scanner plus d’index vu qu’on utilise plus l’index du zip code (situation 3 des compound index)

Note : Les opérateurs $nin, $ne et $regex sont de type Range

Les multi-key index :

Les multi-key index permettent d’indexer un champ d’un document qui contient une liste. Une condition s’applique : on ne peut avoir qu’un seul multi-key dans un index.

Attention : Les multi-key index sont extrêmement gourmands niveau performance (notamment pour les Write). Il faut les faire accompagner d’un partial index.

Note : Pour savoir si on a bien créé un multikey index, on peut voir dans la réponse de explain() le champ Multikey : true.

Sparse index :

Il s’agit d’un index qui est créé que si un champ existe dans le document.

db.orders.createIndex( { "categorie": 1 }, { delivered: true } )

Partial index :

Il s’agit d’une généralisation du sparse index, il permet d’appliquer un filtre aux documents qu’on souhaite indexer, notamment un filtre d’existence d’un champ (en gros le sparse index ne sert à rien).

Le partial index permet d’améliorer les performances vu qu’il y a moins d’index à parcourir pour en saisir un nouveau.

Text index :

Il est utilisé lorsque l’on souhaite chercher une partie d’un texte (équivalent de LIKE en BDDR ou $regex en mongodb).

db.products.createIndex( { "productName": "text" })

Il s’agit en fait d’un multi-key index pour chaque mots du champ text.

Note : le text index est non-case-sensitive, si on souhaite qu’il prennent en compte les majuscules/ minuscule il faut changer les collations mongodb.

Les wildcard index :

Supposons qu’on ai le schéma suivant :

{
"_id" : 12345,
"product_name" : "Switch Ninstendo"
"price" : "500",
"attributes" : {
color : "blue",
size : "small",
language : "french"
}
}

Si on a 30000 documents et qu’on souhaite faire différentes recherches selon plusieurs champs, des fois en fonction du champ attributes.couleur, des fois le price ou alors attribute.size pour un besoin métier de logistique par exemple. On ne peux pas avoir un compound index vu que c’est si on respecte pas l’ordre on ne pourra pas l’utiliser. Mettre un index sur chaque champs serait une torture pour le serveur (pour 30 => 30000 index).

Avant la version 4.2 de mongodb une solution c’était de transformer le schema comme ceci : (Attribute pattern)

{
"k" : "_id",
"v" : 12345,
"k":"product_name",
"v" :"Switch Ninstendo"
"k" : "price",
"v" : "500",
....
}

et mettre un index sur ({ v:1, k:1}). Ce qui permettait d’avoir de bonne performance mais qui implique un changement complet du schema.

Solution : Les wildcard index

Pour les déclarer : db.product.createIndex( { "attributes.$**" : 1 } )

Les wildcard permettent d’utiliser le même index pour tous les champs soit du document soit d’un sous-document, notamment ici avec attributes.

Les wildcards index sont donc très utiles pour les cas où les champs sont imprévisibles (Polymorphic schemas).

Les index hybrides :

Si vous utilisez la version de mongodb 4.2 ou plus cette partie ne vous concerne pas vraiment vu que les indexes hybrides sont utilisés par défaut.

Lors de la création d’un index les requête vers la BDD sont bloquées, c-à-d que si par exemple on crée un index dans une BDD de compte bancaire et qu’au même temps une transaction doit avoir lieux, cette dernière sera bloquée le temps que l’index soit créé.

Il existe une commande dans les versions avant mongodb 4.2 qui permet de run la création d’index de façon non-bloquante : db.collection.createIndex({username :1, background =true}.

Querry plans :

Il s’agit juste d’une option dans la fonction explain qui permet d’executer toutes les requêtes durant un petit laps de temps pour tester la plus performante, c’est ce qu’on retrouve dans winningPlan. Une fois que ceci est fait un cache est créé avec cette solution pour ne pas refaire la même opération la prochaine fois. Le cache est vidé lors d’un reset de la BDD ou de l’index.

L’allocation de ressources pour les index :

Les index sont certes efficaces mais pas magiques, ils nécessitent un espace de stockage.

A leur création les index sont stockés dans un disque, du coup c’est chill. Si la taille de l’index est trop grande pour le disque ce serait le cas aussi pour la collection.

MAIS ceci n’est pas valable pour un déploiement comme celui-ci :

Car dans ce cas on risquerait de ne plus avoir d’espace pour l’index mais encore de l’espace pour les collections. Du coup si l’index est utilisé les nouveaux documents insérés dans les collections ne seront pas pris en compte (je suppose car je n’ai pas essayé).

Pour être utilisé les index doivent être envoyés à la RAM, de façon à ce qu’ils soient parcourus. Si la RAM est suffisamment grande pour accueillir tout le fichier d’index c’est parfait. Sinon l’index sera diviser en plusieurs parties => une perte en performances à cause des opérations de vidage de la RAM.

Conclusion :

Il faut bien choisir ses index parce que sinon ça peut faire perdre du temps et de l’argent. Il y a aussi une autre partie sur les index réservés aux agrégations qui mérite d’être étudiées.

--

--

Rida kejji
Rida kejji

No responses yet