您的位置:首页 > 数据库 > Mongodb

《Mongodb权威指南》学习笔记 第四章 查询(二)

2012-08-24 20:22 309 查看
4.5 游标

数据库使用游标来返回find的执行结果。客户端对游标的实现通常能够对最终结果进行有效的控制。可以限制结果的数量,掠过部分结果,根据任意方向任意键的组合对结果进行各种排序,或者是执行其他一些功能强大的操作。

要想从shell中创建一个游标,首先要对集合填充一些文档,然后对其执行查询,并将结果分配给一个局部变量(用var声明的变量就是局部变量)。这里,先创建一个简单的集合,而后做个查询,并用cursor变量保存结果:

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

这么做的好处是一次可以查看一条结果。如果将结果放在全局变量或者就没有放在变量中,MongoDB shell会自动迭代,自动显示最开始的若干文档。也就是在这之前我们看大的种种例子,一般大家只想通过shell看看集合里面有什么,而不是想在其中市级运行程序,这样设计也就很合适。

要迭代结果,可以使用游标的next方法。也可以使用hasNext来查看有没有其他结果。典型的结果遍历如下:

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

cursor.hasNext()检查是否有后续结果存在,然后用cursor.next()将其获得。游标类还实现了迭代器接口,所以可以在foreach循环中使用。

> var cursor = db.peope.find();
> cursor.forEach(function(x)
... {
... print(x.age);
... });
45
45

当调用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();
false

这时,查询被发往服务器。shell立即获取前100个结果或者前4M数据(两者之中较小者),这样下次调用next或者hasNext时就不必兴师动众跑到服务器上去了。客户端用光了第一组结果,shell会再一次联系数据库,并要求更多的结果。这个过程一直会持续打扫游标耗尽或者结果全部返回。

4.5.1 limit、skip和sort

最常用的查询选项就是限制返回结果的数量,忽略一定数量的结果并排序。所有这些选项一定要在查询被派发到服务器之前添加。

要限制结果数量,可以在find后使用limit函数。例如,只返回3个结果,可以这样:

> db.c.find();
{ "_id" : ObjectId("503dd95a4ba09fc49a57808d"), "y" : null }
{ "_id" : ObjectId("503dd95e4ba09fc49a57808e"), "y" : 1 }
{ "_id" : ObjectId("503dd9624ba09fc49a57808f"), "y" : 2 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5b"), "x" : 0 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5c"), "x" : 1 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5d"), "x" : 2 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5e"), "x" : 3 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5f"), "x" : 4 }
{ "_id" : ObjectId("503e38157d2cae668c71ac60"), "x" : 5 }
{ "_id" : ObjectId("503e38157d2cae668c71ac61"), "x" : 6 }
{ "_id" : ObjectId("503e38157d2cae668c71ac62"), "x" : 7 }
{ "_id" : ObjectId("503e38157d2cae668c71ac63"), "x" : 8 }
{ "_id" : ObjectId("503e38157d2cae668c71ac64"), "x" : 9 }
{ "_id" : ObjectId("503e38157d2cae668c71ac65"), "x" : 10 }
{ "_id" : ObjectId("503e38157d2cae668c71ac66"), "x" : 11 }
{ "_id" : ObjectId("503e38157d2cae668c71ac67"), "x" : 12 }
{ "_id" : ObjectId("503e38157d2cae668c71ac68"), "x" : 13 }
{ "_id" : ObjectId("503e38157d2cae668c71ac69"), "x" : 14 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6a"), "x" : 15 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6b"), "x" : 16 }
has more


> db.c.find().limit(3);
{ "_id" : ObjectId("503dd95a4ba09fc49a57808d"), "y" : null }
{ "_id" : ObjectId("503dd95e4ba09fc49a57808e"), "y" : 1 }
{ "_id" : ObjectId("503dd9624ba09fc49a57808f"), "y" : 2 }

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

> db.c.find().skip(3);
{ "_id" : ObjectId("503e38157d2cae668c71ac5b"), "x" : 0 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5c"), "x" : 1 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5d"), "x" : 2 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5e"), "x" : 3 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5f"), "x" : 4 }
{ "_id" : ObjectId("503e38157d2cae668c71ac60"), "x" : 5 }
{ "_id" : ObjectId("503e38157d2cae668c71ac61"), "x" : 6 }
{ "_id" : ObjectId("503e38157d2cae668c71ac62"), "x" : 7 }
{ "_id" : ObjectId("503e38157d2cae668c71ac63"), "x" : 8 }
{ "_id" : ObjectId("503e38157d2cae668c71ac64"), "x" : 9 }
{ "_id" : ObjectId("503e38157d2cae668c71ac65"), "x" : 10 }
{ "_id" : ObjectId("503e38157d2cae668c71ac66"), "x" : 11 }
{ "_id" : ObjectId("503e38157d2cae668c71ac67"), "x" : 12 }
{ "_id" : ObjectId("503e38157d2cae668c71ac68"), "x" : 13 }
{ "_id" : ObjectId("503e38157d2cae668c71ac69"), "x" : 14 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6a"), "x" : 15 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6b"), "x" : 16 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6c"), "x" : 17 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6d"), "x" : 18 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6e"), "x" : 19 }
has more

上面的操作会略过前三个匹配的文档,然后返回余下的文档。如果集合里面能匹配的文档少于3个,则不会返回任何文档。

sort用一个对象作为参数:一组键/值对,键对应文档的键名,值代表排序的方向。排序方向可以是1(升序)或者-1(降序)。如果指定了多个键,则按照多个键的顺序逐个排序。例如,要按照username升序及age降序排序,可以这样写:

> db.c.find().sort({"username":1,"age":-1})
{ "_id" : ObjectId("503dd95a4ba09fc49a57808d"), "y" : null }
{ "_id" : ObjectId("503dd95e4ba09fc49a57808e"), "y" : 1 }
{ "_id" : ObjectId("503dd9624ba09fc49a57808f"), "y" : 2 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5b"), "x" : 0 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5c"), "x" : 1 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5d"), "x" : 2 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5e"), "x" : 3 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5f"), "x" : 4 }
{ "_id" : ObjectId("503e38157d2cae668c71ac60"), "x" : 5 }
{ "_id" : ObjectId("503e38157d2cae668c71ac61"), "x" : 6 }
{ "_id" : ObjectId("503e38157d2cae668c71ac62"), "x" : 7 }
{ "_id" : ObjectId("503e38157d2cae668c71ac63"), "x" : 8 }
{ "_id" : ObjectId("503e38157d2cae668c71ac64"), "x" : 9 }
{ "_id" : ObjectId("503e38157d2cae668c71ac65"), "x" : 10 }
{ "_id" : ObjectId("503e38157d2cae668c71ac66"), "x" : 11 }
{ "_id" : ObjectId("503e38157d2cae668c71ac67"), "x" : 12 }
{ "_id" : ObjectId("503e38157d2cae668c71ac68"), "x" : 13 }
{ "_id" : ObjectId("503e38157d2cae668c71ac69"), "x" : 14 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6a"), "x" : 15 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6b"), "x" : 16 }
has more

这三个方法可以组合使用。这对于分页非常有用。例如,你有个在线商店,有人想搜索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,或者利用上次的结果来计算下一次查询。

1.不用skip对结果分页

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

//do not use:slow for large skips

> db.c.find().limit(10);
{ "_id" : ObjectId("503dd95a4ba09fc49a57808d"), "y" : null }
{ "_id" : ObjectId("503dd95e4ba09fc49a57808e"), "y" : 1 }
{ "_id" : ObjectId("503dd9624ba09fc49a57808f"), "y" : 2 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5b"), "x" : 0 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5c"), "x" : 1 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5d"), "x" : 2 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5e"), "x" : 3 }
{ "_id" : ObjectId("503e38157d2cae668c71ac5f"), "x" : 4 }
{ "_id" : ObjectId("503e38157d2cae668c71ac60"), "x" : 5 }
{ "_id" : ObjectId("503e38157d2cae668c71ac61"), "x" : 6 }
> db.c.find().skip(10).limit(10);
{ "_id" : ObjectId("503e38157d2cae668c71ac62"), "x" : 7 }
{ "_id" : ObjectId("503e38157d2cae668c71ac63"), "x" : 8 }
{ "_id" : ObjectId("503e38157d2cae668c71ac64"), "x" : 9 }
{ "_id" : ObjectId("503e38157d2cae668c71ac65"), "x" : 10 }
{ "_id" : ObjectId("503e38157d2cae668c71ac66"), "x" : 11 }
{ "_id" : ObjectId("503e38157d2cae668c71ac67"), "x" : 12 }
{ "_id" : ObjectId("503e38157d2cae668c71ac68"), "x" : 13 }
{ "_id" : ObjectId("503e38157d2cae668c71ac69"), "x" : 14 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6a"), "x" : 15 }


{ "_id" : ObjectId("503e38157d2cae668c71ac6b"), "x" : 16 }


> db.c.find().skip(20).limit(10);
{ "_id" : ObjectId("503e38157d2cae668c71ac6c"), "x" : 17 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6d"), "x" : 18 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6e"), "x" : 19 }
{ "_id" : ObjectId("503e38157d2cae668c71ac6f"), "x" : 20 }
{ "_id" : ObjectId("503e38157d2cae668c71ac70"), "x" : 21 }
{ "_id" : ObjectId("503e38157d2cae668c71ac71"), "x" : 22 }
{ "_id" : ObjectId("503e38157d2cae668c71ac72"), "x" : 23 }
{ "_id" : ObjectId("503e38157d2cae668c71ac73"), "x" : 24 }
{ "_id" : ObjectId("503e38157d2cae668c71ac74"), "x" : 25 }
{ "_id" : ObjectId("503e38157d2cae668c71ac75"), "x" : 26 }
>

然而,一般来讲可以找到一种方法实现不用skip的分页,这取决于查询本身。例如,要按照"date"降序显示文档。可以用如下方式获取结果的第一页:

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

> db.users.find().sort({"registered":-1}).limit(100);
{ "_id" : ObjectId("503b5937d2f71a75825a1b5b"), "emails" : [    "joe@example", "joe@gmail.com",         "joe@yahoo.com",        "joe@hotmail.com",      "joe@php.net",  "joe@example.com",      "joe@python.org" ], "id_num" : 6, "registered" : ISODate("2006-12-31T16:00:00Z"), "username" : "joe" }
{ "_id" : ObjectId("503b08dea53a93158463f02e"), "id_num" : 1, "ticket_no" : 725 }
{ "_id" : ObjectId("503b0d70a53a93158463f02f"), "id_num" : 2, "ticket_no" : 542 }
{ "_id" : ObjectId("503b0d78a53a93158463f030"), "id_num" : 3, "ticket_no" : 390 }
{ "_id" : ObjectId("503b0d7ca53a93158463f031"), "age" : 49, "id_num" : 4, "name" : "joe1", "winner" : true }
{ "_id" : ObjectId("503b338da53a93158463f033"), "age" : 30, "id_num" : 5, "location" : "Wisconsin", "name" : "joe", "sex" : "male" }

然后,可以利用最后一个文档中"date"的值作为查询条件,来获取下一页:

var latest = null;

//display first page

while(page1.hasNext()){

latest = page1.next();

display(latest);

}

//get next page

var page2 = db.foo.find({"date":{"$gt":latest.date}});

page2.sort({"date":-1}).limit(100);

2、随机选取文档

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

//do not use

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:

> db.people.find();
{ "_id" : ObjectId("503e45f77d2cae668c71acbf"), "name" : "joe", "random" : 0.8444473752020685 }
{ "_id" : ObjectId("503e45fe7d2cae668c71acc0"), "name" : "john", "random" : 0.04516828536238293 }
{ "_id" : ObjectId("503e46027d2cae668c71acc1"), "name" : "jim", "random" : 0.896214824631229 }

var random = Math.random();

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

也有偶尔遇到随机数比所有集合里面存着的随机值大的情况【还会有极少数相等的时候,$lte可能更加严谨】,这时就没有结果返回了。那就换个方向,这样就万事大吉了:

if(result==null){

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

}

> db.people.findOne({"random":{"$lte":random}})
{
"_id" : ObjectId("503e45fe7d2cae668c71acc0"),
"name" : "john",
"random" : 0.04516828536238293
}

要是集合里面本就没有文档,则会返回null,这说得通。

这种技巧还可以和其他各种复杂的查询一同使用,仅需要确保有包含随机键的索引即可。例如,想随机找一个加州的水暖工,可以对"profession"、"state"和"random"建立索引

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

这样就能很快得出一个随机结果了

4.5.3 高级查询选项

查询分包装的和普通的两类。普通的查询就像下面这个:

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

指定查询最多扫描的文档数量

$min:document

查询的开始条件

$max:document

查询的结束条件

$hint:document

指定服务器使用哪个索引进行查询

$explain:boolean

获取查询执行的细节(用到的索引、结果数量、耗时等),而并非真正执行查询

$snapshot:boolean

确保查询的结果是在查询执行那一刻的一致快照。

4.5.4 获取一致结果

数据处理通常的一种做法就是先把数据从MongoDB中取出来,然后经过某种变换,最后再存回去:

cursor = db.foo.find();

while(cursor.hasNext()){

var doc = cursor.next();

doc = process(doc);

db.foo.save(doc);

}

结果比较少时还好,文档较多的时候就玩不转了。

做查找的时候,从集合的开头返回结果,并向右移动。程序获取前100个文档并处理。当要将其保存回数据库时,如果文档体积增加而预留空间不足,则需要将其移动。通常会将其挪置集合的末尾处。应对这个问题的方法就是对查询进行快照。如果使用了”$snapshot“选项,查询就是针对不变的集合视图进行的。所有返回一组结果的查询实际上都进行了快照,不一致只在游标等待结果时集合内容被改变的情况下发生的。
本文出自 “坚持下去” 博客,请务必保留此出处http://zuoanlove.blog.51cto.com/1422941/1053773
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: