内置函数
MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。
聚合框架是MongoDB的高级查询语言,允许我们通过转化合并由多个文档的数据来生成新的在单个文档里不存在的文档信息。通俗一点来说,可以把MongoDB的聚合查询等价于SQL的GROUP BY语句。
测试数据
db.coll.insertMany([
{"name":"zhangsan", "age":20, "gender":"male", "birth":new ISODate('2000-01-01')},
{"name":"lisi", "age":21, "gender":"male", "birth":new ISODate('1999-02-01')},
{"name":"wangwu", "age":20, "gender":"female", "birth":new ISODate('2000-01-01')},
{"name":"zhaoliu", "age":22, "gender":"female", "birth":new ISODate('1998-01-01')}]);
1 aggregate函数
MongoDB中聚合的方法使用aggregate()。
语法:
db.集合名称.aggregate(<agg_options>)
参数解释:
agg_options:数组类型参数,传入具体的聚合表达式要求,来计算聚合结果。此参数代表聚合规则,如计算总和、平均值、最大最小值等。在MongoDB的聚合查询中,操作复杂度都在这里。
2 求和 $sum
统计coll集合中所有文档age字段的总和:
db.coll.aggregate([{"$group":{"_id":null, "sum_age":{"$sum":"$age"}}}])
语法解释:
- $group:分组,代表聚合的分组条件
- _id:分组的字段。相当于SQL分组语法group by column_name中的column_name部分。如果根据某字段的值分组,则定义为_id:’$字段名’。所以此案例中的null代表一个固定的字面值’null’。
- sum_age:返回结果字段名。可以自定义,类似SQL中的字段别名。
- $sum:求和表达式。相当于SQL中的sum()。
- $age:代表文档中的age字段的值。此案例中的$sum:’$age’代表求文档中age字段的和。如果案例定义为$sum:‘age’,代表求age字面值的和。在MongoDB中,字符串是不会进行数学相关运算的。得到的求和结果一定是0。
3 统计文档数量
聚合统计coll集合中文档数量:
db.coll.aggregate([{$group:{_id:null, count:{$sum:1}}}]);
统计coll集合中,相同年龄的文档数量:
db.coll.aggregate([{"$group":{"_id":"$age", "count":{"$sum":1}}}])
语法解释:
_id:’$age’ : 根据age字段的值进行分组。
doc_num:{$sum:1} : 求和计算。每个符合条件的文档计数为1,进行累加求和。返回的结果作为字段doc_num的值。
4 条件筛选
统计coll集合中,年龄小于22的文档数量:
db.coll.aggregate([{"$match":{"age":{"$lt":22}}}, {"$group":{"_id":null, "count":{"$sum":1}}}])
语法解释:
$match:匹配条件,相当于SQL中的where子句,代表聚合之前进行条件筛选。
{‘age’:{$lt:21}}:age字段小于21。
统计coll集合中,相同年龄的文档数量,且要求统计结果中,同年龄的文档数量大于1:
db.coll.aggregate([{"$group":{"_id":"$age", "count":{"$sum":1}}}, {"$match":{"count":{"$gt":1}}}])
语法解释:
{$group:{_id:’$age’, “count”:{$sum:1}}}:统计每个age字段值的文档数量。
{$match:{“count”:{$gt:1}}}:匹配条件,相当于SQL中的having子句。代表聚合之后进行条件筛选,只能筛选聚合结果,不能筛选聚合条件。返回nums字段值大于1的结果。
注意:
$match表达式定义的位置不同,代表的筛选时机不同,根据具体情况,合理的定义$match表达式,可以有效提升聚合查询效率。尽可能先筛选过滤更多的无效数据,再group分组,效率最好。
5 最大值 $max
查询coll集合中age字段最大的文档:
db.coll.aggregate([{"$group":{"_id":null, "max_age":{"$max":"$age"}}}])
6 最小值 $min
查询coll集合中birth字段最小的文档:
db.coll.aggregate([{"$group":{"_id":null, "min_age":{"$min":"$age"}}}])
7 平均值 $avg
查询coll集合中所有文档age字段的平均值:
db.coll.aggregate([{"$group":{"_id":null, "avg_age":{"$avg":"$age"}}}])
8 字符串拼接
‘$concat’:[’$字段名’, ‘固定字符串值’, ‘其他字符串类型字段或固定字段值’]
拼接coll集合中,name字段和gender字段
db.coll.aggregate([{"$project":{"all_info":{"$concat":["$name"," - ","$gender"]}}}])
$project - 管道,进行字符串拼接处理,日期处理等操作的函数
{“all_info”:{"$concat":["$name"," -","$gender"]}}
all_info:定义别名,处理后的结果的别名
“$concat”:["$name"," - “,”$gender"]
$concat :字符串拼接表达式(函数),对应的参数值是一个数组,数组中的每个元素就是要拼接的字符串,“字面值"或”$字段名"。
9 字符串转大写
'$toUpper':'$字段名'
db.coll.aggregate([{"$project":{"upperName":{"$toUpper":"$name"}}}])
10 字符串转小写
'$toLower':'$字段名'
db.coll.aggregate([{"$project":{"lowerGender":{"$toLower":"$gender"}}}])
姓名转大写,性别转小写:
db.coll.aggregate([{"$project":{"lowerGender":{"$toLower":"$gender"}, "upperName":{"$toUpper":"$name"}}}])
11 截取字符串
'$substr':['$字段名',起始下标,截取长度]
db.coll.aggregate([{"$project":{"subName":{"$substr":["$name", 0, 3]}}}])
12 日期格式化
db.agg.aggregate([{'$project':{'str':{'$dateToString':{'format':'%Y年%m月%d日 %H:%M:%S', 'date':'$birth'}}}}]);
{’$dateToString’:{‘format’:"%Y年%m月%d日 %H:%M:%S",date:"$birth"}}:使用MongoDB中的日期占位表达式格式化birth字段的数据。%Y年、%m月、%d日、%H
24小时制、%M分钟、%S秒。
具体表达式含义如下:
运算符
在MongoDB中,数学类型(int/long/double)和日期类型(date)可以做数学运行。日期只能做加减。日期加减单位是毫秒。因为MongoDB底层记录日期的方式是:1970-01-01 00:00:00.000到日期所在时间经历的毫秒数。
1 加法
查询agg集合中数据,显示name和age字段,并为age字段数据做加1操作:
db.agg.aggregate([{$project:{name:1, new_age:{$add:['$age', 1]}}}]);
2 减法
查询agg集合中数据,显示name和age字段,并为age字段数据做减1操作:
db.agg.aggregate([{$project:{name:1, new_age:{$subtract:['$age', 1]}}}]);
3 乘法
查询agg集合中数据,显示name和age字段,并为age字段数据做乘2操作:
db.agg.aggregate([{$project:{name:1, new_age:{$multiply:['$age', 2]}}}]);
4 除法
查询agg集合中数据,显示name和age字段,并为age字段数据做除2操作:
db.agg.aggregate([{$project:{name:1, new_age:{$divide:['$age', 2]}}}]);
5 取模
查询agg集合中数据,显示name和age字段,并为age字段数据做模2操作:
db.agg.aggregate([{$project:{name:1, new_age:{$mod:['$age', 2]}}}]);
索引
1 索引简介
索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构。
建立索引后,MongoDB会额外存储一份按字段升序/降序排序的索引数据,索引结构通常采用类似btree的结构持久化存储,以保证从索引里快速(O(logN)的时间复杂度)找出某个值对应的位置信息,然后根据位置信息就能读取出对应的文档。简单的说,索引就是将文档按照某个(或某些)字段顺序组织起来,以便能根据该字段高效的查询。
在MongoDB3版本后,创建集合时默认为系统主键字段_id创建索引。且在关闭_id索引创建时会有警告提示。因为_id字段不创建索引,会导致Secondary在同步数据时负载变高。
2 为什么使用索引
当你往某各个集合插入多个文档后,每个文档在经过底层的存储引擎持久化后,会有一个位置信息,通过这个位置信息,就能从存储引擎里读出该文档。比如mmapv1引擎里,位置信息是『文件id + 文件内offset 』, 在wiredtiger存储引擎(一个KV存储引擎)里,位置信息是wiredtiger在存储文档时生成的一个key,通过这个key能访问到对应的文档;为方便介绍,统一用pos(position的缩写)来代表位置信息。
假设集合person中有如下数据:
假设其存储后位置信息如下:
假设现在有个查询 db.person.find( {age: 18} ), 查询所有年龄为18岁的人,这时需要遍历所有的文档(『全表扫描』),根据位置信息读出文档,对比age字段是否为18。当然如果只有4个文档,全表扫描的开销并不大,但如果集合文档数量到百万、甚至千万上亿的时候,对集合进行全表扫描开销是非常大的,一个查询耗费数十秒甚至几分钟都有可能。
如果想加速 db.person.find( {age: 18} ),就可以考虑对person表的age字段建立升序索引(降序索引)。
建立升序索引后,MongoDB会额外存储一份按age字段升序排序的索引数据,索引结构类似如下,索引通常采用类似btree的结构持久化存储,以保证从索引里快速(O(logN)的时间复杂度)找出某个age值对应的位置信息,然后根据位置信息就能读取出对应的文档。
索引内容大致如下:
简单的说,索引就是将文档按照某个(或某些)字段顺序组织起来,以便能根据该字段高效的查询。有了索引,至少能优化如下场景的效率:
- 查询,比如查询年龄为18的所有人
- 更新/删除,将年龄为18的所有人的信息更新或删除,因为更新或删除时,需要根据条件先查询出所有符合条件的文档,所以本质上还是在优化查询
- 排序,将所有人的信息按年龄排序,如果没有索引,需要全表扫描文档,然后再对扫描的结果进行排序
3 索引管理
3.1 创建索引
MongoDB中使用createIndex或ensureIndex函数来创建索引。ensureIndex函数是1.8版本后增加的创建索引函数,是官方推荐使用的函数。createIndex函数在部分高版本MongoDB中已被移除,不推荐使用。
语法:
db.集合名称.createIndex(<keys>, <options>);
db.集合名称.ensureIndex(<keys>, <options>);
参数解释:
1.keys:用于创建索引的列及索引数据的排序规则
如
在stu集合的name字段上创建索引,并升序排列db.stu.ensureIndex({'name':1})
在stu集合的age字段上创建索引,并降序排序 db.stu.ensureIndex({'age':-1})
在stu集合的name和age字段上创建符合索引,以name字段数据升序排列、age字段降序排 db.stu.ensureIndex({'name':1, 'age':-1})
2.options:创建索引时可定义的索引参数。可选参数如
如:在stu集合的name字段上创建升序索引,并后台执行 - db.stu.ensureIndex({'name':1},{'background':true});
3.2 查看索引
在MongoDB中使用getIndexes()函数查看集合的索引信息。
语法:
db.集合名称.getIndexes()
查看stu集合的索引信息:
db.stu.getIndexes();
集合信息简介:
{
"v" : 2, 版本, 根据MongoDB版本决定。
"key" : { 在什么字段上的索引。
"name" : 1 字段名称:排序方式 1:升序; -1:降序
},
"name" : "name_1", 索引名称
"ns" : "test.agg", 索引所在的集合(namespace)
"background" : true 创建索引时的参数。
}
3.3 查看索引键
在MongoDB中使用getIndexKeys()函数查看集合的索引键。
语法:
db.集合名称.getIndexKeys();
查看stu集合的索引键信息:
db.stu.getIndexKeys();
3.4 查看索引详情
在MongoDB中使用getIndexSpecs()函数查看索引详情。
语法:
db.集合名称.getIndexSpecs();
查看stu集合的索引详情:
db.stu.getIndexSpecs();
3.5 查看索引占用空间
在MongoDB中使用totalIndexSize()函数查看集合中索引的大小。
语法:
db.集合名称.totalIndexSize([is_detail]);
参数解释:
is_detail:可选参数,传入除0或false外的任意数据,都会显示该集合中每个索引的大小及总大小。如果传入0或false则只显示该集合中所有索引的总大小。默认值为false。
查看stu集合的索引总大小:
db.stu.totalIndexSize();
或
db.stu.totalIndexSize(0);
或
db.stu.totalIndexSize(false);
查看stu集合的每个索引的大小及总大小:
db.stu.totalIndexSize(1);
或
db.stu.totalIndexSize(true);
3.6 删除指定索引
在MongoDB中使用dropIndex()函数删除指定的索引。
语法:
db.集合名称.dropIndex('索引名');
删除stu集合中命名为name_-1的索引
db.stu.dropIndex('name_-1');
3.7 删除集合的索引自建索引
在MongoDB中使用dropIndexes()函数删除集合中的所有自建索引。此函数只删除自建索引,不会删除MongoDB创建的_id索引。谨慎使用。破坏性太强。
语法:
db.集合名称.dropIndexes();
删除stu集合中所有的自建索引:
db.stu.dropIndexes();
3.8 重建索引
在MongoDB中使用reIndex()函数重建索引。重建索引可以减少索引存储空间,减少索引碎片,优化索引查询效率。一般在数据大量变化后,会使用重建索引来提升索引性能。重建索引是删除原索引重新创建的过程,不建议反复使用。
语法:
db.集合名称.reIndex()
重建stu集合上的所有索引
db.stu.reIndex();
4 索引类型
MongoDB支持多种类型的索引,包括单字段索引、复合索引、多key索引、文本索引等,每种类型的索引有不同的使用场合。
4.1 单字段索引(Single Field Index)
如:db.stu.ensureIndex({'age':1});
上述语句针对age创建了单字段索引,其能加速对age字段的各种查询请求,是最常见的索引形式,MongoDB默认创建的id索引也是这种类型。
{age: 1} 代表升序索引,也可以通过{age: -1}来指定降序索引,对于单字段索引,升序/降序效果是一样的。
在查询数据的时候,find()条件是索引所在列的时候,使用。
4.2 交叉索引
为一个集合的多个字段分别建立索引,在查询的时候通过多个字段作为查询条件,这种情况称为交叉索引。
交叉索引的查询效率较低,在使用时,当查询使用到多个字段的时候,尽量使用复合索引,而不是交叉索引。
如:
stu集合有name字段和age字段等。
有索引:
db.stu.createIndex({'name':1});
db.stu.createIndex({'age':-1});
查询:
db.stu.find({'name':'xxx', 'age': xxx});
4.3 复合|组合|聚合索引(Compound Index)
复合索引是Single Field Index的升级版本,它针对多个字段联合创建索引,先按第一个字段排序,第一个字段相同的文档按第二个字段排序,依次类推,如下针对age, name这2个字段创建一个复合索引。
如:db.stu.ensureIndex({'age':1, 'name':1});
上述索引对应的数据组织类似下表,与{age: 1}索引不同的时,当age字段相同时,在根据name字段进行排序,所以pos5对应的文档排在pos3之前。
复合索引能满足的查询场景比单字段索引更丰富,不光能满足多个字段组合起来的查询,比如db.person.find( {age: 18, name: “jack”} ),也能满足所以能匹配符合索引前缀的查询,这里{age: 1}即为{age: 1, name: 1}的前缀,所以类似db.person.find( {age: 18} )的查询也能通过该索引来加速;但db.person.find( {name: “jack”} )则无法使用该复合索引。如果经常需要根据『name字段』以及『name和age字段组合』来查询,则应该创建如下的复合索引-db.stu.ensureIndex({‘name’:1, ‘age’:1});
除了查询的需求能够影响索引的顺序,字段的值分布也是一个重要的考量因素,即使person集合所有的查询都是『name和age字段组合』(指定特定的name和age),字段的顺序也是有影响的。
age字段的取值很有限,即拥有相同age字段的文档会有很多;而name字段的取值则丰富很多,拥有相同name字段的文档很少;显然先按name字段查找,再在相同name的文档里查找age字段更为高效。
应用索引的最左前缀规则:如复合索引创建在{“name”:1, “age”:1}两个字段上,查询条件为: {“name”:“xxx”} 或 {“name”:“xx”, “age”:xxx} 或 {“age”:xxx, “name”:“xx”},都会使用复合索引。只有查询条件是{“age”:xx}的时候,不使用复合索引。复合索引是一个多维树,不可能绕过第一纬度的树,直接使用第二纬度的树。
4.4 多key索引(Multikey Index)
当索引的字段为数组时,创建出的索引称为多key索引,多key索引会为数组的每个元素建立一条索引,比如stu集合加入一个habbit字段(数组)用于描述兴趣爱好,需要查询有相同兴趣爱好的人就可以利用habbit字段的多key索引。
db.stu.insert({"name" : "jack", "age" : 19, habbies: ["football, runnning"]});
db.stu.ensureIndex( {habbies: 1} ) // 创建多key索引
db.stu.find( {habbies: "football"} )
5 索引特性
MongoDB除了支持多种不同类型的索引,还能对索引定制一些特殊的属性。
5.1 唯一索引
保证索引对应的字段不会出现相同的值,比如_id索引就是唯一索引
如:db.stu.ensureIndex({'name':1},{'unique':true});
- 如果创建唯一索引前所在字段有重复数据,创建失败
- 如果唯一索引所在字段有重复数据写入时,抛出异常。
5.2 部分索引(partial index)
只针对符合某个特定条件的文档建立索引,3.2版本才支持该特性。
MongoDB部分索引只为那些在一个集合中,满足指定的筛选条件的文档创建索引。由于部分索引是一个集合文档的一个子集,因此部分索引具有较低的存储需求,并降低了索引创建和维护的性能成本。部分索引通过指定过滤条件来创建,可以为MongoDB支持的所有索引类型使用部分索引。
简单点说:部分索引就是带有过滤条件的索引,即索引只存在与某些文档之上
如:db.stu.createIndex({'name':1},{'unique':true, 'partialFilterExpression':{'age': {$gt:25}}})
注意:部分索引只为集合中那些满足指定的筛选条件的文档创建索引。如果你指定的partialFilterExpression和唯一约束、那么唯一性约束只适用于满足筛选条件的文档。具有唯一约束的部分索引不会阻止不符合唯一约束且不符合过滤条件的文档的插入。
使用索引:部分索引的使用边界和普通索引不一致。是根据创建部分索引时,提供的partialFilterExpression来决定使用使用索引执行查询。如:索引所在字段是name字段,partialFilterExpression是age>20,那么只有查询条件中包含age>20 & name = ?的时候才使用这个索引。
6 索引覆盖查询特性
官方的MongoDB的文档中说明,覆盖查询是以下的查询:
- 所有的查询字段是索引的一部分
- 所有的查询返回字段都在索引中
由于所有出现在查询中的字段是索引的一部分, MongoDB 无需在整个数据文档中检索匹配查询条件和返回使用相同索引的查询结果。
因为索引存在于RAM中,从索引中获取数据比通过扫描文档读取数据要快得多。
如有如下索引:
db.stu.ensureIndex({gender:1,user_name:1})
那么执行如下查询时,该索引会覆盖查询:
db.stu.find({gender:"M"},{user_name:1,_id:0})
也就是说,对于上述查询,MongoDB的不会去数据库文件中查找。相反,它会从索引中提取数据,这是非常快速的数据查询。
由于我们的索引中不包括 _id 字段,_id在查询中会默认返回,我们可以在MongoDB的查询结果集中排除它。
7 查询计划
索引已经建立了,但查询还是很慢怎么破?这时就得深入的分析下索引的使用情况了,可通过查看下详细的查询计划来决定如何优化。通过执行计划可以看出如下问题
- 根据某个/些字段查询,但没有建立索引
- 根据某个/些字段查询,但建立了多个索引,执行查询时没有使用预期的索引。
查询计划语法是:db.集合名称.find().explain()
建立索引前,db.stu.find( {age:20} )必须执行COLLSCAN,即全表扫描。
建立索引后,通过查询计划可以看出,先进行IXSCAN(从索引中查找),然后FETCH,读取出满足条件的文档。
8 注意事项
既然索引可以加快查询速度,那么是不是只要是查询语句需要,就建上索引?答案是否定的。因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,数据库在运行时也要消耗资源维护索引,因此索引并不是越多越好。一般两种情况下不建议建索引。
第一种情况是表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描就好了。至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以2000作为分界线,记录数不超过 2000可以考虑不建索引,超过2000条可以酌情考虑索引。
另一种不建议建索引的情况是索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:
Index Selectivity = Cardinality / #T
9 索引限制
9.1 额外开销
每个索引占据一定的存储空间,在进行插入,更新和删除操作时也需要对索引进行操作。所以,如果你很少对集合进行读取操作,建议不使用索引。
9.2 内存开销
由于索引是存储在内存(RAM)中,你应该确保该索引的大小不超过内存的限制。
如果索引的大小大于内存的限制,MongoDB会删除一些索引,这将导致性能下降。
9.3 查询限制
索引不能被以下的查询使用:
正则表达式(最左匹配除外)及非操作符,如 $nin, $not, 等。
算术运算符,如 $mod, 等。
所以,检测你的语句是否使用索引是一个好的习惯,可以用explain来查看。
9.4 最大范围
集合中索引不能超过64个
索引名的长度不能超过128个字符
一个复合索引最多可以有31个字段