MongoDB分页的范围查询

时间:2014-01-06 22:51:43

标签: javascript mongodb pagination database

我想在MongoDB上实现分页。对于我的范围查询,我考虑过使用ObjectID:

db.tweets.find({ _id: { $lt: maxID } }, { limit: 50 })

但是,according to the docs,ObjectID的结构意味着“ObjectId值不代表严格的插入顺序”:

  

ObjectId值的顺序与生成时间之间的关系在一秒内不严格。 如果单个系统上的多个系统或多个进程或线程在一秒钟内生成值; ObjectId值不代表严格的插入顺序。客户端之间的时钟偏差也会导致非严格的排序,即使是值,因为客户端驱动程序生成ObjectId值,而不是mongod进程。

然后我考虑用时间戳查询:

db.tweets.find({ created: { $lt: maxDate } }, { limit: 50 })

但是,不能保证日期是唯一的 - 很可能在同一秒内创建两个文档。这意味着在分页时可能会错过文档。

是否有任何类型的远程查询可以为我提供更多稳定性?

5 个答案:

答案 0 :(得分:56)

尽管您的分页语法错误,但使用ObjectId()完全没问题。你想要:

 db.tweets.find().limit(50).sort({"_id":-1});

这表示你希望推文按_id值按降序排序,你想要最新的50个。你的问题是当前结果集改变时分页很棘手 - 所以不要使用跳过在下一页中,您要记下结果集中最小的_id(最近的第50个_id值,然后使用以下内容获取下一页:

 db.tweets.find( {_id : { "$lt" : <50th _id> } } ).limit(50).sort({"_id":-1});

这将为您提供下一个“最新”的推文,而不会有新的推文随着时间推移您的分页。

完全没有必要担心_id值是否与插入顺序严格对应 - 它将足够接近99.999%,并且没有人真正关心第二级上面的推文首先 - 你甚至可能会注意到Twitter经常不按顺序显示推文,但这并不是那么重要。

如果 至关重要,那么您必须使用相同的技术,但使用“推文日期”,其中该日期必须是时间戳,而不仅仅是日期。

答案 1 :(得分:11)

推文“实际”时间戳(即推文的时间和您希望它排序的标准)不会与推文“插入”时间戳(即添加到本地集合的时间)不同。当然,这取决于您的应用程序,但是可能会出现推文插入可能被批处理或最终以“错误”顺序插入的情况。因此,除非您在Twitter工作(并且能够以正确的顺序访问集合),否则您将无法依赖 $naturalObjectID来排序逻辑。

Mongo文档建议skip and limit for paging

db.tweets.find({created: {$lt: maxID}).
          sort({created: -1, username: 1}).
          skip(50).limit(50); //second page

但是,使用skip时存在性能问题:

  

cursor.skip()方法通常很昂贵,因为它要求服务器从集合或索引的开头走,以在开始返回结果之前获取偏移或跳过位置。随着偏移量的增加,cursor.skip()将变得更慢且CPU密集度更高。

这是因为skip不适合MapReduce模型,并且不是一个可以很好地扩展的操作,你必须等待一个已排序的集合才能被“切片”。现在limit(n)听起来像一个同样糟糕的方法,因为它应用了“来自另一端”的类似约束;但是,如果应用了排序,引擎只能在遍历集合时保留每个分片的内存n元素,从而在某种程度上优化过程。

另一种方法是使用基于范围的分页。在检索推文的第一页后,您知道最后一条推文的created值是什么,所以您所要做的就是用原来的maxID替换这个新值:

db.tweets.find({created: {$lt: lastTweetOnCurrentPageCreated}).
          sort({created: -1, username: 1}).
          limit(50); //next page

执行这样的find条件很容易并行化。但是如何处理下一页以外的页面呢?您不知道第5,10,20页页面的开始日期,甚至上一页页面! @SergioTulentsev建议creative chaining of methods,但我建议在单独的pages集合中预先计算聚合字段的倒数第一个范围;这些可以在更新时重新计算。此外,如果您对DateTime不满意(请注意效果评论)或担心重复值,则应考虑compound indexes时间戳+帐号关系(因为用户无法在此处发送两次推文)同时),甚至是两者的人工聚合:

db.pages.
find({pagenum: 3})
> {pagenum:3; begin:"01-01-2014@BillGates"; end:"03-01-2014@big_ben_clock"}

db.tweets.
find({_sortdate: {$lt: "03-01-2014@big_ben_clock", $gt: "01-01-2014@BillGates"}).
sort({_sortdate: -1}).
limit(50) //third page

使用聚合字段进行排序在“折叠”工作(尽管可能有更多犹太方法来处理这种情况)。这可以设置为a unique index,其值在插入时更正,单个推文文档看起来像

{
  _id: ...,
  created: ...,    //to be used in markup
  user: ...,    //also to be used in markup
  _sortdate: "01-01-2014@BillGates" //sorting only, use date AND time
}

答案 2 :(得分:1)

即使来自多个客户端(生成ObjectId),即使在相同的毫秒内插入/更新了多个文档,以下方法也会起作用。对于simiplicity,在以下查询中,我正在预测_id,lastModifiedDate。

  1. 第一页,获取结果由modifiedTime(Descending)排序,ObjectId(Ascending)为第一页。

    db.product.find({},{"_id":1,"lastModifiedDate":1}).sort({"lastModifiedDate":-1, "_id":1}).limit(2)

  2. 记下此页面中提取的最后一条记录的ObjectId和lastModifiedDate。 (loid,lmd)

    1. 对于sencod页面,包含查询条件以搜索if(lastModifiedDate = lmd AND oid&gt; loid)OR(lastModifiedDate&lt; loid)
    2. db.productfind({$or:[{"lastModifiedDate":{$lt:lmd}},{"_id":1,"lastModifiedDate":1},{$and:[{"lastModifiedDate":lmd},{"_id":{$gt:loid}}]}]},{"_id":1,"lastModifiedDate":1}).sort({"lastModifiedDate":-1, "_id":1}).limit(2)

      对后续页面重复相同。

答案 3 :(得分:0)

如果您将查询限制在前一秒(或者不关心次要的怪异可能性),那么ObjectIds应该足够分页。如果这不足以满足您的需求,那么您将需要实现一个像自动增量一样工作的ID生成系统。

更新

要查询前一秒的ObjectIds,您需要手动构建ObjectID。

请参阅ObjectId http://docs.mongodb.org/manual/reference/object-id/

的规范

尝试使用此表达式从mongos中执行此操作。

{ _id : 
  {
      $lt : ObjectId(Math.floor((new Date).getTime()/1000 - 1).toString(16)+"ffffffffffffffff")
  }

}

最后的'f'是为了最大化与时间戳无关的可能的随机位,因为你的查询次数较少。

我建议在应用程序服务器上创建实际的ObjectId而不是mongos,因为如果你有很多用户,这种类型的计算会降低你的速度。

答案 4 :(得分:0)

我用这种方式使用mongodb _id构建了一个分页。

// import ObjectId from mongodb
let sortOrder = -1;
let query = []
if (prev) {
    sortOrder = 1
    query.push({title: 'findTitle', _id:{$gt: ObjectId('_idValue')}})
}

if (next) {
    sortOrder = -1
    query.push({title: 'findTitle', _id:{$lt: ObjectId('_idValue')}})
}

db.collection.find(query).limit(10).sort({_id: sortOrder})