MongoDB中find()函数返回一个游标,客户端通过对游标进行一些设置就能对查询结果进行有效地控制,如可以限制查询得到的结果数量、跳过部分结果、或对结果集按任意键进行排序等!我们之前在Shell中进行操作,都是直接使用find()函数,并没有使用其返回值,如:
- > for(var i=0; i<100; i++){
- … db.coll.insert({”x” : i});
- … }
- > db.coll.find();
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf89”), “x” : 25 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf8a”), “x” : 26 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf8b”), “x” : 27 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf8c”), “x” : 28 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf8d”), “x” : 29 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf8e”), “x” : 30 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf8f”), “x” : 31 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf90”), “x” : 32 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf91”), “x” : 33 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf92”), “x” : 34 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf93”), “x” : 35 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf94”), “x” : 36 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf95”), “x” : 37 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf96”), “x” : 38 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf97”), “x” : 39 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf98”), “x” : 40 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf99”), “x” : 41 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf9a”), “x” : 42 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf9b”), “x” : 43 }
- { ”_id” : ObjectId(“5023997e1ed370450fbdcf9c”), “x” : 44 }
- has more
- >
> for(var i=0; i<100; i++){
... db.coll.insert({"x" : i});
... }
> db.coll.find();
{ "_id" : ObjectId("5023997e1ed370450fbdcf89"), "x" : 25 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf8a"), "x" : 26 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf8b"), "x" : 27 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf8c"), "x" : 28 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf8d"), "x" : 29 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf8e"), "x" : 30 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf8f"), "x" : 31 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf90"), "x" : 32 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf91"), "x" : 33 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf92"), "x" : 34 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf93"), "x" : 35 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf94"), "x" : 36 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf95"), "x" : 37 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf96"), "x" : 38 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf97"), "x" : 39 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf98"), "x" : 40 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf99"), "x" : 41 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf9a"), "x" : 42 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf9b"), "x" : 43 }
{ "_id" : ObjectId("5023997e1ed370450fbdcf9c"), "x" : 44 }
has more
>
我们先通过JavaScript脚本向集合中填充100条文档,然后直接调用find函数。其会自动递归find返回的游标,将前20条数据展示在shell中。如果我们通过变量保留find函数的返回值,其不会自动进行遍历显示操作:
- > var cursor = db.coll.find();
- >
> var cursor = db.coll.find();
>
这样做,实际发生的是,调用完find后,此时Shell并不会去真正地访问数据库,而是等待开始要求获得结果的时候才向数据库发送查询请求!我们此时可以对这个游标进行各种设置,然后调用游标的hashNext()或next()方法,这样就会真正访问数据库,这是一个懒加载的过程。如下:
- > var cursor = db.coll.find();
- > while(cursor.hasNext()){
- … var doc = cursor.next();
- … // do stuff with doc
- … };
- >
> var cursor = db.coll.find();
> while(cursor.hasNext()){
... var doc = cursor.next();
... // do stuff with doc
... };
>
上述代码中,当调用cursor.hasNext()时,查询被发往数据库,默认会返回前100条文档或者前4M的数据(两者之中较小的),这样下次next或hasNext都是本地调用了!当这组数据被遍历完毕,hasNext会导致再次去访问数据库,直到所有结果被返回!
【游标的操作】
上面提到了,当获得游标后,我们可以先对游标进行处理后,再让访问数据库的动作按照我们的意愿发生。这里有3个函数可以在处理游标时使用:limit、skip、sort。limit是限制游标返回的数量,指定了上限;skip是忽略前面的部分文档,如果文档总数量小于忽略的数量,则返回空集合;sort对得到的子集合进行排序,可以按照多个键进行正反排序!对游标的操作有一个技巧就是,操作游标的函数返回的都是游标,所以可以组成方法链调用,如下:
- > db.fruitprice.find();
- { ”_id” : ObjectId(“50226b4c3becfacce6a22a5b”), “apple” : 10, “banana” : 6, “pear” : 3 }
- { ”_id” : ObjectId(“50226ba63becfacce6a22a5c”), “apple” : 10, “watermelon” : 3, “pear” : 3 }
- { ”_id” : ObjectId(“5023a1db7dceac1a6dacb0b7”), “apple” : 8, “orange” : 4, “tomato” : 3 }
- { ”_id” : ObjectId(“5023a1eb7dceac1a6dacb0b8”), “apple” : 9, “orange” : 5, “grape” : 12 }
- { ”_id” : ObjectId(“5023a2037dceac1a6dacb0b9”), “melon” : 7, “orange” : 3, “grape” : 11 }
- > db.fruitprice.find().sort({”apple”:1, “banana”:-1});
- { ”_id” : ObjectId(“5023a2037dceac1a6dacb0b9”), “melon” : 7, “orange” : 3, “grape” : 11 }
- { ”_id” : ObjectId(“5023a1db7dceac1a6dacb0b7”), “apple” : 8, “orange” : 4, “tomato” : 3 }
- { ”_id” : ObjectId(“5023a1eb7dceac1a6dacb0b8”), “apple” : 9, “orange” : 5, “grape” : 12 }
- { ”_id” : ObjectId(“50226b4c3becfacce6a22a5b”), “apple” : 10, “banana” : 6, “pear” : 3 }
- { ”_id” : ObjectId(“50226ba63becfacce6a22a5c”), “apple” : 10, “watermelon” : 3, “pear” : 3 }
- > db.fruitprice.find().skip(1).limit(3).sort({”apple”:1, “banana”:-1});
- { ”_id” : ObjectId(“5023a1db7dceac1a6dacb0b7”), “apple” : 8, “orange” : 4, “tomato” : 3 }
- { ”_id” : ObjectId(“5023a1eb7dceac1a6dacb0b8”), “apple” : 9, “orange” : 5, “grape” : 12 }
- { ”_id” : ObjectId(“50226b4c3becfacce6a22a5b”), “apple” : 10, “banana” : 6, “pear” : 3 }
- >
> db.fruitprice.find();
{ "_id" : ObjectId("50226b4c3becfacce6a22a5b"), "apple" : 10, "banana" : 6, "pear" : 3 }
{ "_id" : ObjectId("50226ba63becfacce6a22a5c"), "apple" : 10, "watermelon" : 3, "pear" : 3 }
{ "_id" : ObjectId("5023a1db7dceac1a6dacb0b7"), "apple" : 8, "orange" : 4, "tomato" : 3 }
{ "_id" : ObjectId("5023a1eb7dceac1a6dacb0b8"), "apple" : 9, "orange" : 5, "grape" : 12 }
{ "_id" : ObjectId("5023a2037dceac1a6dacb0b9"), "melon" : 7, "orange" : 3, "grape" : 11 }
> db.fruitprice.find().sort({"apple":1, "banana":-1});
{ "_id" : ObjectId("5023a2037dceac1a6dacb0b9"), "melon" : 7, "orange" : 3, "grape" : 11 }
{ "_id" : ObjectId("5023a1db7dceac1a6dacb0b7"), "apple" : 8, "orange" : 4, "tomato" : 3 }
{ "_id" : ObjectId("5023a1eb7dceac1a6dacb0b8"), "apple" : 9, "orange" : 5, "grape" : 12 }
{ "_id" : ObjectId("50226b4c3becfacce6a22a5b"), "apple" : 10, "banana" : 6, "pear" : 3 }
{ "_id" : ObjectId("50226ba63becfacce6a22a5c"), "apple" : 10, "watermelon" : 3, "pear" : 3 }
> db.fruitprice.find().skip(1).limit(3).sort({"apple":1, "banana":-1});
{ "_id" : ObjectId("5023a1db7dceac1a6dacb0b7"), "apple" : 8, "orange" : 4, "tomato" : 3 }
{ "_id" : ObjectId("5023a1eb7dceac1a6dacb0b8"), "apple" : 9, "orange" : 5, "grape" : 12 }
{ "_id" : ObjectId("50226b4c3becfacce6a22a5b"), "apple" : 10, "banana" : 6, "pear" : 3 }
>
上述,共执行了3次查询:
第一次只执行了find函数,返回了一个集合,没有顺序!
第二次,我们让其按照键”apple” 键”banana”排序,键”apple”升序(>0的数字),键“banana”降序(<0的数字),即先按找键“apple”升序排,对于键“apple”相等的文档,则按照键“banana”降序排!我们看到,按照这种方式排,没有键“apple”(即键“apple”值为null)的文档排在了第一位,这是在mongodb中,针对相同键不同类型值有一个默认顺序,我们后面会提到!
第三次,我们使用了三个函数来设置游标。这三个函数的关系是,在数据库服务器端,先执行sort,然后再排好序的文档上执行skip,最后按照limit设定的最大数量返回文档子集即可。
【相同键不同类型值的比较顺序】
按照键排序时,MongoDB中对于键并不会强制其值是什么类型,我们在实际中也会遇到同一个键,一个文档中为串在另一个文档中为数字,这种文档在排序时是任何进行的呢?MongoDB中,有一个预先定义的顺序,从小到大,依次为:
(1):最小值
(2):null
(3):数字(整型,长整型,双精度)
(4):字符串
(5):对象/文档
(6):数组
(7):二进制数据
(8):对象ID
(9):布尔值
(10):日期型
(11):时间戳
(12):正则表达式
(13):最大值
在上述第二种查询中,我们按键“apple”查,在一个文档中缺少这个键,即在这个文档中这个键的值为null,在其他文档中该键的值都是数字,按照上述顺序,缺少这个键的文档按升序排理应排在前面!
【避免使用skip略过大量结果】
使用skip略过少量文档效率不会有什么影响,如果略过大量结果,则可能会产生性能瓶颈!对于skip,我们通常的应用可能是在分页时!对于分页,我们有两种方式来应对:
1. 将分页的处理放在应用层,即将数据全部查出,然后在应用层处理分页显示!这就是通常所说的伪分页!
2. 如果分页必须在数据库端进行,这通常是数据量太大的情况!这时,我们先尝试使用skip操作,如果出现性能瓶颈,我们只能根据一个排序键,在获取下页数据时,首先根据上一页最后一个文档中该键的值来查询文档,最后排序截取即可!这样就可以避免使用skip!
【高级查询选项】
查询分为普通查询和包装查询,我们上面演示的各种查询方式都是普通查询, 如下我们再演示一个:
- > db.fruitprice.find({“apple”:10}).sort({“banana”:1});
- { ”_id” : ObjectId(“50226ba63becfacce6a22a5c”), “apple” : 10, “watermelon” : 3, “pear” : 3 }
- { ”_id” : ObjectId(“50226b4c3becfacce6a22a5b”), “apple” : 10, “banana” : 6, “pear” : 3 }
- >
> db.fruitprice.find({"apple":10}).sort({"banana":1});
{ "_id" : ObjectId("50226ba63becfacce6a22a5c"), "apple" : 10, "watermelon" : 3, "pear" : 3 }
{ "_id" : ObjectId("50226b4c3becfacce6a22a5b"), "apple" : 10, "banana" : 6, "pear" : 3 }
>
上述查询我们转换为包装形式的写法是:
- > db.fruitprice.find({“query"</span><span> : {</span><span class="string">"apple"</span><span> : 10}, </span><span class="string">"orderby” : {“banana” : 1}});
- { ”_id” : ObjectId(“50226ba63becfacce6a22a5c”), “apple” : 10, “watermelon” : 3, “pear” : 3 }
- { ”_id” : ObjectId(“50226b4c3becfacce6a22a5b”), “apple” : 10, “banana” : 6, “pear” : 3 }
- >
> db.fruitprice.find({"$query" : {"apple" : 10}, "$orderby" : {"banana" : 1}});
{ "_id" : ObjectId("50226ba63becfacce6a22a5c"), "apple" : 10, "watermelon" : 3, "pear" : 3 }
{ "_id" : ObjectId("50226b4c3becfacce6a22a5b"), "apple" : 10, "banana" : 6, "pear" : 3 }
>
我们的所有查询在发送到数据库端时,都被提前转换成了包装形式!包装形式就是额外使用了一些键,如上述的” query"," orderby”。我们还有如下一些有用的键可用:
1. $maxscan : integer 指定查询时最多扫描文档的数量
2. $min : document 查询的开始条件
3. $max : document 查询的结束条件
4. $hint : document 指定服务器使用哪些索引进行查询
5. $explain : boolean 获取查询细节,如用到的索引,结果数量,耗时等,类似于关系数据库这边查看执行计划。并不会真正执行查询
6. $snapshot : boolean 确保查询的结果是在查询执行那一刻的一致快照!这个在后面还会提到!
【获取一致结果】
我们从MongoDB中获取到数据后,通常会执行这种操作:对文档进行处理后,即时地更新到数据库中!这时对于大量文档的情况有可能产生一个问题,我来描述一下:前面提到了,我们从数据库端调用游标的hasNext时,数据库默认会返回100条文档给我们,我们开始操作。假设对于一个文档,我们增大了其大小,并且超高了MongoDB为文档设置的预留区域,这时我们将这条文档更新到数据库中,数据库没法将其放置在其原始位置上,只能将其移动,通常会移动到集合末尾!这样我们再次获取文档时,有可能又得到这条以被修改的文档!!
应对这个问题,我们的方法就是对查询结果进行快照!如果使用了上面提到的“$snapshop”选项,查询就是针对不变的集合视图运行的!这点我们只是描述一下,实际情况中可以不用担心了,因为MongoDB中,所有返回一组的查询实际都进行了快照!