Blog ENI : Toute la veille numérique !
🎁 Jusqu'au 25/12 : 1 commande de contenus en ligne
= 1 chance de gagner un cadeau*. Cliquez ici
🎁 Jusqu'au 31/12, recevez notre
offre d'abonnement à la Bibliothèque Numérique. Cliquez ici
  1. Livres et vidéos
  2. MongoDB
  3. L’indexation avec MongoDB
Extrait - MongoDB Comprendre et optimiser l'exploitation de vos données (avec exercices et corrigés) (2e édition)
Extraits du livre
MongoDB Comprendre et optimiser l'exploitation de vos données (avec exercices et corrigés) (2e édition)
1 avis
Revenir à la page d'achat du livre

L’indexation avec MongoDB

Comment ça marche ?

Dans la terminologie des bases de données, relationnelles ou non, un index est en tous points semblable à celui que l’on trouve à la fin de n’importe quel livre : on y regroupe les termes importants qui figurent dans l’ouvrage avec, en face de ceux-ci, les numéros des pages dans lesquelles ils se trouvent. Ceci nous évite tout simplement d’avoir à relire la totalité du livre lorsque nous cherchons un simple terme.

Par analogie, un index posé sur le champ ou le sous-champ d’une collection nous évite d’avoir à parcourir toute notre collection pour retrouver les valeurs de ce champ (ou sous-champ) correspondant à notre requête et participe ainsi à garder le temps d’exécution de nos requêtes le plus petit possible.

Les index ont des avantages et des inconvénients : ils améliorent considérablement les temps d’exécution des requêtes en lecture, mais ils ralentissent les opérations d’écriture telles que les insertions, les suppressions ou les mises à jour, qui nécessitent leur reconstruction. Cependant, les ralentissements observés sont généralement négligeables par rapport à la réduction du temps d’exécution qu’ils engendrent.

Pour savoir quels champs d’une...

Index simples

Lorsqu’une collection est créée, MongoDB génère automatiquement un index sur le champ _id. Cet index ne peut aucunement être supprimé, car il garantit l’unicité même de cet identifiant. Pour créer vous-même un index, il vous faut utiliser la fonction createIndex dont voici la syntaxe :

db.collection.createIndex(< champ_et_type >, < options >) 

Laissez-moi vous présenter la nouvelle version de notre collection personnes :

db.personnes.drop() 
 
db.personnes.insertMany( 
[ 
{"nom": "Durand", "prenom": "René", "interets": ["jardinage",
"bricolage"], "age": 77}, 
{"nom": "Durand", "prenom": "Gisèle", "interets": ["bridge", 
"cuisine"], "age": 75}, 
{"nom": "Dupont", "prenom": "Gaston", "interets": ["jardinage",  
"pétanque"], "age": 79}, 
{"nom": "Dupont", "prenom": "Catherine", "interets": ["cuisine"], "age": 66}, 
{"nom": "Duport", "prenom": "Eric", "interets": ["cuisine", 
"pétanque"]...

Index composés

Un index peut porter sur plus d’un champ : c’est ce que l’on appelle un index composé (compound index). Dans ce type d’index, l’ordre dans lequel les champs sont énumérés a son importance. Supprimons notre index idx_age et créons un index composé nommé idx_age_nom qui portera sur l’âge puis sur le nom des personnes :

db.personnes.createIndex({"age": 1, "nom": 1}, 
{"name": "idx_age_nom"}) 

L’index sera ordonné d’abord par valeurs croissantes d’âge et par ordre alphabétique de nom ensuite, au sein de chacune des différentes valeurs d’âge.

Lorsqu’un index composé dont le préfixe n’est pas une chaîne de caractères, un tableau ou un sous-document est utilisé avec une collation, une requête n’utilisant pas la bonne collation pour le champ texte indexé peut toutefois s’appuyer sur le préfixe de l’index.

Supposons que notre index précédent ait été créé de cette façon :

db.personnes.createIndex( 
  {"age": 1, "nom": 1},  
  {"name": "idx_age_nom", "collation": { locale: "fr" }} 
) 

La requête suivante qui utilise pourtant la collation binaire de base pour la comparaison des chaînes de caractères (et non fr) pourra néanmoins utiliser idx_age_nom car age en constitue le préfixe :

db.personnes.find({"age": {$gt: 40}, "prenom": "Christophe"}) 

Préfixe d’un index

Une requête peut utiliser la totalité des champs constituant l’index composé ou bien une sous-partie seulement, à la condition qu’elle soit constituée...

Index uniques

Les index uniques garantissent qu’une valeur donnée apparaîtra au maximum une fois dans l’index. Nous avons vu précédemment qu’un tel index était systématiquement posé sur le champ _id des documents d’une collection et qu’il était par ailleurs impossible de le supprimer ! Dans l’état actuel de notre collection personnes, nous ne pourrions pas poser ce genre d’index sur le champ nom car la plupart des valeurs de ce champ a plusieurs occurrences. Par contre, nous pourrions créer un index unique sur le champ prenom, dont toutes les valeurs sont uniques :

db.personnes.createIndex({"prenom": 1}, {"unique": true}) 

Désormais, toute tentative d’insérer une personne portant un prénom déjà présent dans un des documents de notre collection se soldera par un cuisant échec. Évidemment, poser un index unique sur un champ qui a de très fortes probabilités de contenir des doublons est une assez mauvaise décision.

Par ailleurs, maintenant que notre champ prenom fait l’objet d’une contrainte d’unicité, nous ne pourrons pas insérer plus d’un document qui ne contienne pas ce champ. L’insertion suivante échoue en partie parce que, prenom étant marqué null lors de l’insertion...

Indexation des objets et des tableaux

L’indexation des documents contenus dans des documents se fait sensiblement de la même manière que pour un champ de type scalaire. Supposons que certains des documents de notre collection personnes contiennent désormais un sous-document à la clé adresse contenant les détails de l’adresse postale d’un individu. Pour illustrer cela, mettons à jour les informations relatives aux époux Lejeune :

db.personnes.updateMany( 
  {"nom": "Lejeune"}, 
  {$set : { 
    "adresse": { 
      "num": 546, 
      "voie": "rue", 
      "nom": "Descartes", 
      "code": 71230, 
      "ville": "Saint-Vallier" 
    } 
  } 
}) 

Puis créons un index sur le champ code du document imbriqué dans la clé adresse :

db.personnes.createIndex({"adresse.code": 1}, {"name": "idx_adr_cod"}) 

Cette requête pourra se baser dessus :

db.personnes.find({"adresse.code" : 71230}) 

Sachez qu’il est tout à fait possible d’indexer le sous-document adresse au complet :

db.personnes.createIndex({"adresse":...

Index géospatiaux

L’utilisation de la géolocalisation est complètement ancrée dans les mœurs des internautes : le trajet des vacances, la trottinette électrique la plus proche, le temps moyen pour aller à pied jusqu’à votre restaurant préféré... Difficile de faire sans !

MongoDB propose plusieurs types d’index pour gérer la prise en charge des requêtes géospatiales : les index de type 2dsphere seront utilisés par les requêtes géospatiales opérant sur une surface sphérique tandis que les index 2d serviront plutôt aux requêtes effectuées sur un plan euclidien. Si le champ contenant les données géospatiales dans votre collection plan se nomme geodata, vous créez un index de type 2d en exécutant la commande suivante :

db.plan.createIndex({"geodata": "2d"}) 

Tandis qu’un index de type 2dsphere sur le champ geodata d’une collection nommée sphere se pose de cette façon :

db.sphere.createIndex({"geodata": "2dsphere"}) 

1. Les index 2d

Les index 2d utilisent des couples de coordonnées dits legacy, c’est à dire conservés pour des raisons de rétro-compatibilité. En effet, ce type de coordonnées était utilisé jusqu’à la version 2.2 de MongoDB, mais il lui a depuis été préféré GeoJSON, un format unifié pour décrire des données géographiques à l’aide de JSON. Ce format est en voie de normalisation et vous trouverez ses spécifications complètes à l’adresse https://tools.ietf.org/html/rfc7946.

Si vous utilisez ces coordonnées legacy, il est recommandé de stocker les coordonnées d’un point sous la forme d’un tableau en commençant toujours par l’abscisse, suivie de l’ordonnée :

db.plan.insertOne({"nom": "Point 1", "geodata": [1,1]}) 

Si ces coordonnées legacy matérialisent un couple longitude/latitude alors la longitude doit apparaître en premier :

db.plan.insertOne({"nom": "Point 1", "geodata": [4.805528, 43.949317]}) 

Toutefois...

Index partiels

Les index partiels ont été introduits à partir de la version 3.2 afin de remplacer progressivement les index sparse, bien plus limités. En effet, un index sparse ne contient que les documents dans lesquels le champ ciblé est présent.

Un index partiel se comporte un peu comme un index sparse dans la mesure où il permet lui aussi d’indexer un sous-ensemble des documents contenus dans une collection. Ces index utilisent un filtre qui va agir comme un critère de sélection des documents qui vont y figurer. Mais contrairement aux index sparse qu’ils ambitionnent de supplanter, le filtre n’est pas forcément un champ ciblé par l’index comme nous le verrons. Leur avantage est évident : ces index de taille réduite permettent d’économiser de la mémoire tout en ayant de meilleures performances qu’un index classique lors des écritures.

Construisons une collection nommée disques dans laquelle nous injectons trois documents :

db.disques.insertMany([{ 
    "groupe": "The Who", 
    "titre": "Tommy", 
    "prix": 5, 
    "dispo": true 
}, { 
    "groupe": "AC/DC", 
    "titre": "Powerage", 
    "prix": 8, 
    "etat": null, 
    "dispo": false, 
}, { 
    "groupe": "Queen", 
    "titre": "Innuendo", 
    "prix": 9, 
    "etat": "neuf", 
    "dispo": true 
}]) 

Notre index...

Index TTL

Les index TTL ont une durée de vie (Time To Live) limitée. Ils ne peuvent être appliqués que sur un seul champ, à condition qu’il soit de type date. Les index TTL servent à supprimer des documents dont la date a expiré. Ils sont utiles dans la gestion de logs, de sessions, de paniers ou toute autre chose ayant une durée de vie limitée. Pour les créer, la syntaxe est identique à celle d’un index standard, il suffira de préciser le nombre de secondes après lequel le document sera considéré comme ayant expiré.

La suppression des documents expirés est effectuée par le moniteur TTL, une tâche de fond qui s’exécute toutes les minutes. Il est possible de voir la valeur par défaut du paramètre ttlMonitorSleepSecs en utilisant la commande d’administration suivante :

db.adminCommand({"getParameter":1, "ttlMonitorSleepSecs": 1}) 

Nous vérifions bien le fait que, par défaut, notre moniteur est invoqué toutes les 60 secondes :

{ "ttlMonitorSleepSecs" : 60, "ok" : 1 } 

Pour changer la valeur de ce paramètre et exécuter la tâche toutes les 30 secondes, il vous faudra modifier la valeur en utilisant setParameter :

db.adminCommand({"setParameter":1, "ttlMonitorSleepSecs":...

Index clustered

Un index regroupé (clustered) ordonne les documents au sein d’une collection en fonction de la valeur de sa clé, garantie unique. De tels index caractérisent les clustered collections.

Contrairement aux collections « classiques » qui stockent les identifiants des documents dans une structure de données arborescente différente de celle dans laquelle sont stockés les documents eux-mêmes, les clustered collections regroupent tout dans la même structure de données, le champ identifiant le document servant d’index en face duquel va se trouver le document ciblé, supprimant ainsi la nécessité de parcourir deux arbres pour parvenir à le trouver. De cette façon, nous ne ferons plus qu’une seule opération pour lire, insérer, supprimer ou mettre à jour !

Voici quelques particularités des clustered indexes :

  • Il ne peut exister qu’un seul clustered index sur une collection et il est obligatoirement posé lors de la création de celle-ci.

  • Un clustered index se pose uniquement sur le champ _id de la collection mais ce champ peut être d’un type autre qu’ObjectId, du moment qu’il est garanti unique et immuable. Il convient toutefois de garder l’index le plus petit possible et, si possible, de s’appuyer sur des valeurs qui augmentent de façon...

Index textuels

MongoDB met à votre disposition un type particulier d’index pour la recherche à l’intérieur de champs de type texte ou de type tableau contenant du texte. Pour créer un index textuel il n’est plus question d’ordre croissant ou décroissant comme nous l’avons précédemment vu, il suffit simplement de préciser en face du nom du champ que son index sera de type text :

db.collection.createIndex({"champ": "text"}) 

Gardez à l’esprit qu’une collection ne peut héberger qu’un seul index textuel, qui peut toutefois porter sur plusieurs champs. Un index textuel ne peut pas utiliser de collation.

Commençons par créer la collection livres qui va nous servir à manipuler nos index textuels :

db.livres.insertMany([ 
   { 
       "auteur": "Jack London", 
       "titre": "Croc-Blanc", 
       "resume": "Croc-Blanc est un fier et courageux chien-loup", 
       "pointsDeVente" : [ 
           {"ville": "Aix-en-Provence", "librairie": "Goulard"} 
       ] 
   }, 
   { 
       "auteur": "Stendhal", 
       "titre": "Le Rouge et le noir", 
       "resume": "Le parcours de Julien Sorel" 
   }, 
   { 
       "auteur": ["Goscinny", "Uderzo"], 
       "titre": "Astérix", 
       "resume": "Les aventures du fier guerrier Gaulois" 
       "pointsDeVente" : [ 
          {"ville": "Marseille", "librairie": "La Réserve à bulles"}, 
          {"ville": "Avignon", "librairie":...

Intersection des index

L’intersection des index consiste à utiliser plusieurs index pour satisfaire une requête. Prenons comme base de travail la collection meteo, contenant deux documents très simples :

db.meteo.insertMany([ 
   { 
       "ville": "Marseille",  
       "temperatures": {"journee": 38.5, "nuit": 25},  
       "date": new Date("2024-08-15"), 
       "fiable": true 
   }, { 
       "ville": "Paris",  
       "temperatures": {"journee": 28.3, "nuit": 19.6},  
       "date": new Date("2024-08-15"),  
       "fiable": true 
   } 
]) 

Nous allons poser deux index dessus : un premier qui cible le champ journee du document temperatures :

db.meteo.createIndex({"temperatures.journee": -1}) 

Un second, composé, qui cible d’abord le champ ville (classé par ordre alphabétique) puis, par ordre décroissant, le champ date :

db.meteo.createIndex({"ville": 1, "date": -1}) 

Voici quelques requêtes...

La méthode explain

Cette méthode affiche à l’écran un document contenant des informations relatives à la planification des requêtes et peut également afficher des statistiques d’exécution. Elle a deux modes de fonctionnement : elle peut s’appliquer à une collection ou bien à un curseur.

Si elle est appliquée à une collection, sa syntaxe est la suivante :

db.collection.explain(informations).<méthode> 

Si elle détaille un curseur, elle prend cette forme :

db.collection.find().explain(informations) 

Dans les deux cas, son unique paramètre est une chaîne de caractères qui spécifie la nature des informations à afficher. Ce paramètre peut prendre les valeurs suivantes :

  • queryPlanner

  • executionStats

  • allPlansExecution

Si vous ne précisez pas de paramètre à explain, c’est l’option queryPlanner qui sera la valeur par défaut. Voici quelques exemples d’utilisation sur notre collection livres, tout d’abord une utilisation sur un curseur avec l’option par défaut :

db.livres.find().explain() 

La même chose, mais appliqué à la collection (le résultat produit sera identique) :

db.livres.explain().find() 

Les détails concernant les statistiques d’exécution, au niveau collection :

db.livres.explain("executionStats").find() 

Les détails concernant les plans d’exécution, toujours au niveau collection :

db.livres.explain("allPlansExecution").find() 

Quelle que soit l’option passée à explain(), nous constatons que les informations liées au planificateur de requêtes sont constamment présentes dans le document affiché à l’écran. Si nous nous attardons sur le document généré par le tout dernier explain(), il apparaît que l’ensemble des renseignements y figure : ceux liés au planificateur, aux statistiques d’exécution et aux plans d’exécution, qui sont venus s’ajouter sous la forme d’un champ de type tableau dans le document des statistiques d’exécution lui-même. Ces différentes constituantes figurent en gras ci-après :

{ 
   "queryPlanner"...

Forcer l’utilisation d’un index avec hint

L’opérateur $hint sert à forcer une requête à utiliser un index donné. Il possède une méthode raccourcie dans le shell, qui s’applique à un curseur et possède la forme suivante :

db.collection.find( < critères > ).hint( < index > ) 

Si vous listez l’ensemble de la collection meteo tout en appliquant explain sur le curseur qui en résulte, vous constaterez qu’un COLLSCAN est opéré, ce qui semble logique :

db.meteo.find().explain() 

winningPlan: { stage: 'COLLSCAN', direction: 'forward' } 

Maintenant forçons l’utilisation de l’index posé sur le champ ville et appliquons explain sur le tout pour confirmer le changement de stratégie de l’optimisateur de requête, auquel nous avons quelque peu forcé la main :

db.meteo.find({}).hint({"ville": 1}).explain() 

Le plan gagnant est bien de type IXSCAN dorénavant !

    winningPlan: { 
      stage: 'FETCH', 
      inputStage: {
        stage: 'IXSCAN', 
        keyPattern: { ville: 1 }, 
        indexName:...

Cacher un index avec hideIndex

Cacher un index consiste simplement à le désactiver, c’est-à-dire à faire en sorte qu’il ne soit plus exploitable par le planificateur de requêtes de MongoDB. Cette action peut être défaite et constitue par là même un très bon moyen de vérifier les conséquences de la suppression d’un index, sans toutefois devoir le détruire et le reconstruire, deux opérations qui peuvent s’avérer particulièrement coûteuses sur des collections comportant de nombreuses données.

Supposons que nous souhaitions temporairement désactiver l’index posé sur le champ nom de notre collection bazar, nommé idx_nom ; il nous faudra écrire :

db.bazar.hideIndex("idx_nom"); 

Pour valider cette opération, exécutons la commande getIndexes sur notre collection :

[  
  { v: 2, key: { _id: 1 }, name: '_id_' },  
  { v: 2, key: { nom: 1 }, name: 'idx_nom', hidden: true }  
] 

Nous constatons effectivement la présence du booléen hidden dans le document résultant de cette commande, ce qui signifie que notre index est désormais masqué. Un explain portant sur une recherche par nom nous confirmera que MongoDB doit maintenant opérer...