MongoDB指南---九、游标与数据库命令

上一篇文章: MongoDB指南---八、特定类型的查询
下一篇文章: MongoDB指南---十、索引、复合索引 简介

数据库使用游标返回find的执行结果。客户端对游标的实现一般可以对最终结果进行有效的控制。能够限制结果的数量,略过部分结果,根据任意键按任意顺序的组合对结果进行各类排序,或者是执行其余一些强大的操做。
要想从shell中建立一个游标,首先要对集合填充一些文档,而后对其执行查询,并将结果分配给一个局部变量(用var声明的变量就是局部变量)。这里,先建立一个简单的集合,然后作个查询,并用cursor变量保存结果:正则表达式

> for(i=0; i<100; i++) {
...     db.collection.insert({x : i});
... }
> var cursor = db.collection.find();

这么作的好处是能够一次查看一条结果。若是将结果放在全局变量或者就没有放在变量中,MongoDB shell会自动迭代,自动显示最开始的若干文档。也就是在这以前咱们看到的种种例子,通常你们只想经过shell看看集合里面有什么,而不是想在其中实际运行程序,这样设计也就很合适。
要迭代结果,可使用游标的next方法。也可使用hasNext来查看游标中是否还有其余结果。典型的结果遍历以下所示:shell

> while (cursor.hasNext()) {
...     obj = cursor.next();
...     // do stuff
... }

cursor.hasNext()检查是否有后续结果存在,而后用cursor.next()得到它。
游标类还实现了JavaScript的迭代器接口,因此能够在forEach循环中使用:数据库

> var cursor = db.people.find();
> cursor.forEach(function(x) {
...     print(x.name);
... });
adam
matt
zak

调用find时,shell并不当即查询数据库,而是等待真正开始要求得到结果时才发送查询,这样在执行以前能够给查询附加额外的选项。几乎游标对象的每一个方法都返回游标自己,这样就能够按任意顺序组成方法链。例如,下面几种表达是等价的:segmentfault

> 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个结果或者前4 MB数据(二者之中较小者),这样下次调用next或者hasNext时就没必要再次链接服务器取结果了。客户端用光了第一组结果,shell会再一次联系数据库,使用getMore请求提取更多的结果。getMore请求包含一个查询标识符,向数据库询问是否还有更多的结果,若是有,则返回下一批结果。这个过程会一直持续到游标耗尽或者结果所有返回。服务器

4.5.1 limit、skip和sort

最经常使用的查询选项就是限制返回结果的数量、忽略必定数量的结果以及排序。全部这些选项必定要在查询被发送到服务器以前指定。
要限制结果数量,可在find后使用limit函数。例如,只返回3个结果,能够这样:app

> db.c.find().limit(3)

要是匹配的结果不到3个,则返回匹配数量的结果。limit指定的是上限,而非下限。
skip与limit相似:dom

> db.c.find().skip(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个结果就行了(已经在第一页显示了):

> db.stock.find({"desc" : "mp3"}).limit(50).skip(50).sort({"price" : -1})

然而,略过过多的结果会致使性能问题,下一小节会讲述如何避免略过大量结果。

比较顺序

MongoDB处理不一样类型的数据是有必定顺序的。有时一个键的值多是多种类型的,例如,整型和布尔型,或者字符串和null。若是对这种混合类型的键排序,其排序顺序是预先定义好的。优先级从小到大,其顺序以下:

  1. 最小值;
  2. null;
  3. 数字(整型、长整型、双精度);
  4. 字符串;
  5. 对象/文档;
  6. 数组;
  7. 二进制数据;
  8. 对象ID;
  9. 布尔型;
  10. 日期型;
  11. 时间戳;
  12. 正则表达式;
  13. 最大值 。

4.5.2 避免使用skip略过大量结果

用skip略过少许的文档仍是不错的。可是要是数量很是多的话,skip就会变得很慢,由于要先找到须要被略过的数据,而后再抛弃这些数据。大多数数据库都会在索引中保存更多的元数据,用于处理skip,可是MongoDB目前还不支持,因此要尽可能避免略过太多的数据。一般能够利用上次的结果来计算下一次查询条件。

1. 不用skip对结果分页

最简单的分页方法就是用limit返回结果的第一页,而后将每一个后续页面做为相对于开始的偏移量返回。

> // 不要这么用:略过的数据比较多时,速度会变得很慢
> 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"降序显示文档列表。能够用以下方式获取结果的第一页:

> var page1 = db.foo.find().sort({"date" : -1}).limit(100)

而后,能够利用最后一个文档中"date"的值做为查询条件,来获取下一页:

var latest = null;
// 显示第一页
while (page1.hasNext()) {
    latest = page1.next();
    display(latest);
}
// 获取下一页
var page2 = db.foo.find({"date" : {"$gt" : latest.date}});
page2.sort({"date" : -1}).limit(100);
这样查询中就没有skip了。

2. 随机选取文档

从集合里面随机挑选一个文档算是个常见问题。最笨的(也很慢的)作法就是先计算文档总数,而后选择一个从0到文档数量之间的随机数,利用find作一次查询,略过这个随机数那么多的文档,这个随机数的取值范围为0到集合中文档的总数:

> // 不要这么用
> var total = db.foo.count()
> var random = Math.floor(Math.random()*total)
> db.foo.find().skip(random).limit(1)

这种选取随机文档的作法效率过低:首先得计算总数(要是有查询条件就会很费时),而后用skip略过大量结果也会很是耗时。
略微动动脑筋,从集合里面查找一个随机元素仍是有好得多的办法的。秘诀就是在插入文档时给每一个文档都添加一个额外的随机键。例如在shell中,能够用Math.random()(产生一个0~1的随机数):

> db.people.insert({"name" : "joe", "random" : Math.random()})
> db.people.insert({"name" : "john", "random" : Math.random()})
> db.people.insert({"name" : "jim", "random" : Math.random()})

这样,想要从集合中查找一个随机文档,只要计算一个随机数并将其做为查询条件就行了,彻底不用skip:

> var random = Math.random()
> result = db.foo.findOne({"random" : {"$gt" : random}})

偶尔也会遇到产生的随机数比集合中全部随机值都大的状况,这时就没有结果返回了。遇到这种状况,那就将条件操做符换一个方向:

> if (result == null) {
...     result = db.foo.findOne({"random" : {"$lt" : random}})
... }

要是集合里面本就没有文档,则会返回null,这说得通。
这种技巧还能够和其余各类复杂的查询一同使用,仅须要确保有包含随机键的索引便可。例如,想在加州随机找一个水暖工,能够对"profession"、"state"和"random"创建索引:

> db.people.ensureIndex({"profession" : 1, "state" : 1, "random" : 1})

这样就能很快得出一个随机结果(关于索引,详见第5章)。

4.5.3 高级查询选项

有两种类型的查询:简单查询(plain query)和封装查询(wrapped query)。简单查询就像下面这样:

> var cursor = db.foo.find({"foo" : "bar"})

有一些选项能够用于对查询进行“封装”。例如,假设咱们执行一个排序:

> var cursor = db.foo.find({"foo" : "bar"}).sort({"x" : 1})

实际状况不是将{"foo" : "bar"}做为查询直接发送给数据库,而是先将查询封装在一个更大的文档中。shell会把查询从{"foo" : "bar"}转换成{"$query" : {"foo" : "bar"},"$orderby" : {"x" : 1}}。
绝大多数驱动程序都提供了辅助函数,用于向查询中添加各类选项。下面列举了其余一些有用的选项。

  • $maxscan : integer

指定本次查询中扫描文档数量的上限。

> db.foo.find(criteria)._addSpecial("$maxscan", 20)

若是不但愿查询耗时太多,也不肯定集合中到底有多少文档须要扫描,那么可使用这个选项。这样就会将查询结果限定为与被扫描的集合部分相匹配的文档。这种方式的一个坏处是,某些你但愿获得的文档没有扫描到。

  • $min : document

查询的开始条件。在这样的查询中,文档必须与索引的键彻底匹配。查询中会强制使用给定的索引。
在内部使用时,一般应该使用"$gt"代替"$min"。可使用"$min"强制指定一次索引扫描的下边界,这在复杂查询中很是有用。

  • $max : document

查询的结束条件。在这样的查询中,文档必须与索引的键彻底匹配。查询中会强制使用给定的索引。
在内部使用时,一般应该使用"$lg"而不是"$max"。可使用"$max"强制指定一次索引扫描的上边界,这在复杂查询中很是有用。

  • $showDiskLoc : true

在查询结果中添加一个"$diskLoc"字段,用于显示该条结果在磁盘上的位置。例如:

> db.foo.find()._addSpecial('$showDiskLoc',true)
{ "_id" : 0, "$diskLoc" : { "file" : 2, "offset" : 154812592 } }
{ "_id" : 1, "$diskLoc" : { "file" : 2, "offset" : 154812628 } }

文件号码显示了这个文档所在的文件。若是这里使用的是test数据库,那么这个文档就在test.2文件中。第二个字段显示的是该文档在文件中的偏移量。

4.5.4 获取一致结果

数据处理一般的作法就是先把数据从MongoDB中取出来,而后作一些变换,最后再存回去:

cursor = db.foo.find();

while (cursor.hasNext()) {
    var doc = cursor.next();
    doc = process(doc);
    db.foo.save(doc);
}

结果比较少,这样是没问题的,可是若是结果集比较大,MongoDB可能会屡次返回同一个文档。为何呢?想象一下文档到底是如何存储的吧。能够将集合看作一个文档列表,如图4-1所示。雪花表明文档,由于每个文档都是美丽且惟一的。

clipboard.png

图4-1 待查询的集合
这样,进行查找时,从集合的开头返回结果,游标不断向右移动。程序获取前100个文档并处理。将这些文档保存回数据库时,若是文档体积增长了,而预留空间不足,如图4-2所示,这时就须要对体积增大后的文档进行移动。一般会将它们挪至集合的末尾处(如图4-3所示)。

clipboard.png

图4-2 体积变大的文档,可能没法保存回原先的位置

clipboard.png

图4-3  MongoDB会为更新后没法放回原位置的文档从新分配存储空间
如今,程序继续获取大量的文档,如此往复。当游标移动到集合末尾时,就会返回因体积太大没法放回原位置而被移动到集合末尾的文档,如图4-4所示。

clipboard.png

图4-4 游标可能会返回那些因为体积变大而被移动到集合末尾的文档
应对这个问题的方法就是对查询进行快照(snapshot)。若是使用了这个选项,查询就在"_id"索引上遍历执行,这样能够保证每一个文档只被返回一次。例如,将db.foo.find()改成:

> db.foo.find().snapshot()

快照会使查询变慢,因此应该只在必要时使用快照。例如,mongodump(用于备份,第22章会介绍)默认在快照上使用查询。
全部返回单批结果的查询都被有效地进行了快照。当游标正在等待获取下一批结果时,若是集合发生了变化,数据才可能出现不一致。

4.5.5 游标生命周期

看待游标有两种角度:客户端的游标以及客户端游标表示的数据库游标。前面讨论的都是客户端的游标,接下来简要看看服务器端发生了什么。
在服务器端,游标消耗内存和其余资源。游标遍历尽告终果之后,或者客户端发来消息要求终止,数据库将会释放这些资源。释放的资源能够被数据库另做他用,这是很是有益的,因此要尽可能保证尽快释放游标(在合理的前提下)。
还有一些状况致使游标终止(随后被清理)。首先,游标完成匹配结果的迭代时,它会清除自身。另外,若是客户端的游标已经不在做用域内了,驱动程序会向服务器发送一条特别的消息,让其销毁游标。最后,即使用户没有迭代完全部结果,而且游标也还在做用域中,若是一个游标在10分钟内没有使用的话,数据库游标也会自动销毁。这样的话,若是客户端崩溃或者出错,MongoDB就不须要维护这上千个被打开却再也不使用的游标。
这种“超时销毁”的行为是咱们但愿的:极少有应用程序但愿用户花费数分钟坐在那里等待结果。然而,有时的确但愿游标持续的时间长一些。如果如此的话,多数驱动程序都实现了一个叫immortal的函数,或者相似的机制,来告知数据库不要让游标超时。若是关闭了游标的超时时间,则必定要迭代完全部结果,或者主动将其销毁,以确保游标被关闭。不然它会一直在数据库中消耗服务器资源。

4.6 数据库命令

有一种很是特殊的查询类型叫做数据库命令(database command)。前面已经介绍过文档的建立、更新、删除以及查询。这些都是数据库命令的使用范畴,包括管理性的任务(好比关闭服务器和克隆数据库)、统计集合内的文档数量以及执行聚合等。
本节主要讲述数据库命令,在数据操做、管理以及监控中,数据库命令都是很是有用的。例如,删除集合是使用"drop"数据库命令完成的:

> db.runCommand({"drop" : "test"});
{
    "nIndexesWas" : 1,
    "msg" : "indexes dropped for collection",
    "ns" : "test.test",
    "ok" : true
}

也许你对shell辅助函数比较熟悉,这些辅助函数封装数据库命令,并提供更加简单的接口:

> db.test.drop()

一般,只使用shell辅助函数就能够了,可是了解它们底层的命令颇有帮助。尤为是当使用旧版本的shell链接到新版本的数据库上时,这个shell可能不支持新版数据库的一些命令,这时候就不得不直接使用runCommand()。
在前面的章节中已经看到过一些命令了,好比,第3章使用getLastError来查看更新操做影响到的文档数量:

> db.count.update({x : 1}, {$inc : {x : 1}}, false, true)
> db.runCommand({getLastError : 1})
{
    "err" : null,
    "updatedExisting" : true,
    "n" : 5,
    "ok" : true
}

本节会更深刻地介绍数据库命令,一块儿来看看这些数据库命令究竟是什么,究竟是怎么实现的。本节也会介绍MongoDB提供的一些很是有用的命令。在shell中运行db.listCommands()能够看到全部的数据库命令。

数据库命令工做原理

数据库命令总会返回一个包含"ok"键的文档。若是"ok"的值是1,说明命令执行成功了;若是值是0,说明因为一些缘由,命令执行失败。
若是"ok"的值是0,那么命令的返回文档中就会有一个额外的键"errmsg"。它的值是一个字符串,用于描述命令的失败缘由。例如,若是试着在上一节已经删除的集合上再次执行drop命令:

> db.runCommand({"drop" : "test"});
{ "errmsg" : "ns not found", "ok" : false }

MongoDB中的命令被实现为一种特殊类型的查询,这些特殊的查询会在$cmd集合上执行。runCommand只是接受一个命令文档,而且执行与这个命令文档等价的查询。因而,drop命令会被转换为以下代码:

db.$cmd.findOne({"drop" : "test"});

当MongoDB服务器获得一个在$cmd集合上的查询时,不会对这个查询进行一般的查询处理,而是会使用特殊的逻辑对其进行处理。几乎全部的MongoDB驱动程序都会提供一个相似runCommand的辅助函数,用于执行命令,并且命令老是可以以简单查询的方式执行。
有些命令须要有管理员权限,并且要在admin数据库上才能执行。若是在其余数据库上执行这样的命令,就会获得一个"access denied"(访问被拒绝)错误。若是当前位于其余的数据库,可是须要执行一个管理员命令,可使用adminCommand而不是runCommand:

> use temp
switched to db temp
> db.runCommand({shutdown:1})
{ "errmsg" : "access denied; use admin db", "ok" : 0 }
> db.adminCommand({"shutdown" : 1})

MongoDB中,数据库命令是少数与字段顺序相关的地方之一:命令名称必须是命令中的第一个字段。所以, {"getLastError" : 1, "w" : 2}是有效的命令,而{"w" : 2, "getLastError" : 1}不是。

上一篇文章: MongoDB指南---八、特定类型的查询
下一篇文章: MongoDB指南---十、索引、复合索引 简介
相关文章
相关标签/搜索