Back

Paginierungsmuster in MongoDB

Paginierungsmuster in MongoDB

Sie entwickeln einen Feed, eine Produktliste oder eine Seite mit Suchergebnissen. Die Collection ist auf Millionen von Dokumenten angewachsen, und plötzlich dauern Ihre Paginierungsabfragen Sekunden statt Millisekunden. Der Übeltäter ist fast immer derselbe: Sie verwenden skip und limit im großen Maßstab.

Dieser Artikel behandelt die grundlegenden MongoDB-Paginierungsmuster – offset-basiert und cursor-/keyset-basiert – und erklärt, wann jedes Muster gut funktioniert und wann nicht. Keine Framework-spezifischen Ratschläge, nur Muster, die gültig bleiben, während Ihre Daten wachsen.

Wichtigste Erkenntnisse

  • Offset-Paginierung mit skip() und limit() ist intuitiv, verschlechtert sich aber linear mit steigenden Offset-Werten – reservieren Sie sie für flache Paginierung oder kleine Datensätze.
  • Keyset-Paginierung (cursor-basiert) verwendet Bereichsabfragen für konsistente Performance unabhängig von der Position und ist daher ideal für große Datensätze.
  • Verwenden Sie bei Keyset-Paginierung immer ein eindeutiges Tiebreaker-Feld (typischerweise _id), um Duplikate oder übersprungene Dokumente zu verhindern.
  • Wählen Sie Ihr Paginierungsmuster basierend auf Datengröße, Zugriffsmustern und ob Benutzer sequenzielle Navigation oder beliebige Seitensprünge benötigen.

MongoDB Skip-Limit-Paginierung: Der vertraute Ansatz

Offset-basierte Paginierung verwendet skip() und limit(), um Ergebnisse in Seiten aufzuteilen. Sie ist intuitiv und entspricht direkt UI-Mustern wie „Seite 1, Seite 2, Seite 3”.

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

Dieses Muster funktioniert gut für flache Paginierung – die ersten paar Ergebnisseiten, Admin-Dashboards mit begrenzten Daten oder interne Tools, bei denen Performance nicht kritisch ist.

Warum Skip im großen Maßstab degradiert

MongoDB-Indizes sind B-Tree-Strukturen, keine Arrays. Während die Datenbank effizient in einen Index springen kann, muss sie dennoch durch übersprungene Einträge navigieren, um den angeforderten Offset zu erreichen. Das Überspringen von 10 Dokumenten bedeutet, 10 Index-Einträge zu durchlaufen. Das Überspringen von 100.000 bedeutet, 100.000 Einträge zu durchlaufen.

Das bedeutet: Seite 1 ist schnell, Seite 100 ist langsamer, und Seite 10.000 ist noch deutlich langsamer. Die CPU-Auslastung steigt linear mit dem Offset-Wert, unabhängig von Ihrer Seitengröße.

Verwenden Sie Offset-Paginierung, wenn:

  • Benutzer selten über die ersten paar Seiten hinaus navigieren
  • Der gesamte Datensatz klein ist (unter 10.000 Dokumenten)
  • Sie „Springe zu Seite X”-Funktionalität benötigen und den Kompromiss akzeptieren
  • Sie interne Tools entwickeln, bei denen die Abfragezeit weniger kritisch ist

MongoDB Cursor-basierte Paginierung: Konsistente Performance

MongoDB Keyset-Paginierung (auch cursor-basierte Paginierung genannt) verwendet Bereichsabfragen anstelle von positionsbasierten Offsets. Anstatt zu sagen „überspringe 1.000 Dokumente”, sagen Sie „gib mir Dokumente nach diesem spezifischen Punkt”.

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

Die Datenbank führt ein effizientes Index-Seek aus, um den Startpunkt zu lokalisieren, und liest dann nur die Dokumente, die Sie benötigen. Seite 1 und Seite 10.000 haben identische Performance-Charakteristiken.

Die Tiebreaker-Anforderung

Ein einzelnes Sortierfeld erzeugt Probleme, wenn Werte nicht eindeutig sind. Wenn mehrere Dokumente denselben createdAt-Zeitstempel haben, können einige über Seiten hinweg übersprungen oder dupliziert werden.

Die Lösung ist eine zusammengesetzte Sortierung mit einem eindeutigen Tiebreaker – typischerweise _id:

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

Dies erfordert einen passenden zusammengesetzten Index:

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

Die Reihenfolge der Index-Felder muss Ihrer Sortierreihenfolge für optimale Performance entsprechen.

Konsistenz-Einschränkungen

Cursor-basierte Paginierung ist stabiler als Offset-Paginierung, aber nicht magisch konsistent. Wenn Dokumente zwischen Anfragen eingefügt, gelöscht werden oder sich veränderliche Sortierfelder ändern, können Benutzer dennoch Duplikate sehen oder Elemente verpassen. Der Unterschied besteht darin, dass Keyset-Paginierung an spezifischen Werten statt an Positionen verankert ist, sodass gleichzeitige Schreibvorgänge typischerweise weniger sichtbare Anomalien verursachen.

Für Feeds und Listen, bei denen perfekte Konsistenz nicht erforderlich ist, ist dieser Kompromiss normalerweise akzeptabel.

Atlas Search Paginierung: Ein anderer Mechanismus

Wenn Sie MongoDB Atlas Search mit der $search-Stage verwenden, funktioniert die Paginierung anders. Atlas Search verwendet ein eigenes token-basiertes System – über searchSequenceToken im Hintergrund – mit searchAfter- und searchBefore-Parametern anstelle von _id-Cursorn. Vermischen Sie diese Ansätze nicht – verwenden Sie die Paginierungsmethode, die zu Ihrem Abfragetyp passt.

Das richtige Muster wählen

SzenarioEmpfohlenes Muster
Infinite-Scroll-FeedsKeyset-Paginierung
„Mehr laden”-ButtonsKeyset-Paginierung
Admin-Tabellen mit SeitenzahlenOffset (nur flache Seiten)
Große Datensätze mit sequenzieller NavigationKeyset-Paginierung
Kleine, statische DatensätzeBeides funktioniert

Die Entscheidung hängt oft von UI-Anforderungen ab. Wenn Benutzer zu beliebigen Seiten springen müssen, sind Sie auf Offset-Paginierung oder Hybrid-Ansätze beschränkt. Wenn sequenzielle Navigation ausreicht – weiter, zurück, mehr laden – skaliert Keyset-Paginierung besser.

Fazit

Offset-Paginierung mit skip und limit ist einfach, verschlechtert sich aber linear mit der Offset-Größe. Reservieren Sie sie für flache Paginierung oder kleine Datensätze.

Keyset-Paginierung behält konsistente Performance unabhängig von der Position bei, erfordert aber deterministische Sortierung mit einem eindeutigen Tiebreaker. Sie ist die bessere Wahl für Feeds, Listen und jede Oberfläche, bei der Benutzer sequenziell durch große Ergebnismengen navigieren.

Keines der Muster ist universell falsch. Wählen Sie basierend auf Ihrer Datengröße, Ihren Zugriffsmustern und UI-Anforderungen.

Häufig gestellte Fragen

Ja, Sie können mehrere Sortierfelder mit Keyset-Paginierung verwenden. Die Hauptanforderung ist, dass Ihr letztes Feld eindeutig sein muss, um als Tiebreaker zu dienen. Ihr zusammengesetzter Index muss der exakten Reihenfolge und Richtung aller Sortierfelder entsprechen. Die Abfragelogik wird mit jedem zusätzlichen Feld komplexer, da Sie verschachtelte OR-Bedingungen benötigen, um Gleichheitsfälle für jedes vorhergehende Feld zu behandeln.

Für Rückwärts-Paginierung ist ein gängiger Ansatz, Ihre Vergleichsoperatoren und Sortierrichtung umzukehren. Wenn vorwärts $lt mit absteigender Sortierung verwendet, nutzt rückwärts $gt mit aufsteigender Sortierung. Rufen Sie die Ergebnisse ab und kehren Sie sie dann in Ihrer Anwendung um, um die Anzeigereihenfolge beizubehalten. Speichern Sie sowohl den ersten als auch den letzten Dokument-Cursor von jeder Seite, um bidirektionale Navigation zu ermöglichen.

Wenn sich das Sortierfeld eines Dokuments ändert, während ein Benutzer paginiert, kann er dieses Dokument zweimal sehen oder ganz verpassen, je nachdem, ob es sich relativ zu seiner Cursor-Position vorwärts oder rückwärts bewegt hat. Für häufig aktualisierte Felder sollten Sie erwägen, unveränderliche Werte wie _id als Ihren primären Sortierschlüssel zu verwenden, oder dies als inhärente Einschränkung von Echtzeit-Daten zu akzeptieren.

Keyset-Paginierung unterstützt von Natur aus keine Gesamtzahlen, da sie keine Position verfolgt. Sie können eine separate Count-Abfrage ausführen, aber dies fügt Overhead bei großen Collections hinzu. Überlegen Sie, ob Benutzer wirklich exakte Zahlen benötigen – oft bieten ungefähre Zahlen oder einfach die Angabe, dass weitere Ergebnisse existieren, ausreichenden Kontext ohne die Performance-Kosten.

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