内容简介:上一篇文章:下一篇文章:数据库使用游标返回find的执行结果。客户端对游标的实现通常能够对最终结果进行有效的控制。可以限制结果的数量,略过部分结果,根据任意键按任意顺序的组合对结果进行各种排序,或者是执行其他一些强大的操作。
上一篇文章: MongoDB指南---8、特定类型的查询
下一篇文章:
数据库使用游标返回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.1limit、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}不是。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- sql server游标使用步骤示例(创建游标 关闭游标)
- Oracle游标使用详解
- Oracle中游标Cursor基本用法详解
- Elasticsearch系列---搜索执行过程及scroll游标查询
- Oracle使用游标给所有数据表添加主键约束
- ArangoDB 发布 3.4 正式版:全文搜索、GeoJSON、流式游标
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。