转自:http://www.2cto.com/database/201302/191101.html
mongo简介——查询(续)
mongo简介——查询
http://www.2cto.com/database/201302/190196.html
这一篇继续说一些查询相关的问题。
上一篇提到的查询都是等值条件查询,但是我们更多的时候需要模糊查询、非等值查询、模式匹配等。mongo不是key-value存储,它支持非常灵活复杂的查询方式,甚至比rdbms还要灵活的多,当然也复杂的多。
www.2cto.com
另外,需要多说一点,用nosql归类这些
数据库并不准确,只是RDBMS都是用SQL的,而它们都是不用SQL的,所以就用nosql来归类这些数据库了。其实从技术上考虑,完全可以实现一个非RDBMS而继续使用SQL的全部特性来操作和管理数据库。当然为了表达方便这一系列文章仍然使用nosql这一并不准确的名词。
既然没有了sql,要操作mongo自然就要使用其它的方式了。前面的文章都已经出现过一些了,就是用mongo定义的数据库操作api配合它的文档形式的操作参数完成数据库的创建、修改、删除和数据的增删改查。
关于查询的参数在上一篇几乎已经说完,还剩下的就是find*和count的第一个参数。
由于find*的第一个参数和count参数都一样,本文就只以find函数做说明。
www.2cto.com
查询某个字段比指定值小:$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代码作为查询条件,不利用索引
假如说要查询所有闰年出生的用户 www.2cto.com
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的查询方式就这么多了。下一篇将要简单说下聚合。
mongo真是个神奇的数据库,它还能很方便的完成很多数据分析工作。很多使用mongo的公司也确实在用它做数据分析。