MongoDB - Paging

81

Gibt es bei der Verwendung von MongoDB spezielle Muster, um z. B. eine Seitenansicht zu erstellen? Sagen wir ein Blog, das die 10 neuesten Beiträge auflistet, in denen Sie rückwärts zu älteren Beiträgen navigieren können.

Oder löst man es mit einem Index auf zB blogpost.publishdate und überspringt und begrenzt das Ergebnis einfach?

Roger Johansson
quelle
1
Ich werde diesen hier hängen lassen, da es einige Meinungsverschiedenheiten darüber zu geben scheint, wie diese Skala richtig hergestellt werden kann.
Roger Johansson

Antworten:

98

Die Verwendung von skip + limit ist keine gute Möglichkeit, Paging durchzuführen, wenn die Leistung ein Problem darstellt oder wenn große Sammlungen vorhanden sind. Mit zunehmender Seitenzahl wird es immer langsamer. Bei Verwendung von Überspringen muss der Server alle Dokumente (oder Indexwerte) von 0 bis zum Versatzwert (Überspringen) durchlaufen.

Es ist viel besser, eine Bereichsabfrage (+ Limit) zu verwenden, bei der Sie den Bereichswert der letzten Seite übergeben. Wenn Sie beispielsweise nach "Veröffentlichungsdatum" sortieren, übergeben Sie einfach den letzten Wert für "Veröffentlichungsdatum" als Kriterium für die Abfrage, um die nächste Datenseite zu erhalten.

Scott Hernandez
quelle
4
Es wird großartig sein, einige Dokumente zu sehen, die bestätigen, dass das Überspringen in Mongodb alle Dokumente durchläuft.
Andrew Orsich
5
Los geht's: Dokumente überspringen Wenn es einen anderen Ort gibt, an dem die Informationen aktualisiert werden sollten, lassen Sie es mich bitte wissen.
Scott Hernandez
2
@ScottHernandez: Ich habe Paging mit Links zu mehreren Seiten (wie zum Beispiel: Seite: Erste, 2, 3, 4, 5, Letzte) und Sortieren nach allen Feldern. Nur eines meiner Felder ist eindeutig (und indiziert). Funktioniert eine Bereichsabfrage für diesen Anwendungsfall? Ich fürchte nicht, ich wollte nur bestätigen, ob es überhaupt möglich ist. Vielen Dank.
user183037
7
Hier ist der Link
Ulises
8
Dies würde anscheinend nicht funktionieren, wenn mehrere Dokumente mit demselben Veröffentlichungsdatum vorhanden wären.
d512
12
  1. Bereichsbasiertes Paging ist schwer zu implementieren, wenn Sie Elemente auf viele Arten sortieren müssen.
  2. Denken Sie daran, dass das bereichsbasierte Paging nicht mehr möglich ist, wenn der Feldwert des Sortierparameters nicht eindeutig ist.

Mögliche Lösung: Versuchen Sie, das Design zu vereinfachen, und überlegen Sie, ob wir nur nach ID oder einem eindeutigen Wert sortieren können.

Und wenn wir können, kann bereichsbasiertes Pageing verwendet werden.

Die übliche Methode ist die Verwendung von sort (), skip () und limit (), um das oben beschriebene Paging zu implementieren.

Jackalope
quelle
Einen guten Artikel mit Python-Codebeispielen finden Sie hier codementor.io/arpitbhayani/…
Gianfranco P.
1
Danke - tolle Antwort! Ich ärgere mich, wenn Leute Paginierung vorschlagen, indem sie Filter verwenden, zB { _id: { $gt: ... } }... es funktioniert einfach nicht, wenn benutzerdefinierte Bestellungen verwendet werden - z .sort(...).
Nick Grealy
1
@NickGrealy Ich habe ein Tutorial befolgt, um genau dies zu tun, und bin jetzt in einer Situation, in der das Paging so aussieht, als würde es funktionieren, aber ich erhalte fehlende Dokumente, weil ich die Mongo-ID verwende, aber wenn neue Daten in die Datenbank eingefügt werden, und dann die Die Sammlung wird alphabetisch sortiert, wenn die Startseite Datensätze enthält, die mit A beginnen, die IDs jedoch höher sind als die Datensätze, die mit AA beginnen, da sie danach eingefügt wurden. Die AA-Datensätze werden vom Paging nicht zurückgegeben. Ist Überspringen und Begrenzen geeignet? Ich habe in der Region von 60 Millionen Dokumenten zu suchen.
Berimbolo
@berimbolo - das ist ein Gespräch wert - Sie werden Ihre Antwort hier in den Kommentaren nicht bekommen. Frage: Welches Verhalten erwarten Sie? Sie arbeiten mit einem Live-System, bei dem ständig Datensätze erstellt und gelöscht werden. Wenn Sie für jedes Laden einer neuen Seite erneut einen Live-Snapshot Ihrer Daten anfordern, sollten Sie damit rechnen, dass sich Ihre zugrunde liegenden Daten ändern. Wie soll das Verhalten sein? Wenn Sie mit einem "Zeitpunkt" -Daten-Snapshot arbeiten, haben Sie "feste Seiten", aber auch "veraltete" Daten. Wie groß ist das Problem, das Sie beschreiben, und wie oft stoßen Menschen darauf?
Nick Grealy
1
Es ist definitiv ein Gespräch wert. Mein Problem ist, dass ich eine einmalige Datei in alphabetischer Reihenfolge der Nummernschilder abgerufen habe und alle 15 Minuten Aktualisierungen auf geänderte (entfernte oder hinzugefügte) Kennzeichen angewendet habe. Das Problem ist, dass wenn ein neues Kennzeichen hinzugefügt wird und es startet Mit einem A zum Beispiel und aufgrund der Seitengröße ist das letzte auf der Seite. Wenn das nächste angefordert wird, werden meiner Meinung nach keine Datensätze zurückgegeben (eine Annahme und ein erfundenes Beispiel, aber veranschaulichend für mein Problem), da die ID höher ist als alle anderen in der Satz. Ich möchte jetzt das vollständige Nummernschild verwenden, um den größten Teil der Abfrage zu steuern.
Berimbolo
5

Dies ist die Lösung, die ich verwendet habe, als meine Sammlung zu groß wurde, um in einer einzigen Abfrage zurückgegeben zu werden. Es nutzt die inhärente Reihenfolge des _idFelds und ermöglicht es Ihnen, eine Sammlung nach angegebener Stapelgröße zu durchlaufen.

Hier ist es als npm-Modul, Mungo-Paging , vollständiger Code ist unten:

function promiseWhile(condition, action) {
  return new Promise(function(resolve, reject) {
    process.nextTick(function loop() {
      if(!condition()) {
        resolve();
      } else {
        action().then(loop).catch(reject);
      }
    });
  });
}

function findPaged(query, fields, options, iterator, cb) {
  var Model  = this,
    step     = options.step,
    cursor   = null,
    length   = null;

  promiseWhile(function() {
    return ( length===null || length > 0 );
  }, function() {
    return new Promise(function(resolve, reject) {

        if(cursor) query['_id'] = { $gt: cursor };

        Model.find(query, fields, options).sort({_id: 1}).limit(step).exec(function(err, items) {
          if(err) {
            reject(err);
          } else {
            length  = items.length;
            if(length > 0) {
              cursor  = items[length - 1]._id;
              iterator(items, function(err) {
                if(err) {
                  reject(err);
                } else {
                  resolve();
                }
              });
            } else {
              resolve();
            }
          }
        });
      });
  }).then(cb).catch(cb);

}

module.exports = function(schema) {
  schema.statics.findPaged = findPaged;
};

Befestigen Sie es wie folgt an Ihrem Modell:

MySchema.plugin(findPaged);

Dann fragen Sie wie folgt ab:

MyModel.findPaged(
  // mongoose query object, leave blank for all
  {source: 'email'},
  // fields to return, leave blank for all
  ['subject', 'message'],
  // number of results per page
  {step: 100},
  // iterator to call on each set of results
  function(results, cb) {
    console.log(results);
    // this is called repeatedly while until there are no more results.
    // results is an array of maximum length 100 containing the
    // results of your query

    // if all goes well
    cb();

    // if your async stuff has an error
    cb(err);
  },
  // function to call when finished looping
  function(err) {
    throw err;
    // this is called once there are no more results (err is null),
    // or if there is an error (then err is set)
  }
);
mz3
quelle
Ich weiß nicht, warum diese Antwort nicht mehr positive Stimmen hat. Dies ist eine effizientere Art zu paginieren als überspringen / begrenzen
nxmohamad
Ich bin auch von diesem Paket gekommen, aber wie ist es die Leistung im Vergleich zu Überspringen / Limit und die Antwort von @Scott Hernandez?
Tanckom
5
Wie würde diese Antwort funktionieren, um nach einem anderen Feld zu sortieren?
Nick Grealy
1

Bereichsbasiertes Paging ist machbar, aber Sie müssen klug sein, wie Sie die Abfrage minimieren / maximieren.

Wenn Sie es sich leisten können, sollten Sie versuchen, die Ergebnisse einer Abfrage in einer temporären Datei oder Sammlung zwischenzuspeichern. Dank TTL-Sammlungen in MongoDB können Sie Ihre Ergebnisse in zwei Sammlungen einfügen.

  1. Suche + Benutzer + Parameter Abfrage (TTL was auch immer)
  2. Ergebnisse der Abfrage (TTL unabhängig + Reinigungsintervall + 1)

Wenn Sie beide Zusicherungen verwenden, erhalten Sie keine Teilergebnisse, wenn sich die TTL der aktuellen Zeit nähert. Sie können einen einfachen Zähler verwenden, wenn Sie die Ergebnisse speichern, um an diesem Punkt eine SEHR einfache Bereichsabfrage durchzuführen.

whardier
quelle
1

Hier ist ein Beispiel für das Abrufen einer Liste von UserDokumenten, die mithilfe des offiziellen C # -Treibers sortiert wurden CreatedDate(wobei pageIndexnull basiert).

public void List<User> GetUsers() 
{
  var connectionString = "<a connection string>";
  var client = new MongoClient(connectionString);
  var server = client.GetServer();
  var database = server.GetDatabase("<a database name>");

  var sortBy = SortBy<User>.Descending(u => u.CreatedDate);
  var collection = database.GetCollection<User>("Users");
  var cursor = collection.FindAll();
  cursor.SetSortOrder(sortBy);

  cursor.Skip = pageIndex * pageSize;
  cursor.Limit = pageSize;
  return cursor.ToList();
}

Alle Sortier- und Paging-Vorgänge werden auf der Serverseite ausgeführt. Obwohl dies ein Beispiel in C # ist, kann das gleiche auch auf andere Sprachports angewendet werden.

Siehe http://docs.mongodb.org/ecosystem/tutorial/use-csharp-driver/#modifying-a-cursor-before-enumerating-it .

Alex Ho
quelle
0
    // file:ad-hoc.js
    // an example of using the less binary as pager in the bash shell
    //
    // call on the shell by:
    // mongo localhost:27017/mydb ad-hoc.js | less
    //
    // note ad-hoc.js must be in your current directory
    // replace the 27017 wit the port of your mongodb instance
    // replace the mydb with the name of the db you want to query
    //
    // create the connection obj
    conn = new Mongo();

    // set the db of the connection
    // replace the mydb with the name of the db you want to query
    db = conn.getDB("mydb");

    // replace the products with the name of the collection
    // populate my the products collection
    // this is just for demo purposes - you will probably have your data already
    for (var i=0;i<1000;i++ ) {
    db.products.insert(
        [
            { _id: i, item: "lamp", qty: 50, type: "desk" },
        ],
        { ordered: true }
    )
    }


    // replace the products with the name of the collection
    cursor = db.products.find();

    // print the collection contents
    while ( cursor.hasNext() ) {
        printjson( cursor.next() );
    }
    // eof file: ad-hoc.js
Yordan Georgiev
quelle