4.1 find简介
MongoDB中使用find来进行查询。查询就是返回一个集合中文档的子集,子集的范围从0个文档到整个集合。find的第一个参数决定了要返回那些文档,这个参数是一个文档,用于指定查询条件。
>db.c.find()
将批量返回集合c中的所有文档。
想要查找"age"值为27的所有文档,直接将这样的键/值对写进查询文档就好了:
>db.users.find({"age":27})
要是想匹配一个字符串,比如值为"joe"的"username"键,那么直接将键/值对写在查询文档中即可:
>db.users.find({"username":"joe"})
db.users.find({"username":"joe","age":27})
4.1.1 指定需要返回的键
如果只对用户集合的"username"和"email"键感兴趣,可以:
文档中有很多键,但是我们不希望结果中含有"fatal_weakness"键:
db.users.find({},{"fatal_weakness":0})
使用这种方式,也可以把"_id"键剔除掉:
>db.users.find({},{"username":1,"_id":0})
{
"username":"joe",
}
4.2 查询条件
4.2.1 查询条件
"$lt","$lte","$gt","$gte"就是全部的比较操作符,分别对应<,<=,>和>=。
查询18-30岁(含)的用户,就可以像下面这样:
>db.users.find({"age":{"$gte":18,"$lte":30}})
查找在2007年1月1日前注册的人,可以:
>start=new Date("01/01/2007")
>db.users.find({"registered":{"$lt":start}})
查询所有名字不为joe的用户,可以:
>db.users.find({"username":{"$ne";"joe"}})
4.2.2 OR查询
例如,抽奖活动的中奖号码是725、542、390.要找出全部的中奖文档的话,可以构建如下查询:
>db.raffle.find({"ticket_no":{"$in":[725,542,390]}})
>db.users.find({"user_id":{"$in":[12345,"joe]})
这会匹配"user_id"等于12345的文档,也会匹配"user_id"等于"joe"的文档。
查询返回所有没有中奖的人:
>db.raffle.find({"ticket_no":{"$nin":{752,542,390]}})
希望匹配到中奖的"ticket_no",或者"winner"键的值为true的文档,就可以这么做:
>db.raffle.find({"$or":[{"ticket_no":{"$in":[725,542,390]}},{"winner":true}]})
4.2.3 $not
>db.users.find({"id_num":{"$mod":[5,1]}})
上面的查询会返回"id_ num"值为1、6、11、16等的用户。
返回"id_num"为2,3,4,5,7,8,9,10,12等的用户,就要用"$not"了:
>db.users.find({"id_num":{"$mod":[5,1]}}})
4.2.4 条件语义
查找年龄为20-30的所有用户,可以在"age"键上使用"$gt"和"$lt":
>db.users.find({"age":{"$lt":30,"$gt":20}})
一个键可以有任意多个条件,但是一个键不能对应多个更新修改器。例如,修改器文档不能同时含有{"$inc":{"age":1},"$set":{age:40}},因为修改了"age"两次。但是对于查询条件句就没有这种限定。
>db.users.find({"$and":[{"x":{"$lt":1}},{"x":4}]})
这个查询会匹配那些"x"字段的值小于等于1并且等于4的文档。比如{"x":[0,4]},那么这个文档就与查询条件向匹配。如果把上面的查询改成下面这样,效率会更高:
>db.users.find({"x":{"$lt":1,"$in":[4]}})
4.3 特定类型的查询
> db.c.insert({"y":null})
WriteResult({ "nInserted" : 1 })
> db.c.insert({"y":1})
WriteResult({ "nInserted" : 1 })
> db.c.insert({"y":2})
WriteResult({ "nInserted" : 1 })
> db.c.find()
{ "_id" : ObjectId("5ab04d2791589385765220dd"), "y" : null }
{ "_id" : ObjectId("5ab04d2c91589385765220de"), "y" : 1 }
{ "_id" : ObjectId("5ab04d3191589385765220df"), "y" : 2 }
> db.c.find({"y":null})
{ "_id" : ObjectId("5ab04d2791589385765220dd"), "y" : null }
> db.c.find({"z":null})
{ "_id" : ObjectId("5ab04d2791589385765220dd"), "y" : null }
{ "_id" : ObjectId("5ab04d2c91589385765220de"), "y" : 1 }
{ "_id" : ObjectId("5ab04d3191589385765220df"), "y" : 2 }
如果仅想匹配键值为null的文档,既要检查该键的值是否为null,还要通过"$exists"条件判定键值已存在:
> db.c.find({"z":{"$in":[null],"$exists":true}})
4.3.2 正则表达式
查找所有名为Joe或者joe的用户,就可以使用正则表达式执行不区分大小写的匹配:
>db.users.find({"name":/joe/i})
匹配如"joey"这样的键:
>db.users.find({"name":/joey?/i})
> db.foo.find({"bar":/baz/})
{ "_id" : ObjectId("5ab04f3091589385765220e0"), "bar" : /baz/ }
4.3.3 查询数组
> db.food.insert({"fruit":["apple","banana","peach"]})
WriteResult({ "nInserted" : 1 })
> db.food.find({"fruit":"banana"})
{ "_id" : ObjectId("5ab04f9391589385765220e1"), "fruit" : [ "apple", "banana", "peach" ] }
这个查询好比我们对一个这样的(不合法)文档进行查询:{"fruit":"apple","fruit":"banana","fruit":"peach"}。
1.$all
> db.food.insert({"_id":1,"fruit":["apple","banana","peach"]})
WriteResult({ "nInserted" : 1 })
> db.food.insert({"_id":2,"fruit":["apple","kumquat","orange"]})
WriteResult({ "nInserted" : 1 })
> db.food.insert({"_id":3,"fruit":["cherry","banana","apple"]})
WriteResult({ "nInserted" : 1 })
要找到既有"apple"又有"banana"的文档,可以使用"$all"来查询:
> db.food.find({fruit:{$all:["apple","banana"]}})
{ "_id" : 1, "fruit" : [ "apple", "banana", "peach" ] }
{ "_id" : 3, "fruit" : [ "cherry", "banana", "apple" ] }
> db.food.find({"fruit":["apple","banana","peach"]})
{ "_id" : 1, "fruit" : [ "apple", "banana", "peach" ] }
> db.food.find({"fruit":["apple","banana"]})
> db.food.find({"fruit":["banana","apple","peach"]})
> db.food.find({"fruit.2":"peach"})
{ "_id" : 1, "fruit" : [ "apple", "banana", "peach" ] }
2.$size
> db.food.find({"fruit":{"$size":3}})
{ "_id" : 1, "fruit" : [ "apple", "banana", "peach" ] }
{ "_id" : 2, "fruit" : [ "apple", "kumquat", "orange" ] }
{ "_id" : 3, "fruit" : [ "cherry", "banana", "apple" ] }
得到一个长度范围内的文档是一种常见的查询。"$size"并不能与其他查询条件(比如"$gt")组合使用,但是这种查询可以通过在文档中添加一个"size"键的方式来实现。这样每一次向指定数组添加元素时,同时增加"size"的值。比如,原本这样的更新:
>db.food.uupdate(criteria,{"$push":{"fruit":"strawberry"}})
就要变成下面这样:
> db.food.update(criteria,
... {"$push":{"fruit":"strawberry"},"$inc":{"size":1}})
>db.food.find({"size":{"$gt":3}})
3.$slice操作符
假设现在有一个博客文章的文档,我们希望返回前10条评论,可以这样做:
>db.blog.posts.findOne(criteria,{"comments":{"$slice":10}})
也可以返回后10条评论,只要在查询条件中使用-10就可以了:
>db.blog.posts.findOne(criteria,{"comments":{"$slice":-10}})
>db.blog.posts.findOne(criteria,{"comments":{"$slice":[23,10]}})
这个操作会跳过前23个元素,返回第24-33个元素。如果数组不够33个元素,则返回第23个元素后面的所有元素。
用"$slice"来获取最后一条评论:
4.返回一个匹配的数组元素
返回与查询条件相匹配的任意一个数组元素。可以使用$操作符得到一个匹配的元素。对于上面的博客文章示例,可以用如下的方式得到Bob的评论:
5.数组和范围查询的相互作用
{"x":5}
{"x":15}
{"x":25}
{"x":[5,25]}
> db.test.find({"x":{"$gt":10,"$lt":20}})
{"x":15}
{"x":[5,25]}
>db.test.find({"x":{"$gt":10,"$lt"20}).min({"x":10}).max("x":20})
{"x":15}
4.3.4 查询内嵌文档
> db.people.insert({"name":{"first":"Joe","last":"Schmoe"},"age":45})
WriteResult({ "nInserted" : 1 })
> db.people.find()
{ "_id" : ObjectId("5ab05aaf91589385765220e2"), "name" : { "first" : "Joe", "last" : "Schmoe" }, "age" : 45 }
要查询姓名为Joe Schmoe的人可以这样:
> db.people.find({"name":{"first":"Joe","last":"Schmoe"}})
{ "_id" : ObjectId("5ab05aaf91589385765220e2"), "name" : { "first" : "Joe", "last" : "Schmoe" }, "age" : 45 }
>db.people.find({"name.first":"Joe","name.last":"Schmoe"})
要找到有Joe发表的5分以上的评论。
内嵌文档的匹配,必须要整个文档完全匹配。
>db.blog.find({"comments":{"$elemMathc":{"author":"joe","score":{"$gte":5}}}})
4.4 $where查询
> db.foo.insert({"apple":1,"banana":6,"peach":3})
WriteResult({ "nInserted" : 1 })
> db.foo.insert({"apple":8,"spinach":4,"watermelon":4})
WriteResult({ "nInserted" : 1 })
希望返回两个键具有相同值的文档。只能用"$where"子句借助JavaScript来完成了:
> db.foo.find({"$where":function(){
... for(var current in this){
... for(var other in this){
... if(current!=other&&this[current]==this[other]){
... return true;
... }
... }
... }
... return false;
... }});
{ "_id" : ObjectId("5ab05d7c91589385765220e4"), "apple" : 8, "spinach" : 4, "watermelon" : 4 }
服务器端脚本
在服务器上执行JavaScript时必须注意安全性。如果使用不当,服务器端JavaScript很容易受到注入攻击,与关系型数据库中的注入攻击类似。不过,只要在接受输入时遵循的一些规则,就可以安全地使用JavaScript。也可在运行mongod时指定--noscripting选项,完全关闭JavaScript执行。
例如,假如你希望打印一句"Hello,name!",这里的name是由用户提供的。
> func="function(){print('Hello,"+name+"!');}"
如果这里的name是一个用户定义的变量,它可能会是"');db.dropDatabase();print('"这样一个字符串,
>func="function(){pirnt('Hello,');db.dropDatabase();print('!');}"
如果执行这段代码,你的整个数据库就会删除!
为了避免这种情况,应该使用作用域来传递name的值。以Python为例:
func=pymongo.code.Code("function(){print('Hello,'+username+'!');}",{"username":name})
现在数据库会输出如下的内容,不会有任何风险:
Hello,');db.dropDatabase();print('!
4.5 游标
数据库使用游标返回find的执行结果。客户端对游标的实现通常能够对最终结果进行有效的控制。可以限制结果的数量,略过部分结果,根据任意键按任意顺序的组合对结果进行各种排序,或者是执行其他一些强大的操作。
先创建一个简单的集合,而后做个查询,并用cursor变量保存结果:
> for(i=0;i<100;i++){
... db.collection.insert({x:i});
... }
WriteResult({ "nInserted" : 1 })
> var cursor=db.collection.find();
遍历:
> while(cursor.hasNext()){
... obj=cursor.next();
... // do stuff
... }
游标类还实现了JavaScript的迭代器接口,所以可以在foreach循环中使用:
> var cursor=db.people.find();
> cursor.forEach(function(x){
... print(x.name)
... });
几乎游标对象的每个方法都返回游标本身,这样就可以按任意顺序组成方法链。例如,下面几种表达式时等价的:
>cursor.hasNext()
这时,查询被发送服务器。shell立刻获取前100个结果或者前4MB数据(两者之中较小者),这样下次调用next或者hasNext是就不必再次连接服务器取结果了。
4.5.1 limit,skip和sort
>db.c.find().limit(3)
要是匹配结果不到3个,则返回匹配数量的结果。limit指定的是上线,而非下线。
>db.c.find().skip(3)
上面的操作会略过前三个匹配的文档,然后返回余下的文档。如果集合里面能匹配的文档少于3个,则不会返回任何文档。
>db.c.find().sort({username:1,age:-1})
按照"username"升序及"age"降序排序。
>db.stock.find({"desc":"mp3"}).limit(50).sort({"price":-1})
搜索mp3,每页返回50个结果,而且按照价格从高到低排序.
点击"下一页"可以看到更多的结果,通过skip也可以非常简单地实现,只需要略过前50个结果就好了:
>db.stock.find({"desc":"mp3"}).limit(50).skip(50).sort({"price":-1})
比较顺序
MongoDB处理不同类型的数据有一定顺序的。有时一个键的值可能是多种类型的,优先级从小到大,其顺序如下:
(1)最小值;
(2)null;
(3)数字(整型、长整型、双精度)
(4)字符串;
(5)对象/文档
(6)数组;
(7)二进制数据;
(8)对象ID
(9)布尔型
(10)日期型
(11)时间戳
(12)正则表达式
(13)最大值
4.5.2 避免使用skip略过大量结果
用skip略过少量的文档还是不错的。但是要数量非常多的话,skip就会变得非常慢,因为要先找到需要被略过的数据,然后再抛弃这些数据。
1.不用skip对结果分页
不用skip的情况下实现分页,这取决于查询本身。例如,要按照"date"降序显示文档列表。可以用如下方式获取结果的第一页:
>var page1=db.foo.find().sort({"date":-1}).limit(100)
然后,可以利用最后一个文档中"date"的值作为查询条件,来获取下一页:
2.随机选取文档
这种选取随机文档的做法效率太低:首先得计算总数(要是有查询条件就会很费时),然后用skip略过大量结果也会非常耗时。
在插入文档时给每个文档都添加一个额外的随机键。
> db.people.insert({"name":"joe","random":Math.random()})
WriteResult({ "nInserted" : 1 })
> db.people.insert({"name":"john","random":Math.random()})
WriteResult({ "nInserted" : 1 })
> db.people.insert({"name":"jim","random":Math.random()})
WriteResult({ "nInserted" : 1 })
想要从集合中查找一个随机文档,只要计算一个随机数并将其作为查询条件就好了,完全不用skip:
> var random=Math.random()
> result=db.foo.findOne({"random":{"$gt":random}})
偶尔会遇到产生的随机数比集合中所有的随机值都大的情况,那就将条件操作符换一个方向:
> if(result==null){
... result=db.foo.findOne({"random":{"$lt":random}})
... }
例如,想在加州随机找一个水暖工,可以对"profession","state",和"random"建立索引:
>db.people.ensureIndex({"profession":1,"state":1,"random":1})
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
- $showDiskLoc:true
在查询结果中添加一个"$diskLoc"字段,用于显示该条结果在磁盘上的位置。例如:
文件号码显示了这个文档所在的文件。如果这里使用的是test数据库,那么这个文档就在test.2文件中.第二个字段显示的是该文档在文件中的偏移量。
4.5.4 获取一致结果
数据处理通常的做法就是先把数据从MongoDB中取出来,然后做一些变换,最后再存回去:
但是如果结果集比较大,MongoDB可能会多次返回同一个文档。
应对这个问题的方法就是对查询进行快照(snapshot)。如果使用了这个选项,查询就在"_id"索引上遍历执行,这样可以保证每个文档只被返回一次。
db.foo.find()改为:>db.foo.find().snapshot()
快照会使查询变慢,所以应该只在必要时使用快照。
4.5.5 游标声明周期
看待游标有两种角度:客户端的游标以及客户端游标表示的数据库游标。
在服务器端,游标消耗内存和其他资源。游标遍历尽了结果以后,或者客户端发来消息要求终止,数据库将会释放这些资源。释放的资源可以被数据库另做他用。
还有一种情况导致游标终止(随后被清理)。首先,游标完成匹配结果的迭代时,它会清除自身。另外,如果客户端的游标已经不再作用域内了,驱动程序会向服务器发送一条特别的消息,让其销毁游标。最后,游标会自动销毁。
4.6 数据库命令
删除集合是使用"drop"数据库命令完成的:
>db.test.drop()
数据库命令工作原理
数据库命令总会返回一个包含"ok"键的文档。如果"ok"是0,那么命令的返回文档就会有一个额外的键"errmsg"。
MongoDB中的命令被实现为一种特殊类型的查询,这些特殊的查询会在$cmd集合上执行。runCommand只是接受一个命令文档,并且执行与这个命令文档等价的插叙。
当MongoDB服务器得到一个在$cmd集合上的查询时,不会对这个查询进行通常的查询处理,而是会使用特殊的逻辑对其进行处理。几乎所有的MongoDB驱动程序都会提供一个类似runCommand的辅助函数,用于执行命令,而且命令总是能够以简单查询的方式执行。