MongoDb查询详解
查询符合条件的第一个文档(对于mongo来说不能叫记录了)
db.COLLECTION_NAME.findOne({},{});
查询符合条件的文档,并按照指定条件排序,跳过前面N1个文档,返回最多数量为N2的文档列表
sort skip limit三个函数可选
db.COLLECTION_NAME.find({},{}).sort({}).skip(N1).limit(N2);
返回条件的文档数量
db.COLLECTION_NAME.count({});
上面三行代码就是mongo的世界里查询语句的全部。
findOne find count sort的参数都包含在花括号里。用mongo的术语来说就是,它们的参数也是一个文档,而且值得一提的是,在mongo的世界里函数参数如果不是文档就是基本类型。在与查询有关的参数里就是skip和limit以整数为参数,一些与重命名集合、创建集合、分片的初始化与管理等函数以字符串为参数。其它绝大部分函数参数是是以文档形式传递。
find和findOne的第一个参数是查询条件,第二个参数是指定返回条例查询条件的那些文档里的哪些字段,第二个参数可省略。
mongo的文档也就是json对象。不过这些作为函数参数的文档,它们的属性名是有特殊含义的。
find和findOne的第一个参数:
第一个参数的属性名是要查询的文档内的属性名,属性的值就是针对这个属性名的查询条件。
假定我们有以下数据库:
use TEST;
//db.createCollection('USER');
db.USER.findOne({name:'Tom',password:'passkey'});
这行代码将从集合USER里面查询属性名有name和password,且值分别是'Tom'和‘passkey'的第一个文档
查询参数的属性值如果是简单类型的值,mongo就会像上文一样将查询条件按相等性处理。更复杂的查询将在后续文章介绍。
上文的findOne调用将会把符合条件的文档全部属性都返回,如果我们只需要文档中的一部分属性这个时候查询函数的第二个参数就派上用场了。
db.USER.findOne({name:'Tom',password:'passkey'},{name:1,registTime:1});
这行代码仍然会命中数据库内相同的文档,只是并不会返回被命中文档的全部属性,而是只返回name和registTime这两个属性。
之前提到过mongo不是模式严格的数据库,也就是说同一个集合内的文档不一定有相同的属性,有些文档可能包含更多的属性,同名的属性在不同文档里可能是不同的数据类型。
假如说USER集合里面有一部分老数据没有registTime,后来由于需求变更新用户都增加了registTime。
如果是在关系数据库里,肯定所有记录都增加了一个registTime字段,而且必须要首先修改数据表定义才行。不过,现在是mongo的世界了。我们可以在需求发生变化的时候,随时修改程序的代码而不必显式修改数据库模式的定义。这样势必就会造成同一个数据集合里有一部分旧数据没有新增的字段。这种情况下,那些没有registTime而又符合查询条件的文档会被怎么处理呢?答案就是这些文档依然会被返回,如果文档中有registTime就返回registTime,如果没有就不返回。换句话说,如果把数据库中的文档属性名看作数学意义上的集合,find*函数的第二个参数的属性名是另一个集合;那么返回的结果集中的文档属性名就是前面两个集合的交集。
find函数的两个参数与findOne完全一样。就不多说了。
值得一提的是,除非在find*函数的第二个参数显式指定_ID:0,否则_ID属性默认会包含在结果集中。
sort的参数
顾名思义,就是针对被命中文档排序。
它的参数是一个文档。文档属性名是要排序的属性,属性的值只能取1或-1,1表示升序,-1表示降序。
skip(N1)
它会跳过结果集的前N1个文档,但是它仍然会扫描这N1个文档,因此skip的效率很低。
为保证效率应在调用skip之前使结果尽量的小。
最好的做法是在find函数的第一个参数文档里指定属性,这些属性与sort函数的参数文档里的属性一样,如果是升序排序就指定返回这些属性的值大于上一页的最大值的文档,反之就指定返回它们小于上一页的最小值的文档。用这种方式代替skip。
limit(N2)
这是查询语句里面最后调用的函数,意思就是最多返回命中结果集里面的N2个文档。
关于复杂的查询条件,比如大于、小于、存在性、空值等比较将在下一篇文章里介绍。
mongo的查询条件甚至还支持正则表达式查询,可以在查询语句里嵌入javascript代码,支持嵌套子文档和数组索引的查询条件等。它也可针对嵌套子文档和数组创建索引。
上面提到的查询都是等值条件查询,但是我们更多的时候需要模糊查询、非等值查询、模式匹配等。mongo不是key-value存储,它支持非常灵活复杂的查询方式,甚至比rdbms还要灵活的多,当然也复杂的多。
另外,需要多说一点,用nosql归类这些数据库并不准确,只是RDBMS都是用SQL的,而它们都是不用SQL的,所以就用nosql来归类这些数据库了。其实从技术上考虑,完全可以实现一个非RDBMS而继续使用SQL的全部特性来操作和管理数据库。当然为了表达方便这一系列文章仍然使用nosql这一并不准确的名词。
既然没有了sql,要操作mongo自然就要使用其它的方式了。前面的文章都已经出现过一些了,就是用mongo定义的数据库操作api配合它的文档形式的操作参数完成数据库的创建、修改、删除和数据的增删改查。
关于查询的参数在上一篇几乎已经说完,还剩下的就是find*和count的第一个参数。
由于find*的第一个参数和count参数都一样,本文就只以find函数做说明。
查询某个字段比指定值小:$lt
//假设存在集合USERdb.USER.find({REGIST_DATE:{$lt:new Date(2013,0,1)}});
/*前面提到过mongo完全遵守JAVASCRIPT语法,在JAVASCRIPT里面,月份是从0开始的,即上面的查询是查询2013-1-1以前注册的的用户。*/
查询某个字段比指定值大:$gt
db.USER.find({REGIST_DATE:{$gt:new Date(2013,0,1)}});
/*$lt表示小于,表示大于的自然就是$gt了*/
大于等于:$gte 小于等于:$lte
db.USER.find({REGIST_DATE:{$gte:new Date(2013,0,1),$lte:new Date(2013,0,31)}});
/*关于这个时间的问题看起来似乎有些别扭哈,没办法啦,MONGO就是这样啦。习惯就好啦。*/
/*上面的一行查询就是针对REGIST_DATE的组合查询形式,表示查询出所有在2013-1-1到2013-1-31注册的用户*/
接下来的查询方式就比较复杂了
正则表达式,mongo里面没有类似sql的like特性,不过可以用正则表达式代替
使用正则表达式查询有两种情况,在支持正则表达式字面值(标量)的语言里可以直接使用正则表达式字面值,比如RUBY NODEJS等。
db.USER.find({NAME:/^run/i});//以javascript为例,这个查询出所有用户名以run开头的用户,且不分大小写
像JAVA这样不支持正则表达式标量的语言怎么办呢?就有些麻烦了,需要借助MONGO api完成从字符串到正则表达式的转化。
db.USER.find(NAME:{$regex:'^run',$options:'i'});//这行命令完成跟上一行一样的工作。
其中,$options是正则表达式的选项,它一共有三个字母的任意组合可选,这三个字母分别是g i m,这三个字母可以任意组合表达不同的意义。
g:表示针对整个字符串做匹配,如果不加正则表达式在匹配到第一个符合的子串时就返回了。(global)
i:忽略大小写(insenssitive)
m:一个字符串内如果存在换行符,将作为多行模式匹配(multiple)
除了i以外其它两个选项在查询的时候恐怕用不到。
$in---相当于sql的in,它可以利用索引
db.USER.find(NAME:{$in:['tom','jerry']});
/*如果为NAME字段创建了索引,它就会从索引里面查找*/
/*mongo是区分大小写的,所以集合和文档属性的名字必须与创建它们的时候一致。*/
/*也就是NAME和name是两个不同的属性,它们可以同时存在于一个文档内*/
/*在一个集合内如果即存在NAME属性的文档,又存在name属性的文档,那么上面的那条命令不会把name属性查询出来*/
$nin---$in的相反操作,相当于sql的not in
db.USER.find(NAME:{$nin:['tom','jerry']});
注意:$nin不会利用索引,也就是说上面的命令$nin不会使用针对NAME属性的索引。
$all---没有sql类似的特性与之类比了,它的意义在于:查询条件是一个简单值数组,只有返回属性满足数组内的所有值的文档。这种查询条件只有在属性值是一个数组的情况下。
以我的这篇博文为例。要查询出所有含有nosql和mongo这两个标签的文档可以这么做
假设iteye要把数据库迁移到mongo,博客文章的数据模型就可以这样定义
首先定义一个名为blog的集合。
这篇blog可以如下方式存储
{
_ID:ObjectID(............),
subject:'mongo简介——查询(续)',
category:'database',
user_category:['nosql','mongo'],
content:'.............',
tags:['nosql','mongo'],
origrinal:true,
pub:'blog'
}
/*origrinal表示是否原创;由于没有附件,本文的文档就不包含附件属性,由于我不知道iteye如何定义频道,我就用字符串表示了*/
下面如果要查询包含‘nosql’标签的所有博文
db.blog.find(tags:'nosql');//这样就可以了
下面要查询同时包含'nosql'和'mongo'这两个标签的博文
db.blog.find(tags:{$all:['nosql','mongo']});
如果有的博文除了包含'nosql'和'mongo'标签,还包含'MONGO' 'Mongo' 'mongodb' 'MongoDB' ‘NOSQL’等标签,上面的那行命令也会一起返回。
如果要进行忽略大小写的查询,我又不想使用正则表达式做模糊匹配该怎么办呢?
答案是不能。
而这样的需求还是很常见的,那么惟一的做法就是,在用户保存博文的时候,程序根据以前已经存在的标签找出相似词,自动创建几个可能会出现的不同大小写的标签。比如我保存这篇文章的时候程序再自动创建上面提到的那几个我没有指定的标签。
$ne---不等性比较,它接收单值或数组
db.blog.find(tags:{$ne:'nosql'});//返回所有不包含nosql标签的博文
db.blog.find(tags:{$ne:['nosql','mongo']});//返回所有不包含nosql和mongo这两个标签的博文
$size---检查一个数组的尺寸,不利用索引,不能做范围查询,以后可能会增加这方面的支持
有时iteye会给用户快递一些奖品,iteye可能会把用户曾经填过的地址保存下来。可以这么保存
在USER集合里增加一个address属性,没有留过地址的可以没有。
一个用户不一定只有一个地址,这个address就可以创建为一个字符串数组。
比如要返回所有只留了一个地址的用户。
db.USER.find(address:{$size:1});
有些时候,如果ITEYE想要知道用户更详细的地址信息,就要用更复杂的文档保存地址。比如:
{
_ID:ObjectID(.........),
accountName:'runfriends',
address:[{category:'home',city:'北京',district:'东城',street:'.....'},
{category:'company',city:'北京',district:'海淀',street:'........'}]
}
如果要查出所有家在北京的用户,要怎么做呢?
可能会这样写:
db.USER.find({'address.category':'home','address.city':'北京'})。
但是这样是错的!这行命令的意义是查询出所有地址里面分类包含home,而且地址所在城市包含北京的用户。有些用户的公司地址在北京,而家庭地址不是,这些用户也会被匹配到。
那么接下来就用到了$elemMatch
它只在需要匹配子文档的多个属性时才会用到
db.USER.find(address:{$elemMatch:{category:'home',city:'北京'}});
$not---取反值,只有在没有指定相反操作时才需要用到它。因为绝大部分操作符都有对应的相反操作,所以应当尽量使用相反操作符,比如正则表达式匹配没有相反操作
假如有一天,ITEYE只允许用户名以字母开头了就可以把所有不以字母开头的用户查出来给他们发一封邮件,让他们改名。
db.USER.find(accountName:{$not:/^[a-zA-Z]/})
当然$not也接收单值,但是不建议使用
$exists----检查某个属性的存在性。
比如要把所有包含附件的博文查询出来。
db.blog.find({attachment:{$exists:true}});
没有附件的就是db.blog.find({attachment:{$exists:false}});
或才可以这样做:
db.log.find({attachment:null});//不存在
db.blog.find({attachment:{$ne:null}});//存在
前面介绍BSON的时候说过空值使用nil,但是这里却用了null,是因为nil是BSON的定义,这里是JAVASCRIPT的语法
$mod-----求余数,不利用索引
假如说某天ITEYE心血来潮要给所有年龄能被4整队的用户快递一份奖品。
db.USER.find({age:{$mod:[4,0]}});//数组的第一个值是除数,第二个值是期望的余数
$type---以属性类型查询
虽然不建议在同一集合的不同文档相同属性名保存着不同类型的数据,但是有时由于程序bug或设计不严谨可能会出现这种情况。现在要把这种情况找出来,比如_ID属性有的是ObjectID类型,有的是整数。下面把所有_ID是字符串的文档找出来。
db.USER.find(_id:{$type:2})
db.USER.find(_id:{$not:{$type:7}});//把所有主键ObjectID类型的文档找出来
$or $and----逻辑运算
它们的意义就不多解释了。不过它们的用法非常有意思
比如找出所有月收入在20000以上或2000以下的用户
db.USER.find({$or:[{salary:{$gt:20000}},{salary:{$lt:2000}}]});
找出所有月收入在8000以上,20000以下的用户
db.USER.find({$and:[{salary:{$gte:8000}},{salary:{$lte:20000}}]});
查询嵌套文档和数组元素
前面的内容已经简单介绍一些嵌套文档和数组元素的查询。嵌套文档和数组的查询遵守相同的语法规则
1. 它们都采用点号分割嵌套文档的属性,如果是数组的索引就用从0开始的数字表示。
db.USER.find({'address.category':'home'});//这个是查出所有留了家庭地址的用户
如果想知道用户留下的第一个地址是不是家庭地址可以这么做:
db.USER.find({'address.0.category':'home'});
那么如果想只返回留了家庭地址的用户而又只返回家庭地址却不返回其它的地址该怎么做呢?
目前采用的数据模型恐怕做不到这一点,如果有这样的需求,恐怕只能为不同的地址定义不同的字段了。
当然如果代码规范规定第一个地址必须是家庭地址,那么可以这样做:
db.USER.find({'address.0.category':'home'},{'accountName':1,'address.0':1});
不过通常情况下,把第一个地址定义为默认地址更好一些。
$where-----接收一段javascript代码作为查询条件,不利用索引
假如说要查询所有闰年出生的用户
db.USER.find({$where:'var year=birthday.getFullYear();return year%4==0 && year%100>0 || year%400==0';});
或:
db.USER.find($where:'function(){var year=this.birthday.getFullYear();return year%4==0 && year%100>0 || year%400==0'}');
好了,关于mongo的查询方式就这么多了。