数据库会使用游标返回 find 的执行结果。游标的客户端实现通常能够在很大程度上对查询的最终输出进行控制。你可以限制结果的数量,跳过一些结果,按任意方向的任意键组合对结果进行排序,以及执行许多其他功能强大的操作。
要使用 shell 创建游标,首先要将一些文档放入集合中,对它们执行查询,然后将结果分配给一个局部变量(用 “var” 定义的变量就是局部变量)。在这里,先创建一个非常简单的集合并对其进行查询,然后将结果存储在 cursor 变量中:
> for (i = 0; i < 100; i++) {
... db.collection.insertOne({x: i})
... }
{
"acknowledged" : true,
"insertedId" : ObjectId("6359f2eb20adf7c001484f21")
}
> var cursor = db.collection.find()
这样做的好处是可以一次查看一个结果。如果将结果存储到全局变量中或根本不存储到变量中,那么 MongoDB shell 将自动遍历并显示最开始的几个文档。这是到目前为止我们一直看到的种种例子,通常大家也只是希望看到集合中的内容,而不是使用 shell 进行编程。
要遍历结果,可以在游标上使用 next 方法。可以使用 hasNext 检查是否还有其他结果。典型的结果遍历如下所示:
> while (cursor.hasNext()) {
... obj = cursor.next();
... // 执行任务
... }
cursor.hasNext() 会检查是否有后续结果存在,而 cursor.next() 用来对其进行获取。
cursor 类还实现了 JavaScript 的迭代器接口,因此可以在 forEach 循环中使用:
> var cursor = db.users.find()
> cursor.forEach(function (x) { print(x.name) })
zhangsan
lisi
调用 find 时,shell 并不会立即查询数据库,而是等到真正开始请求结果时才发送查询,这样可以在执行之前给查询附加额外的选项。cursor 对象的大多数方法会返回游标本身,这样就可以按照任意顺序将选项链接起来了。例如,以下这些是等价的:
> var cursor = db.foo.find().sort({x: 1}).limit(1).skip(10)
> var cursor = db.foo.find().limit(1).sort({x: 1}).skip(10)
> var cursor = db.foo.find().skip(10).limit(1).sort({x: 1})
这时,查询还没有真正执行。所有这些函数只会构造查询。现在,假设执行以下调用:
> cursor.hasNext()
这时,查询会被发往服务器端。shell 会立刻获取前 100 个结果或者前 4MB 的数据(两者之中较小者),这样下次调用 next 或者 hasNext 时就不必再次连接服务器端去获取结果了。在客户端遍历完第一组结果后,shell 会再次连接数据库,使用 getMore 请求更多的结果。getMore 请求包含一个游标的标识符,它会向数据库询问是否还有更多的结果,如果有则返回下一批结果。这个过程会一直持续,直到游标耗尽或者结果被全部返回。
limit、skip和sort
最常用的查询选项是限制返回结果的数量、略过一定数量的结果以及排序。所有这些选项必须在查询被发送到数据库之前指定。
要限制结果数量,可以在 find 之后链式调用 limit 函数。如果只返回 3 个结果,那么可以这样做:
> db.c.find().limit(3)
如果集合中所匹配的文档不到 3 个,则仅返回匹配数量的结果。limit 指定的是上限,而不是下限。
skip 与 limit 类似:
> db.c.find().skip(3)
这个操作会略过前 3 个匹配的文档,然后返回剩下的文档。如果集合中匹配的文档少于 3个,则不会返回任何文档。
sort 会接受一个对象作为参数,这个对象是一组键–值对,键对应文档的键名,值对应排序的方向。排序方向可以是 1(升序)或 -1(降序)。如果指定了多个键,则结果会按照这些键被指定的顺序进行排序。例如,要按照 “username” 升序及 “age” 降序排列,可以这样做:
> db.c.find().sort({"username" : 1, "age" : -1})
这 3 个方法可以结合使用。对于分页来说,这非常方便。假设你正在运营一个在线商店,有人想搜索 mp3。如果想每页返回 50 个结果并按照价格从高到低排序,可以像下面这样做:
> db.stock.find({"desc" : "mp3"}).limit(50).sort({"price" : -1})
如果顾客单击了“下一页”以获取更多的结果,可以通过对查询添加 skip 来实现,这样就可以略过前 50 个结果了(这些结果已经显示在第 1 页了):
> db.stock.find({"desc" : "mp3"}).limit(50).skip(50).sort({"price" : -1})
然而,略过大量的结果会导致性能问题。
比较顺序
MongoDB 对于类型的比较有一个层次结构。有时一个键的值可能有多种类型:整型和布尔型,或者字符串和 null。如果对混合类型的键进行排序,那么会有一个预定义的排序顺序。从最小值到最大值,顺序如下:
- 最小值
- null
- 数字(整型、长整型、双精度浮点型、小数型)
- 字符串
- 对象/文档
- 数组
- 二进制数据
- 对象 ID
- 布尔型
- 日期
- 时间戳
- 正则表达式
- 最大值
避免略过大量结果
使用 skip 来略过少量的文档是可以的。但对于结果非常多的情况,skip 会非常慢,因为需要先找到被略过的结果,然后再丢弃这些数据。大多数数据库会在索引中保存更多的元数据以处理 skip,但 MongoDB 目前还不支持这样做,所以应该避免略过大量的数据。通常下一次查询的条件可以基于上一次查询的结果计算出来。
不使用skip对结果进行分页
最简单的分页方式是使用 limit 返回结果的第 1 页,然后将每个后续页面作为相对于开始的偏移量进行返回:
> // 不要这么做:略过大量数据会非常慢
> var page1 = db.foo.find(criteria).limit(100)
> var page2 = db.foo.find(criteria).skip(100).limit(100)
> var page3 = db.foo.find(criteria).skip(200).limit(100)
...
然而,通常可以根据你的查询找到一种不使用 skip 来进行分页的方法。假设要按照"date" 降序显示文档。可以通过以下方式来获得结果的第 1 页:
> var page1 = db.foo.find().sort({"date" : -1}).limit(100)
然后,假设日期是唯一的,可以使用最后一个文档的 “date” 值作为获取下一页的查询条件:
var latest = null;
// 显示第1页
while (page1.hasNext()) {
latest = page1.next();
display(latest);
}
// 获取下一页
var page2 = db.foo.find({"date" : {"$lt" : latest.date}});
page2.sort({"date" : -1}).limit(100);
这样查询中就没有 skip 了。
查找一个随机文档
从集合中随机选择文档是一个常见的问题。最简单(但很慢)的解决方案是先计算文档总数,然后略过 0 和集合大小之间的一个随机的文档数量来执行一次 find 查询:
> // 不要这样做
> var total = db.foo.count()
> var random = Math.floor(Math.random()*total)
> db.foo.find().skip(random).limit(1)
以这种方式获取随机元素实际上非常低效:必须先计算总数(如果使用查询条件,这会是一个昂贵的操作),并且略过大量的元素也会非常耗时。
这需要一些提前规划,但如果你知道要在集合中查找随机元素,那么有一种高效得多的方法可以执行此操作。诀窍就是在插入文档时为每个文档添加一个额外的随机键。如果正在使用 shell,那么可以使用 Math.random() 函数(这会产生 0 和 1 之间的一个随机数):
> db.people.insertOne({"name" : "joe", "random" : Math.random()})
> db.people.insertOne({"name" : "john", "random" : Math.random()})
> db.people.insertOne({"name" : "jim", "random" : Math.random()})
这样,当从集合中查找一个随机文档时,可以计算一个随机数并将其作为查询条件,而不再使用 skip:
> var random = Math.random()
> result = db.people.findOne({"random" : {"$gt" : random}})
random 可能会大于集合中的任何 “random” 值,并且不会返回任何结果。可以简单地从另一个方向返回文档以避免这种情况:
> if (result == null) {
... result = db.users.findOne({"random" : {"$lte" : random}})
... }
如果集合中没有任何文档,那么这种方式会返回 null,这也是合理的。
这种方式可以用于任意复杂的查询,只需确保有一个包含随机键的索引。如果要在加利福尼亚州随机找一个水管工,那么可以在 “profession”、“state” 和 “random” 上创建一个索引:
> db.users.ensureIndex({"profession" : 1, "state" : 1, "random" : 1})
这便可以快速地找到一个随机结果。
游标生命周期
游标包括两个部分:面向客户端的游标和由客户端游标所表示的数据库游标。
在服务器端,游标会占用内存和资源。一旦游标遍历完结果之后,或者客户端发送一条消息要求终止,数据库就可以释放它正在使用的资源。释放这些资源可以让数据库将其用于其他用途,这是非常有益的,因此要确保可以尽快(在合理的范围内)释放游标。
还有一些情况可能导致游标终止以及随后的清理。首先,当游标遍历完匹配的结果时,它会清除自身。其次,当游标超出客户端的作用域时,驱动程序会向数据库发送一条特殊的消息,让数据库知道它可以“杀死”该游标。最后,即使用户没有遍历完所有结果而且游标仍在作用域内,如果 10 分钟没有被使用的话,数据库游标也将自动“销毁”。这样,如果客户端崩溃或者出错,MongoDB 就不需要维护上千个被打开的游标了。
这种“超时销毁”的机制通常是用户所期望的:很少有用户愿意花几分钟坐在那里等待结果。然而,有时可能的确需要一个游标维持很长时间。在这种情况下,许多驱动程序实现了一个称为 immortal 的函数,或者类似的机制,它告诉数据库不要让游标超时。如果关闭了游标超时,则必须遍历完所有结果或主动将其销毁以确保游标被关闭。否则,它会一直占用数据库的资源,直到服务器重新启动。