Maîtrise de l'indexation MongoDB pour des performances de requêtes optimales
Optimisez les performances des requêtes MongoDB en maîtrisant les techniques d'indexation. Ce guide complet couvre les index à champ unique, composés et multicolés, explique la puissance des requêtes couvrantes et vous guide dans l'utilisation de `explain()` pour l'analyse des performances. Apprenez les meilleures pratiques pour accélérer vos opérations de lecture, réduire la charge de la base de données et créer une application plus réactive. Lecture essentielle pour tout développeur MongoDB axé sur l'optimisation des performances.
Maîtrise de l'indexation MongoDB pour des performances de requêtes optimales
L'indexation MongoDB devient intéressante lorsque la base de données n'est plus assez petite pour les suppositions chanceuses. Une requête qui semblait instantanée en développement peut devenir douloureuse en production une fois qu'une collection contient des millions de documents, qu'un tableau de bord ajoute un tri, ou qu'un point de terminaison API commence à filtrer par plusieurs champs à la fois.
L'objectif n'est pas d'indexer chaque champ. Cela ralentit généralement les écritures, consomme de la mémoire et du disque, et laisse encore des requêtes importantes non couvertes. L'objectif est de comprendre les quelques formes de requêtes dont votre application dépend réellement, puis de construire des index qui correspondent à ces formes.
Comprendre les index MongoDB
À la base, un index est comme un index dans un livre. Au lieu de lire tout le livre pour trouver un sujet, vous consultez une référence triée et sautez près de la bonne page. Les index MongoDB aident le planificateur de requêtes à localiser les documents correspondants sans analyser toute la collection. Sans un index utile, MongoDB peut effectuer une analyse de collection, examinant les documents un par un jusqu'à trouver les correspondances.
Les analyses de collection ne sont pas toujours mauvaises. Analyser une petite collection peut être acceptable. Exécuter un rapport d'administration une fois par mois peut être acceptable. Mais une analyse de collection dans un chemin de requête à fort trafic est différente. Elle entre en compétition avec les lectures et écritures normales, s'aggrave à mesure que les données augmentent, et se manifeste souvent par une latence imprévisible.
Comment fonctionnent les index
MongoDB utilise généralement des index de type B-tree pour les index de champs normaux. Le détail pratique important est que les valeurs indexées sont stockées dans l'ordre. Cet ordre aide MongoDB avec les filtres d'égalité, les filtres de plage et les tris lorsque la forme de la requête s'aligne avec l'index.
Par exemple, un index sur { email: 1 } est parfait pour :
db.users.findOne({ email: "[email protected]" })
Il n'est pas utile pour :
db.users.find({ lastLoginAt: { $lt: ISODate("2025-01-01") } })
Cette deuxième requête a besoin d'un index qui commence par lastLoginAt, ou elle doit analyser.
Quand utiliser les index
Les index sont les plus bénéfiques pour les champs fréquemment utilisés dans :
- Critères de requête (
find(),findOne()) : Champs utilisés dans le documentfilterde vos requêtes. - Critères de tri (
sort()) : Champs utilisés pour ordonner les résultats de vos requêtes. - Champ
_id: Par défaut, MongoDB crée un index sur le champ_id, garantissant l'unicité et des recherches rapides par ID.
Cependant, les index ont aussi un coût :
- Espace de stockage : Les index consomment de l'espace disque.
- Performances d'écriture : Les index doivent être mis à jour chaque fois que des documents sont insérés, mis à jour ou supprimés, ce qui peut ralentir les opérations d'écriture.
- Pression mémoire : Les pages d'index fréquemment utilisées entrent en compétition pour le cache. Trop d'index volumineux peuvent rendre plus difficile le maintien de l'ensemble de travail en mémoire.
Par conséquent, il est crucial de créer des index de manière stratégique, en se concentrant sur les champs qui apporteront les gains de performance les plus significatifs pour vos opérations de lecture courantes.
Création et gestion des index
MongoDB fournit la méthode createIndex() pour créer des index et getIndexes() pour voir les index existants. La méthode dropIndex() est utilisée pour les supprimer.
Création d'index de base
Pour créer un index à champ unique, vous spécifiez le nom du champ et le type d'index (généralement 1 pour l'ordre croissant ou -1 pour l'ordre décroissant).
db.collection.createIndex( { fieldName: 1 } );
Exemple : Indexation d'un champ username en ordre croissant :
db.users.createIndex( { username: 1 } );
Affichage des index
Pour voir les index sur une collection :
db.collection.getIndexes();
Exemple : Affichage des index sur la collection users :
db.users.getIndexes();
Cela retournera un tableau de définitions d'index, y compris l'index _id par défaut.
Sur une collection très sollicitée, créez les index délibérément. Les versions modernes de MongoDB prennent en charge les constructions d'index en ligne dans de nombreux cas courants, mais les constructions d'index consomment toujours du CPU, des E/S disque et de la mémoire. Sur les systèmes de production, planifiez les constructions d'index volumineuses pendant les périodes plus calmes et surveillez le retard de réplication si vous utilisez un jeu de réplicas.
Suppression des index
Pour supprimer un index :
db.collection.dropIndex( "indexName" );
Vous pouvez trouver le indexName à partir de la sortie de getIndexes(). Alternativement, vous pouvez supprimer un index en spécifiant le(s) champ(s) indexé(s) dans le même format que createIndex() :
db.collection.dropIndex( { fieldName: 1 } );
Exemple : Suppression de l'index username :
db.users.dropIndex( "username_1" ); // En utilisant le nom de l'index
// OU
db.users.dropIndex( { username: 1 } ); // En utilisant la définition de l'index
Avant de supprimer un index, vérifiez si quelque chose l'utilise encore :
db.users.aggregate([{ $indexStats: {} }])
Cela montre les compteurs d'accès depuis le démarrage du serveur. Un compteur à zéro est un indice, pas une preuve absolue. Le serveur a peut-être redémarré récemment, ou la requête peut s'exécuter uniquement pendant un travail hebdomadaire. Pour les systèmes importants, combinez $indexStats, la recherche dans le code de l'application, les journaux de requêtes et une courte période d'observation.
Index composés
Les index composés impliquent plusieurs champs. L'ordre des champs dans un index composé est critique. MongoDB utilise les index composés pour les requêtes qui impliquent plusieurs champs dans les clauses filter ou sort.
Quand utiliser les index composés
Les index composés sont les plus efficaces lorsque vos requêtes filtrent ou trient fréquemment par une combinaison de champs. L'index peut satisfaire les requêtes qui correspondent aux champs dans le même ordre qu'ils sont définis dans l'index ou un préfixe de l'index.
Exemple : Considérez une collection de orders avec des champs comme userId, orderDate et status. Si vous interrogez fréquemment les commandes par un utilisateur spécifique et les triez par date, un index composé sur { userId: 1, orderDate: 1 } serait très bénéfique.
db.orders.createIndex( { userId: 1, orderDate: 1 } );
Cet index peut prendre en charge efficacement des requêtes comme :
db.orders.find( { userId: "user123" } ).sort( { orderDate: -1 } )db.orders.find( { userId: "user123", orderDate: { $lt: ISODate() } } )
Cependant, il pourrait ne pas être aussi efficace pour les requêtes qui filtrent uniquement par orderDate si userId n'est pas également spécifié, ou si les champs sont dans un ordre différent.
L'ordre des champs est important
L'ordre des champs dans un index composé détermine les modèles de requêtes qu'il peut bien prendre en charge. Une règle empirique utile est de placer les champs d'égalité en premier, puis les champs de tri, puis les champs de plage. C'est souvent appelé la directive ESR : égalité, tri, plage. C'est une directive, pas une loi, mais elle évite de nombreuses conceptions d'index médiocres.
Supposons que votre page de commandes exécute cette requête :
db.orders.find({
tenantId: "t1",
status: "paid",
createdAt: { $gte: ISODate("2025-01-01") }
}).sort({ createdAt: -1 })
Un index raisonnable pourrait être :
db.orders.createIndex({ tenantId: 1, status: 1, createdAt: -1 })
tenantId et status sont des filtres d'égalité. createdAt prend en charge le tri et la plage. Si vous créez plutôt { createdAt: -1, status: 1, tenantId: 1 }, MongoDB peut encore l'utiliser dans certains cas, mais il est généralement moins aligné avec cette requête.
Pour les requêtes qui trient les résultats, l'ordre des champs dans l'index doit correspondre à l'ordre des champs dans l'opération sort() pour des performances optimales. Si une requête inclut à la fois un filtre et un tri, et que l'index correspond aux champs du filtre, il peut également être utilisé pour le tri sans une analyse de collection séparée pour le tri.
Les index composés peuvent également servir les requêtes de préfixe. Un index sur { tenantId: 1, status: 1, createdAt: -1 } peut aider une requête sur tenantId seul, ou tenantId plus status. Il ne peut généralement pas beaucoup aider avec une requête sur status seul car status n'est pas le champ principal.
Requêtes couvrantes
Une requête couvrante est une requête où MongoDB peut satisfaire l'intégralité de la requête en utilisant uniquement l'index. Cela signifie que l'index contient tous les champs qui sont interrogés et projetés. Les requêtes couvrantes évitent de récupérer les documents de la collection elle-même, ce qui les rend extrêmement rapides.
Comment réaliser des requêtes couvrantes
Pour réaliser une requête couvrante, assurez-vous que :
- Vous avez un index qui inclut tous les champs utilisés dans le filtre de la requête.
- Vous incluez uniquement ces champs indexés (ou un sous-ensemble d'entre eux) dans votre projection.
Exemple : Considérez une collection employees avec les champs name, age et city. Si vous avez un index { city: 1, age: 1 } et que vous souhaitez récupérer les noms et âges des employés dans une ville spécifique, vous pouvez créer une requête couvrante :
db.employees.find( { city: "New York" }, { name: 1, age: 1, _id: 0 } ).explain()
Dans cette requête, city est dans l'index, et name et age sont inclus dans la projection. Si l'index contenait également name et age, ce serait une requête couvrante.
Affinons l'index et la requête pour une véritable requête couvrante :
// Créez un index qui inclut tous les champs nécessaires pour la requête et la projection
db.employees.createIndex( { city: 1, age: 1, name: 1 } );
// Maintenant, une requête qui filtre par ville et projette le nom et l'âge peut être couverte
db.employees.find( { city: "New York" }, { name: 1, age: 1, _id: 0 } )
Lorsque vous exécutez explain("executionStats") sur cette requête, un plan couvert devrait examiner les clés d'index sans récupérer les documents complets de la collection. Dans de nombreux plans d'explication, cela signifie que vous verrez une IXSCAN sans étape FETCH, et totalDocsExamined devrait être 0. La sortie d'explain varie selon la version de MongoDB et la forme de la requête, alors concentrez-vous sur les étapes réelles du plan et les comptes examinés plutôt que de chercher une étiquette exacte.
Les requêtes couvrantes sont utiles pour les chemins de lecture à chaud tels que la saisie semi-automatique, les petites vues de liste ou les vérifications d'autorisations. Elles sont moins utiles si la projection inclut des champs volumineux, de nombreux champs ou des champs qui changent constamment. Ajouter trop de champs à un index juste pour couvrir une requête peut créer un index volumineux qui nuit aux performances d'écriture.
Autres types d'index importants
MongoDB propose divers types d'index pour des cas d'utilisation spécifiques :
Index multicolés
Les index multicolés sont automatiquement créés lorsque vous indexez un champ de tableau. Ils vous permettent d'interroger des éléments dans des tableaux.
Exemple : Si vous avez une collection products avec un champ de tableau tags ["electronics", "gadgets"] :
db.products.createIndex( { tags: 1 } );
Cet index prendra en charge des requêtes comme db.products.find( { tags: "electronics" } ).
Les tableaux nécessitent une attention particulière dans les index composés. Un index multicolé stocke des entrées pour les éléments du tableau, ce qui peut rapidement augmenter la taille de l'index. MongoDB a également des restrictions concernant les index composés multicolés lorsque plus d'un champ indexé peut contenir des tableaux dans le même document. Si votre modèle de données a plusieurs tableaux et des filtres complexes, testez la requête exacte avec des données représentatives avant de supposer qu'un index composé se comportera comme un index à champ scalaire.
Index de texte
Les index de texte prennent en charge la recherche efficace de contenu textuel dans les documents. Ils sont utilisés pour les requêtes de recherche de texte à l'aide de l'opérateur $text.
db.articles.createIndex( { content: "text" } );
Cela permet des recherches comme : db.articles.find( { $text: { $search: "performance base de données" } } ).
Les index de texte sont utiles pour la recherche de texte de base, mais ils ne constituent pas une plateforme de recherche complète. Si vous avez besoin d'un réglage avancé de la pertinence, d'une tolérance aux fautes de frappe, de facettes, de surlignage ou d'un comportement de recherche spécifique à une langue, MongoDB Atlas Search ou un moteur de recherche dédié peut être un meilleur choix.
Index géospatiaux
Les index géospatiaux sont utilisés pour interroger efficacement des données géographiques à l'aide des opérateurs $near, $geoWithin et $geoIntersects.
db.locations.createIndex( { loc: "2dsphere" } ); // Pour un index 2dsphere
Index uniques
Les index uniques imposent l'unicité pour un champ ou une combinaison de champs. Si une valeur en double est insérée ou mise à jour, MongoDB retournera une erreur.
db.users.createIndex( { email: 1 }, { unique: true } );
Pour les tables utilisateur en production, normalisez avant d'imposer l'unicité. Les adresses e-mail sont un exemple courant. Si votre application traite [email protected] et [email protected] comme le même utilisateur, stockez un champ normalisé tel que emailLower et placez l'index unique là-dessus. Ne vous fiez pas uniquement au code de l'application pour empêcher les doublons en concurrence.
Index partiels
Les index partiels n'indexent que les documents qui correspondent à une expression de filtre. Ils sont utiles lorsqu'une requête se concentre sur un sous-ensemble d'une collection.
db.orders.createIndex(
{ tenantId: 1, createdAt: -1 },
{ partialFilterExpression: { status: "open" } }
)
Cela peut aider si votre application lit fréquemment les commandes ouvertes et que les commandes fermées constituent la majeure partie de la collection. L'index est plus petit car il exclut les documents qui ne correspondent pas au filtre partiel. La requête doit inclure une condition compatible pour que MongoDB l'utilise.
Index TTL
Les index TTL suppriment automatiquement les documents après un temps configuré. Ils sont couramment utilisés pour les sessions, les jetons temporaires ou les événements de courte durée.
db.sessions.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 }
)
La suppression TTL n'est pas instantanée au moment exact de l'expiration. MongoDB supprime les documents expirés en arrière-plan. Utilisez-le pour le nettoyage, pas pour un timing de sécurité précis où un jeton doit devenir invalide immédiatement. Votre application doit toujours vérifier l'expiration lors des lectures.
Analyse des performances avec explain()
Comprendre comment MongoDB exécute vos requêtes est crucial pour les optimiser. La méthode explain() fournit des informations sur le plan d'exécution de la requête, y compris si un index a été utilisé et comment.
db.collection.find( {...} ).explain( "executionStats" );
Champs clés à rechercher dans la sortie de explain() :
winningPlan.stage: Indique l'étape du plan d'exécution (par exemple,COLLSCANpour une analyse de collection,IXSCANpour une analyse d'index).executionStats.totalKeysExamined: Le nombre de clés d'index examinées.executionStats.totalDocsExamined: Le nombre de documents examinés.
Un bon plan d'exécution aura totalDocsExamined proche ou égal au nombre de documents retournés, et totalKeysExamined significativement inférieur au nombre total de documents dans la collection. Si totalDocsExamined est très élevé, ou si COLLSCAN est utilisé, cela suggère qu'un index est manquant ou n'est pas utilisé efficacement.
Voici la façon rapide dont je lis un plan d'explication :
- Recherchez
COLLSCAN. Si c'est un chemin à chaud et que la collection est grande, c'est généralement le premier problème. - Recherchez
IXSCANsuivi deFETCH. Une récupération est normale lorsque la requête a besoin de champs en dehors de l'index, mais un examen excessif de documents signifie que l'index n'est pas assez sélectif. - Comparez
nReturned,totalKeysExaminedettotalDocsExamined. Retourner 20 documents après avoir examiné 25 clés est sain. Retourner 20 documents après avoir examiné 500 000 clés ne l'est pas. - Surveillez les tris en mémoire. Si MongoDB doit trier un grand ensemble de résultats après le filtrage, un index composé qui prend en charge le tri peut aider.
Utilisez des filtres réalistes lors des tests. Un plan d'explication pour tenantId: "demo" peut ne pas correspondre à un grand locataire avec des millions de documents. La distribution des données est importante.
Un guide pratique de conception d'index
Imaginez une application avec une collection tickets. Les agents de support utilisent une page de file d'attente avec ces filtres :
db.tickets.find({
tenantId: "acme",
status: "open",
assigneeId: "u123"
}).sort({ updatedAt: -1 }).limit(50)
Commencez par la forme de la requête, pas par la liste des champs. La collection est multi-locataire, les agents filtrent généralement par statut et assigné, et l'interface utilisateur trie par mises à jour les plus récentes en premier. Un index pratique est :
db.tickets.createIndex({
tenantId: 1,
status: 1,
assigneeId: 1,
updatedAt: -1
})
Considérons maintenant une autre page : les gestionnaires voient tous les tickets ouverts, quel que soit l'assigné :
db.tickets.find({
tenantId: "acme",
status: "open"
}).sort({ updatedAt: -1 }).limit(100)
L'index précédent peut utiliser le préfixe { tenantId, status }, mais assigneeId se trouve avant updatedAt, donc il peut ne pas prendre en charge le tri aussi bien pour cette requête de gestionnaire. Vous pourriez avoir besoin d'un deuxième index :
db.tickets.createIndex({
tenantId: 1,
status: 1,
updatedAt: -1
})
C'est un compromis normal. Un seul index ne sert rarement parfaitement chaque écran. Le travail consiste à prendre en charge les chemins importants sans créer un tas d'index qui se chevauchent et qui coûtent tous des écritures.
Meilleures pratiques pour l'indexation MongoDB
- Indexez uniquement ce dont vous avez besoin : Évitez de créer des index sur des champs rarement interrogés ou triés. Chaque index ajoute une surcharge.
- Utilisez les index composés judicieusement : Ordonnez correctement les champs en fonction des modèles de requêtes. Considérez les champs les plus sélectifs en premier.
- Visez les requêtes couvrantes : Si les performances de lecture sont critiques, concevez des index pour couvrir les opérations de lecture courantes.
- Surveillez l'utilisation des index : Examinez régulièrement l'utilisation des index à l'aide de
explain()etdb.collection.aggregate([{ $indexStats: {} }])pour identifier les index inutilisés ou inefficaces. - Considérez la sélectivité des index : Les index sur des champs à faible cardinalité (peu de valeurs distinctes) peuvent ne pas être aussi efficaces que ceux sur des champs à haute cardinalité.
- Gardez les index petits : Évitez d'inclure des champs volumineux ou des tableaux dans les index, sauf si cela est absolument nécessaire pour les requêtes couvrantes.
- Testez vos index : Testez toujours l'impact des nouveaux index sur les performances de lecture et d'écriture dans des conditions de charge réalistes.
- Supprimez les index redondants avec précaution : Si vous avez
{ a: 1, b: 1 }, un index séparé{ a: 1 }peut être redondant pour de nombreuses charges de travail. Confirmez l'utilisation avant de supprimer. - Concevez en fonction des écrans et des tâches réelles : Les index doivent correspondre au comportement de l'application : recherche de connexion, page de file d'attente, filtre de rapport, analyse de travailleur en arrière-plan.
- Revisitez après les changements de schéma : Un nouveau champ, un nouvel ordre de tri ou un nouveau modèle de locataire peut rendre un ancien index moins utile.
Ce que ressent une bonne indexation
Une bonne indexation MongoDB est généralement silencieuse. Les requêtes importantes examinent approximativement la quantité de données qu'elles retournent. Les tris ne débordent pas dans un travail coûteux. Les écritures ne sont pas alourdies par une douzaine d'index spéculatifs. Lorsqu'une nouvelle fonctionnalité ajoute une nouvelle forme de requête, vous la testez avec explain("executionStats") avant qu'elle ne devienne un incident de production.
L'habitude pratique est simple : collectez la requête réelle, concevez le plus petit index utile pour cette forme de requête, testez avec des données représentatives et continuez à vérifier l'utilisation de l'index au fil du temps. Cette habitude fera plus pour les performances de MongoDB que de mémoriser chaque type d'index.