MongoDB-分页


81

使用MongoDB时,是否有任何特殊的模式可用于创建页面视图?说一个博客,其中列出了10条最新的帖子,您可以在其中向后浏览到较早的帖子。

还是用例如blogpost.publishdate上的索引来解决它,而只是跳过并限制结果?


1
我将把这个挂起,因为对于制作这种音阶的正确方法似乎存在一些分歧。
罗杰·约翰逊

Answers:


98

当性能有问题或有大量集合时,使用skip + limit并不是进行分页的好方法。随着页码的增加,它会变得越来越慢。使用跳过要求服务器将所有文档(或索引值)从0遍历到偏移(跳过)值。

最好使用范围查询(+限制),在其中传递最后一页的范围值。例如,如果按“发布日期”进行排序,则只需传递最后一个“发布日期”值作为查询条件即可获取下一页数据。


4
很高兴看到某些文档确认在mongodb中跳过所有文档。
Andrew Orsich

5
在这里,您可以进行以下操作:跳过docs 如果还有其他地方需要更新信息,请告诉我。
Scott Hernandez

2
@ScottHernandez:我的分页带有指向多个页面的链接(例如:Page:First,2、3、4、5,Last),并在所有字段上进行排序。我只有一个字段是唯一的(并已建立索引),范围查询是否适用于该用例?恐怕不是,我只是想确认一下是否有可能。谢谢。
user183037 2011年


7
如果有多个具有相同publishdate值的文档,则似乎无法正常工作。
2015年

12
  1. 如果您需要以多种方式对项目进行排序,则很难实现基于范围的分页。
  2. 请记住,如果sort参数的字段值不是unique,则基于范围的分页将变得不可靠。

可能的解决方案:尝试简化设计,考虑是否只能按ID或某个唯一值排序?

如果可以的话,可以使用基于范围的分页。

常见的方法是使用sort(),skip()和limit()来实现上述分页。


与Python代码示例的好文章可以在这里找到codementor.io/arpitbhayani/...
詹弗兰科P.

1
谢谢-很好的答案!当人们通过使用过滤器如建议分页我生气{ _id: { $gt: ... } }如- ...,如果使用自定义排序根本不起作用.sort(...)
尼克·格莱利

@NickGrealy我按照教程进行了此操作,现在我处于分页“看起来”有效的情况,但是我丢失了文档,因为我使用的是mongo ID,但是随着新数据被插入到db中,然后如果起始页包含以A开头但ID高于起始AA的记录,则该收集按字母顺序排序,因为它们是在AA记录之后插入的,则分页不返回AA记录。跳过和限制是否合适?我在大约6000万个文档中进行搜索。
berimbolo

@berimbolo-这很值得交谈-您不会在评论中得到答案。问题:您期望什么行为?您正在使用实时系统,并且一直在创建和删除记录。如果您为每个新页面加载请求数据的实时快照,那么您应该期望基础数据会发生变化。行为应该是什么?如果使用“时间点”数据快照,则将具有“固定页面”,但也将具有“过时”数据。您描述的问题有多大?人们经常遇到该问题吗?
Nick Grealy19年

1
绝对值得一谈,我的问题是我按照车牌的字母顺序检索了一个文件,并且每隔15分钟将更新应用于更改(删除或添加)的车牌,问题是如果添加了新车牌并开始以A为例,由于页面大小是页面的最后一页,因此如果请求next,则不会返回任何记录(我认为是一个假设和人为的示例,但可以说明我的问题),因为ID高于集合。我现在正在考虑使用完整的车牌来驱动更大范围的查询。
berimbolo

5

这是我的集合太大而无法在单个查询中返回时使用的解决方案。它利用了_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)
  }
);

不知道为什么这个答案没有更多的投票。这是一种比跳过/限制更有效的分页方式
nxmohamad

我也有这个软件包,但是与跳过/限制和@Scott Hernandez提供的答案相比,它的性能如何?
Tanckom

4
对于在其他任何字段上进行排序,此答案将如何工作?
尼克·格莱利

1

基于范围的分页是可行的,但是您需要对最小/最大查询方式保持谨慎。

如果负担得起,则应尝试将查询结果缓存在临时文件或集合中。感谢MongoDB中的TTL集合,您可以将结果插入两个集合中。

  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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.