Back

Modèles de pagination dans MongoDB

Modèles de pagination dans MongoDB

Vous développez un fil d’actualité, une liste de produits ou une page de résultats de recherche. La collection a atteint plusieurs millions de documents, et soudain vos requêtes de pagination prennent des secondes au lieu de millisecondes. Le coupable est presque toujours le même : vous utilisez skip et limit à grande échelle.

Cet article couvre les principaux modèles de pagination MongoDB — par décalage (offset) et par curseur/keyset — en expliquant quand chacun fonctionne bien et quand il ne fonctionne pas. Pas de conseils spécifiques à un framework, juste des modèles qui resteront valables à mesure que vos données augmentent.

Points clés à retenir

  • La pagination par décalage avec skip() et limit() est intuitive mais se dégrade linéairement à mesure que les valeurs de décalage augmentent — réservez-la à la pagination superficielle ou aux petits ensembles de données.
  • La pagination par keyset (basée sur curseur) utilise des requêtes de plage pour des performances constantes quelle que soit la position, ce qui la rend idéale pour les grands ensembles de données.
  • Utilisez toujours un champ de départage unique (généralement _id) avec la pagination par keyset pour éviter les doublons ou les documents manqués.
  • Choisissez votre modèle de pagination en fonction de la taille des données, des modèles d’accès et de la nécessité pour les utilisateurs d’une navigation séquentielle ou de sauts de page arbitraires.

Pagination MongoDB Skip Limit : l’approche familière

La pagination par décalage utilise skip() et limit() pour diviser les résultats en pages. Elle est intuitive et correspond directement aux modèles d’interface « page 1, page 2, page 3 ».

db.posts.find()
  .sort({ createdAt: -1 })
  .skip((page - 1) * pageSize)
  .limit(pageSize)

Ce modèle fonctionne bien pour la pagination superficielle — les premières pages de résultats, les tableaux de bord d’administration avec des données limitées, ou les outils internes où la performance n’est pas critique.

Pourquoi Skip se dégrade à grande échelle

Les index MongoDB sont des structures en arbre B, pas des tableaux. Bien que la base de données puisse effectuer une recherche efficace dans un index, elle doit toujours parcourir les entrées ignorées pour atteindre le décalage demandé. Ignorer 10 documents signifie parcourir 10 entrées d’index. Ignorer 100 000 signifie parcourir 100 000 entrées.

Cela signifie que la page 1 est rapide, la page 100 est plus lente, et la page 10 000 est encore beaucoup plus lente. L’utilisation du CPU augmente linéairement avec la valeur du décalage, quelle que soit la taille de votre page.

Utilisez la pagination par décalage lorsque :

  • Les utilisateurs naviguent rarement au-delà des premières pages
  • L’ensemble de données total est petit (moins de 10 000 documents)
  • Vous avez besoin de la fonctionnalité « aller à la page X » et acceptez le compromis
  • Vous développez des outils internes où le temps de requête est moins critique

Pagination MongoDB par curseur : performances constantes

La pagination par keyset MongoDB (également appelée pagination par curseur) utilise des requêtes de plage au lieu de décalages positionnels. Plutôt que de dire « ignorer 1 000 documents », vous dites « donnez-moi les documents après ce point spécifique ».

db.posts.find({ createdAt: { $lt: lastSeenDate } })
  .sort({ createdAt: -1 })
  .limit(pageSize)

La base de données effectue une recherche d’index efficace pour localiser le point de départ, puis lit uniquement les documents dont vous avez besoin. La page 1 et la page 10 000 ont des caractéristiques de performance identiques.

L’exigence du champ de départage

Un seul champ de tri crée des problèmes lorsque les valeurs ne sont pas uniques. Si plusieurs documents partagent le même horodatage createdAt, certains peuvent être ignorés ou dupliqués d’une page à l’autre.

La solution est un tri composé avec un champ de départage unique — généralement _id :

db.posts.find({
  $or: [
    { createdAt: { $lt: lastDate } },
    { createdAt: lastDate, _id: { $lt: lastId } }
  ]
})
.sort({ createdAt: -1, _id: -1 })
.limit(pageSize)

Cela nécessite un index composé correspondant :

db.posts.createIndex({ createdAt: -1, _id: -1 })

L’ordre des champs de l’index doit correspondre à votre ordre de tri pour des performances optimales.

Avertissements sur la cohérence

La pagination par curseur est plus stable que la pagination par décalage, mais elle n’est pas magiquement cohérente. Si des documents sont insérés, supprimés, ou si des champs de tri mutables changent entre les requêtes, les utilisateurs peuvent toujours voir des doublons ou manquer des éléments. La différence est que la pagination par keyset s’ancre à des valeurs spécifiques plutôt qu’à des positions, donc les écritures concurrentes causent généralement moins d’anomalies visibles.

Pour les fils d’actualité et les listes où une cohérence parfaite n’est pas requise, ce compromis est généralement acceptable.

Pagination Atlas Search : un mécanisme différent

Si vous utilisez MongoDB Atlas Search avec l’étape $search, la pagination fonctionne différemment. Atlas Search utilise son propre système basé sur des jetons — via searchSequenceToken en coulisses — avec les paramètres searchAfter et searchBefore plutôt que des curseurs _id. Ne mélangez pas ces approches — utilisez la méthode de pagination qui correspond à votre type de requête.

Choisir le bon modèle

ScénarioModèle recommandé
Fils d’actualité à défilement infiniPagination par keyset
Boutons « Charger plus »Pagination par keyset
Tables d’administration avec numéros de pageDécalage (pages superficielles uniquement)
Grands ensembles de données avec navigation séquentiellePagination par keyset
Petits ensembles de données statiquesLes deux fonctionnent

La décision se résume souvent aux exigences de l’interface utilisateur. Si les utilisateurs doivent accéder à des pages arbitraires, vous êtes contraint à la pagination par décalage ou à des approches hybrides. Si la navigation séquentielle suffit — suivant, précédent, charger plus — la pagination par keyset évolue mieux.

Conclusion

La pagination par décalage avec skip et limit est simple mais se dégrade linéairement avec la taille du décalage. Réservez-la à la pagination superficielle ou aux petits ensembles de données.

La pagination par keyset maintient des performances constantes quelle que soit la position, mais nécessite un tri déterministe avec un champ de départage unique. C’est le meilleur choix pour les fils d’actualité, les listes et toute interface où les utilisateurs naviguent séquentiellement à travers de grands ensembles de résultats.

Aucun modèle n’est universellement mauvais. Choisissez en fonction de la taille de vos données, des modèles d’accès et des exigences de votre interface utilisateur.

FAQ

Oui, vous pouvez utiliser plusieurs champs de tri avec la pagination par keyset. L'exigence clé est que votre dernier champ doit être unique pour servir de départage. Votre index composé doit correspondre à l'ordre exact et à la direction de tous les champs de tri. La logique de requête devient plus complexe avec chaque champ supplémentaire, car vous avez besoin de conditions OR imbriquées pour gérer les cas d'égalité pour chaque champ précédent.

Pour la pagination arrière, une approche courante consiste à inverser vos opérateurs de comparaison et la direction de tri. Si l'avant utilise $lt avec un tri décroissant, l'arrière utilise $gt avec un tri croissant. Récupérez les résultats, puis inversez-les dans votre application pour maintenir l'ordre d'affichage. Stockez les curseurs du premier et du dernier document de chaque page pour permettre une navigation bidirectionnelle.

Si le champ de tri d'un document change pendant qu'un utilisateur pagine, il peut voir ce document deux fois ou le manquer entièrement selon qu'il s'est déplacé vers l'avant ou vers l'arrière par rapport à sa position de curseur. Pour les champs fréquemment mis à jour, envisagez d'utiliser des valeurs immuables comme _id comme clé de tri principale, ou acceptez cela comme une limitation inhérente aux données en temps réel.

La pagination par keyset ne prend pas naturellement en charge les comptages totaux car elle ne suit pas la position. Vous pouvez exécuter une requête de comptage séparée, mais cela ajoute une surcharge sur les grandes collections. Demandez-vous si les utilisateurs ont vraiment besoin de comptages exacts — souvent, des comptages approximatifs ou simplement indiquer qu'il existe plus de résultats fournit un contexte suffisant sans le coût de performance.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay