MongoDB - 分页

88
使用MongoDB时,制作分页视图是否有特殊的模式?比如,展示最新的10篇博客文章的博客列表,您可以向后导航到较旧的文章。
或者,我们可以通过对例如blogpost.publishdate进行索引并跳过和限制结果来解决这个问题。

1
我会把这个问题留着,因为似乎对于如何制作这个比例尺存在一些分歧。 - Roger Johansson
6个回答

108
使用skip+limit进行分页不是一种处理性能问题或大型集合的好方法;随着页面数量的增加,它会变得越来越慢。使用skip需要服务器遍历从0到偏移(skip)值的所有文档(或索引值)。
更好的方法是使用范围查询(+ limit),其中您传递上一页的范围值作为查询下一页数据的条件。例如,如果您按“publishdate”排序,则只需将上一页的最后一个“publishdate”值作为查询的条件即可获取下一页的数据。

5
很高兴看到确认在 MongoDB 中跳过迭代会遍历所有文档的文件。 - Andrew Orsich
5
这是链接:跳过文档 如果还有其他需要更新的地方,请告诉我。 - Scott Hernandez
2
@ScottHernandez:我有一个带有链接到多个页面的分页(例如:页面:第一页,2,3,4,5,最后一页),并对所有字段进行排序。只有一个字段是唯一的(并且已索引),范围查询是否适用于此用例?恐怕不行,我只是想确认是否可能。谢谢。 - user183037
7
这是跳过文档链接 - Ulises
10
如果有多个发布日期相同的文档,似乎这种方法不起作用。 - d512
显示剩余5条评论

15
  1. 如果需要按多种方式排序,基于范围的分页会很难实现。
  2. 请记住,如果排序参数的字段值不唯一,则基于范围的分页将变得不可靠。

可能的解决方案是简化设计,思考是否只能按id或某些唯一值进行排序?

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

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


这里可以找到一篇带有Python代码示例的好文章:https://www.codementor.io/arpitbhayani/fast-and-efficient-pagination-in-mongodb-9095flbqr - Gianfranco P.
1
谢谢 - 很棒的回答!当人们建议使用过滤器(例如 { _id: { $gt: ... } })进行分页时,我感到很烦恼... 如果使用自定义排序,它根本行不通 - 例如 .sort(...) - Nick Grealy
1
@NickGrealy 我按照教程做了这个,现在的情况是分页“看起来”可以工作,但我会遗漏一些文档,因为我使用的是mongo ID,随着新数据插入到数据库中,然后对集合进行字母排序,如果起始页面包含以A开头的记录,但ID比以AA开头的记录更高,因为它们是之后插入的,那么AA记录将不会被分页返回。跳过和限制是否适用?我需要搜索大约6000万个文档。 - berimbolo
@berimbolo - 这值得讨论- 你不会在评论中获得答案。问题:您期望什么行为? 您正在使用实时系统,记录一直在创建和删除。如果每次新页面加载时重新请求数据的实时快照,则应该期望底层数据发生更改。应该是什么行为? 如果您使用“时间点”数据快照,则将拥有“固定页面”,但您也将拥有“过时”的数据。您描述的问题有多大,并且人们经常遇到它吗? - Nick Grealy
1
这绝对值得讨论,我的问题是我按照车牌号的字母顺序检索了一个文件,并且每15分钟应用更改(删除或添加)到车牌上,问题是如果添加了一个以A开头的新车牌,并且由于页面大小是最后一页,则如果请求下一页,则不会返回任何记录,因为ID比集合中的任何其他记录都要高。现在我正在考虑使用完整的车牌号来驱动查询的大于部分。 - berimbolo
事实上,分页超出有限结果集只是一个神话。人类无法扩展到6000万行,因此您的软件也不需要。如果有人正在寻找某些内容,并且您返回了超过50行,最好告诉他们是否有更多记录,并建议他们尝试提供更多信息进行搜索。此外,在处理分页时,由于行在添加和删除,可能会出现一致性问题,因此请考虑在结果附近放置“截至[date/time]”的消息(用户可以简单地保留页面打开)。 - Neil Barnwell

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
我也遇到了这个包,但它的性能如何与skip/limit和@Scott Hernandez提供的答案相比? - Advena
5
对于任何其他字段排序,这个答案怎么适用? - Nick Grealy

1
这里是使用官方的C#驱动程序检索按CreatedDate排序的User文档列表的示例(其中pageIndex是从零开始的)。
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

1
基于范围的分页是可行的,但您需要聪明地处理如何最小化/最大化查询。
如果您有条件,应尝试将查询结果缓存到临时文件或集合中。由于MongoDB中的TTL集合,您可以将结果插入两个集合中。
1.搜索+用户+参数查询(TTL任何值) 2.查询结果(TTL任何值+清理间隔+ 1)
同时使用两者可以确保在TTL接近当前时间时不会获得部分结果。当您存储结果时,可以利用简单的计数器进行非常简单的范围查询。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接