对集合进行排序和分页处理

6
如何对非唯一字段进行排序并分页查询?例如,集合中的文档可能是这样排列的(按s:1排序,然后按_id:-1排序):
{_id: 19, s: 3},
{_id: 17, s: 3},
{_id: 58, s: 4},
// etc...

有一种简单的限制/跳过方法可以使用...但速度较慢。

是否可能使用类似以下的内容:

db.collection.find()
  .sort({s:1, _id:-1})
  .min({s:3, _id:17})    // this does not work as wanted!
  .limit(2);

获取

{_id: 17, s: 3},
{_id: 58, s: 4}

?

2个回答

12

如果您想通过“页面数字”进行分页,那么您基本上只能使用在对键排序后应用的.limit().skip()方法,您可能已经阅读了一些内容,并发现这种方法“效率不高”,主要是由于“跳过”“n”个结果以到达特定页面的成本很高。

但当您需要时,这个原则是有道理的:

db.collection.find().sort({ "s": -1, "_id": 1 }).skip(<page-1>).limit(<pageSize>)

如果您只需要在分页中向“前”移动,那么有一种更快的替代方法,也适用于“已排序”的结果。

关键是保持对“s”的“最后可见”值的引用,并通常保留_id值列表,直到该值的“s”发生更改。因此,演示一下更多已经排序过的文档:

{ "_id": 1, "s": 3 },
{ "_id": 2, "s": 3 },
{ "_id": 3, "s": 3 },
{ "_id": 4, "s": 2 },
{ "_id": 5, "s": 1 },
{ "_id": 6, "s": 1 },

为了获取“两个”结果的“第一页”,您的第一个查询很简单:

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

但是将其延伸到处理文件方面:

var lastVal = null,
    lastSeen = [];

db.collection.find().sort({ "s": -1, "_id": 1}).limit(2).forEach(function(doc) {
    if ( doc.s != lastVal ) {    // Change when different
        lastVal = doc.s;
        lastSeen = [];
    }
    lastSeen.push(doc._id);      // Push _id onto array
    // do other things like output
})

所以在第一次迭代中,lastVal的值将是3,而lastSeen将包含数组[1,2]中文档_id的值。

这些东西你会存储在类似于用户会话数据的东西中,等待下一页请求。

然后在您请求下一页设置时,您应该发出以下命令:

var lastVal = 3,
    lastSeen = [1,2];

db.collection.find({ 
    "_id": { "$nin": lastSeen }, 
    "s": { "$lte": lastVal }
}).sort({ "s": -1, "_id": 1}).limit(2).forEach(function(doc) {
    if ( doc.s != lastVal ) {    // Change when different
        lastVal = doc.s;
        lastSeen = [];
    }
    lastSeen.push(doc._id);      // Push _id onto array
    // do other things like output
})

这要求选择“s”的起始值必须是小于等于(由于排序的方向)记录的lastVal的值,并且“_id”字段不能包含在lastSeen中记录的值。

结果的下一页是:

{ "_id": 3, "s": 3 },
{ "_id": 4, "s": 2 },

但是现在,如果您按照逻辑,lastVal 当然是 2,而 lastSeen 现在仅具有单个数组元素 [4]。由于接下来的查询只需要从 2 开始作为小于或等于值进行跟随,因此无需保留其他先前查看过的 "_id" 值,因为它们不会在该选择范围内。

然后这个过程就继续了:

var lastVal = 2,
    lastSeen = [2];

db.collection.find({ 
    "_id": { "$nin": lastSeen }, 
    "s": { "$lte": lastVal }
}).sort({ "s": -1, "_id": 1}).limit(2).forEach(function(doc) {
    if ( doc.s != lastVal ) {    // Change when different
        lastVal = doc.s;
        lastSeen = [];
    }
    lastSeen.push(doc._id);      // Push _id onto array
    // do other things like output
})

所以,通过遵循这种逻辑模式,您可以“存储”从“先前的页面”中找到的信息,并通过结果非常高效地向“前进”。

但是,如果您需要跳转到“第20页”或类似的操作,则只能使用.limit().skip()。 这样做速度会慢一些,但这取决于您能接受什么。


好的解决方案,@blakes-seven 你能解释一下为什么在获取第二页时使用 lastSeen = [2] 而不是 [4] 吗? - Jakub Złoczewski
你还可以将条件应用于 s < lastVal or (s == lastVal and _id > lastId)。这样你就可以省略IN操作,因为它们往往比简单的比较慢。 - Genarito

-3
db.t1.drop()
db.t1.save({_id:19, s:3})
db.t1.save({_id:17, s:3})
db.t1.save({_id:58, s:4})

db.t1.find().sort({s:1, _id:-1}).skip(1).limit(2)

--Result
{ "_id" : 17, "s" : 3 }
{ "_id" : 58, "s" : 4 }

-$


我猜测这是由于没有任何解释就直接跳入代码并(可能)使用了.skip().limit()所致。 - ozanmuyes

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