资讯专栏INFORMATION COLUMN

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

lemanli / 3864人阅读

摘要:例如在中,可以用产生一个的随机数这样,想要从集合中查找一个随机文档,只要计算一个随机数并将其作为查询条件就好了,完全不用偶尔也会遇到产生的随机数比集合中所有随机值都大的情况,这时就没有结果返回了。指定本次查询中扫描文档数量的上限。

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

数据库使用游标返回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来查看游标中是否还有其他结果。典型的结果遍历如下所示:

> 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并不立即查询数据库,而是等待真正开始要求获得结果时才发送查询,这样在执行之前可以给查询附加额外的选项。几乎游标对象的每个方法都返回游标本身,这样就可以按任意顺序组成方法链。例如,下面几种表达是等价的:

> 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个结果,可以这样:

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

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

> 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。如果对这种混合类型的键排序,其排序顺序是预先定义好的。优先级从小到大,其顺序如下:

最小值;

null;

数字(整型、长整型、双精度);

字符串;

对象/文档;

数组;

二进制数据;

对象ID;

布尔型;

日期型;

时间戳;

正则表达式;

最大值 。

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所示。雪花代表文档,因为每一个文档都是美丽且唯一的。

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

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

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

图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指南---8、特定类型的查询
下一篇文章:MongoDB指南---10、索引、复合索引 简介

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/44103.html

相关文章

  • MongoDB指南---9游标据库命令

    摘要:例如在中,可以用产生一个的随机数这样,想要从集合中查找一个随机文档,只要计算一个随机数并将其作为查询条件就好了,完全不用偶尔也会遇到产生的随机数比集合中所有随机值都大的情况,这时就没有结果返回了。指定本次查询中扫描文档数量的上限。 上一篇文章:MongoDB指南---8、特定类型的查询下一篇文章:MongoDB指南---10、索引、复合索引 简介 数据库使用游标返回find的执行结果...

    sutaking 评论0 收藏0
  • MongoDB指南---14、特殊的索引和集合:固定集合、TTL索引、全文本索引

    摘要:固定集合不能被分片。为固定集合指定文档数量限制时,必须同时指定固定集合的大小。没有索引的集合默认情况下,每个集合都有一个索引。 上一篇文章:MongoDB指南---13、索引类型、索引管理下一篇文章:MongoDB指南---15、特殊的索引和集合:地理空间索引、使用GridFS存储文件 本章介绍MongoDB中一些特殊的集合和索引类型,包括: 用于类队列数据的固定集合(capped...

    cikenerd 评论0 收藏0
  • MongoDB指南---14、特殊的索引和集合:固定集合、TTL索引、全文本索引

    摘要:固定集合不能被分片。为固定集合指定文档数量限制时,必须同时指定固定集合的大小。没有索引的集合默认情况下,每个集合都有一个索引。 上一篇文章:MongoDB指南---13、索引类型、索引管理下一篇文章:MongoDB指南---15、特殊的索引和集合:地理空间索引、使用GridFS存储文件 本章介绍MongoDB中一些特殊的集合和索引类型,包括: 用于类队列数据的固定集合(capped...

    QLQ 评论0 收藏0
  • (翻译) MongoDB(19) mongo Shell

    摘要:概述是的交互式接口你可以使用查询和更新数据以及执行管理操作是发行版的一个组件一旦你已经安装并且启动了连接到你运行的实例在手册的大部分示例都是使用然而许多驱动程序为提供了类似的接口启动重要在尝试运行之前确保正在运行启动并使用默认端口连接到本地 概述 mongo shell 是 MongoDB的交互式 JavaScript 接口. 你可以使用 mongo shell 查询和更新数据以及执行...

    张率功 评论0 收藏0
  • MongoDB指南---10、索引、复合索引 简介

    摘要:可以通过来强制使用某个特定的索引,再次执行这个查询,但是这次使用,作为索引。 上一篇文章:MongoDB指南---9、游标与数据库命令下一篇文章:MongoDB指南---11、使用复合索引、$操作符如何使用索引、索引对象和数组、索引基数 本章介绍MongoDB的索引,索引可以用来优化查询,而且在某些特定类型的查询中,索引是必不可少的。 什么是索引?为什么要用索引? 如何选择需要建立...

    enrecul101 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<