MongoDB-ページング


81

MongoDBを使用する場合、ページビューなどを作成するための特別なパターンはありますか?古い投稿に戻ることができる最新の10件の投稿をリストしたブログを言います。

または、blogpost.publishdateなどのインデックスを使用して解決し、スキップして結果を制限しますか?


1
このスケールを作成する正しい方法について意見の相違があるように思われるので、これをぶら下げたままにしておきます。
ロジャーヨハンソン

回答:


98

パフォーマンスが問題になる場合、またはコレクションが大きい場合、skip + limitを使用することはページングを行うための良い方法ではありません。ページ番号を増やすと、だんだん遅くなります。スキップを使用するには、サーバーが0からオフセット(スキップ)値までのすべてのドキュメント(またはインデックス値)をウォークスルーする必要があります。

最後のページの範囲値を渡す範囲クエリ(+制限)を使用することをお勧めします。たとえば、「publishdate」で並べ替える場合は、最後の「publishdate」値をクエリの基準として渡すだけで、次のページのデータを取得できます。


4
mongodbのスキップがすべてのドキュメントを反復処理することを確認するいくつかのドキュメントを見るのは素晴らしいことです。
Andrew Orsich 2011

5
ここに行きます:ドキュメントをスキップします 情報を更新する必要がある他の場所があれば、私に知らせてください。
スコットヘルナンデス

2
@ScottHernandez:複数のページへのリンク(ページ:First、2、3、4、5、Lastなど)とすべてのフィールドでの並べ替えをページングしています。私のフィールドの1つだけが一意(およびインデックス付き)ですが、範囲クエリはこのユースケースで機能しますか?恐れ入りませんが、それが可能かどうかを確認したかっただけです。ありがとう。
user183037 2011年


8
同じpublishdate値を持つ複数のドキュメントがある場合、このようには機能しないようです。
d512 2015

12
  1. アイテムをさまざまな方法で並べ替える必要がある場合、範囲ベースのページングを実装するのは困難です。
  2. sortパラメータのフィールド値が一意でない場合、範囲ベースのページングは​​実現不可能になることに注意してください。

考えられる解決策:IDまたは一意の値でのみ並べ替えることができるかどうかを考えて、設計を単純化してみてください。

そして、可能であれば、範囲ベースのページングを使用できます。

一般的な方法は、sort()、skip()、limit()を使用して、上記のページングを実装することです。


Pythonのコード例に関する優れた記事はここにありますcodementor.io/arpitbhayani/…–
Gianfranco P.

1
ありがとう-素晴らしい答えです!人々がフィルターを使用してページ付けを提案するとき、私はイライラします。{ _id: { $gt: ... } }たとえば、カスタム順序を使用すると、単に機能しません.sort(...)
ニックグリーリー2018

1
@NickGrealyチュートリアルに従ってこれを実行したところ、ページングが機能しているように見えますが、mongo IDを使用しているためにドキュメントが欠落しているのに、新しいデータがデータベースに挿入されてから、開始ページにAで始まるレコードが含まれているが、その後に挿入されたためにIDがAAで始まるレコードよりも高い場合、コレクションはアルファベット順にソートされます。その後、AAレコードはページングによって返されません。スキップと制限は適切ですか?私は約6000万のドキュメントを検索する必要があります。
berimbolo

@ berimbolo-これは会話に値します-ここのコメントではあなたの答えは得られません。質問:どのような行動を期待しますか?あなたはライブシステムで作業しており、レコードは常に作成および削除されています。新しいページが読み込まれるたびにデータのライブスナップショットを再リクエストする場合は、基になるデータが変更されることを期待する必要があります。振る舞いはどうあるべきですか?「特定の時点」のデータスナップショットを使用する場合、「固定ページ」がありますが、「古い」データもあります。あなたが説明している問題の大きさ、そして人々はどのくらいの頻度でそれに遭遇しますか?
ニックグリーリー

1
それは間違いなく会話の価値があります、私の問題はナンバープレートのアルファベット順に1回限りのファイルを取得し、15分ごとに変更された(削除または追加された)プレートに更新を適用することです、問題は新しいプレートが追加されて開始する場合ですたとえばAを使用し、ページサイズがページの最後であるため、次が要求された場合、レコードは返されません(仮定と不自然な例ですが、私の問題の説明です)。IDが他のどのファイルよりも高いためです。セット。現在、完全なナンバープレートを使用してクエリの大部分を駆動することを検討しています。
berimbolo

5

これは、コレクションが大きくなりすぎて1つのクエリで返すことができない場合に使用したソリューションです。_idフィールドの固有の順序を利用して、指定されたバッチサイズでコレクションをループすることができます。

これはnpmモジュール、mongoose-pagingとして、完全なコードは以下のとおりです。

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;
};

次のようにモデルに添付します。

MySchema.plugin(findPaged);

次に、次のようにクエリします。

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)
  }
);

この回答に賛成票がない理由がわかりません。これは、スキップ/限界以下のpaginateに、より効率的な方法である
nxmohamad

私もこのパッケージで来ましたが、スキップ/制限と比較したパフォーマンスと@Scott Hernandezによって提供された回答はどうですか?
Tanckom 2018年

5
他のフィールドで並べ替える場合、この回答はどのように機能しますか?
ニックグリーリー2018

1

範囲ベースのページングは​​実行可能ですが、クエリを最小化/最大化する方法について賢明である必要があります。

余裕がある場合は、クエリの結果を一時ファイルまたはコレクションにキャッシュしてみてください。MongoDBのTTLコレクションのおかげで、結果を2つのコレクションに挿入できます。

  1. 検索+ユーザー+パラメータクエリ(TTLは何でも)
  2. クエリの結果(TTLは何でも+クリーニング間隔+ 1)

両方を使用すると、TTLが現在の時刻に近い場合に部分的な結果が得られないことが保証されます。結果を保存するときに単純なカウンターを利用して、その時点で非常に単純な範囲クエリを実行できます。


1

これは、公式のC#ドライバーを使用して(がゼロベースの場合)、Userドキュメントの順序のリストを取得する例です。CreatedDatepageIndex

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();
}

すべてのソートおよびページング操作はサーバー側で実行されます。これはC#の例ですが、他の言語のポートにも同じことが当てはまると思います。

http://docs.mongodb.org/ecosystem/tutorial/use-csharp-driver/#modifying-a-cursor-before-enumerating-itを参照してください。


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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.