我想在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 })
但是,不能保证日期是唯一的 - 很可能在同一秒内创建两个文档。这意味着在分页时可能会错过文档。
是否有任何类型的远程查询可以为我提供更多稳定性?
答案 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工作(并且能够以正确的顺序访问集合),否则您将无法依赖仅 $natural
或ObjectID
来排序逻辑。
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。
第一页,获取结果由modifiedTime(Descending)排序,ObjectId(Ascending)为第一页。
db.product.find({},{"_id":1,"lastModifiedDate":1}).sort({"lastModifiedDate":-1, "_id":1}).limit(2)
记下此页面中提取的最后一条记录的ObjectId和lastModifiedDate。 (loid,lmd)
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})