MongoDB高阶技术 1. 聚合操作 聚合操作处理数据并返回结果集(如 求和、去重等)。聚合操作可以操作多个文档,然后对数据进行处理,返回结果集。
聚合操作主要分为三个方法:
单一聚合
聚合管道
聚合管道是一个数据聚合的框架,基于流水线概念,文档进入流水线处理,把上一个节点处理结果用于下一个节点的数据源继续处理,最终返回结果集。
MapReduce(用于大数据)
处理每个文档并向每个输入文档发射一个或多个对象的map阶 段,以及reduce组合map操作的输出阶段。
1)单一聚合 MongoDB提供三种聚合函数,使用简单但是缺少灵活性和功能性。
函数
描述
db.collection.estimatedDocumentCount()
忽略查询条件,返回集合中所有文档的总数量
db.collection.count()
可以根据条件在集合中进行求和
db.collection.distinct()
可以根据条件在集合中进行去重,并通过数组的形式返回数据
db.orders.distinct(“cust_id”) 根据cust_id 去重,去重结果:A123、B212
单一聚合实验
1 2 3 4 5 6 7 8 #检索books集合中所有文档的计数 db.books.estimatedDocumentCount() # 检索books集合中 所有favCount 大于50 的文档数量(求和) db.books.count ({favCount:{$gt:50 }}) # 根据type进行去重 db.books.distinct("type") # 返回收藏数大于90 的文档,然后根据type 进行去重 db.books.distinct("type",{favCount:{$gt:90 }})
2)聚合管道 MongoDB 聚合框架(Aggregation Framework)是一个计算框架,它可以:
作用在一个或几个集合上;
对集合中的数据进行的一系列运算;
将这些数据转化为期待的数据形式;
聚合框架更类似于 SQL中的GROUP BY、LEFT JOIN 、AS 别名等功能。
管道(Pipeline)和阶段(Stage)
整个聚合运算过程称为管道(Pipeline),它是由多个阶段(Stage)组成的,每个阶段都可以对文档进行运算,并将文档输送给下一个阶段。
聚合管道操作语法 1 db.collection.aggregate([$stage1, $stage2, ...$stageN], {options})
[$stage ……] 一组数据聚合阶段。除$out、$Merge和$geonear阶段之外,每个阶段都可以在管道中 出现多次。
options 可选,聚合操作的其他参数。包含:查询计划、是否使用临时文件、 游标、最大操作时 间、读写策略、强制索引等等
$match 匹配数据 status = A,剩余 3个文档
$group 根据cust_id 分组查询,剩余2条文档
total $sum求和
常用的管道聚合阶段
阶段
描述
SQL等价运算符
$match
筛选条件
WHERE 条件查询
$project
投影
AS 别名
$lookup
左外连接
LEFT OUTER JOIN
$sort
排序
ORDER BY
$group
分组
GROUP BY
$skip/$limit
分页
$unwind
展开数组
$graphLookup
图搜索
$facet/$bucket
分面搜索
数据准备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var tags = ["nosql" ,"mongodb" ,"document" ,"developer" ,"popular" ];var types = ["technology" ,"sociality" ,"travel" ,"novel" ,"literature" ];var books=[];for (var i=0 ;i<50 ;i++){ var typeIdx = Math .floor (Math .random ()*types.length ); var tagIdx = Math .floor (Math .random ()*tags.length ); var tagIdx2 = Math .floor (Math .random ()*tags.length ); var favCount = Math .floor (Math .random ()*100 ); var username = "xx00" +Math .floor (Math .random ()*10 ); var age = 20 + Math .floor (Math .random ()*15 ); var book = { title : "book-" +i, type : types[typeIdx], tag : [tags[tagIdx],tags[tagIdx2]], favCount : favCount, author : {name :username,age :age} }; books.push (book) } db.books .insertMany (books);
导入后的数据:
$project 该函数有两个功能:
1、投影操作, 将原始字段投影成指定名称, 如将集合中的 title 投影成 name,可以理解为 SQL中的 as 别名
2、可以返回 指定的字段(或者说 排除不想要的字段),类似于SQL的 select xxx
1 > db.books.aggregate([{$project:{name:"$title"}}])
指定想要返回的字段(或者说 排除不想返回的字段):
1 2 # value 为0 的代表隐藏,为1 代表显示 。除了_id外,其他字段如果不写的话默认都是隐藏。 > db.books.aggregate([{$project:{name:"$title",_id:0 ,type:1 ,author:1 }}])
从嵌套字段内 排除字段
上面的测试数据中,author作者字段里面嵌套了 name、age,我们也可以实现 只返回name。
1 2 3 4 5 # 投影,将 title 投影成name ;指定返回的字段。 author.name 嵌套数据也可以指定返回字段。 > db.books.aggregate([{$project:{name:"$title",_id:0 ,type:1 ,"author.name":1 }}])# 这种方式也可以实现同样效果 > db.books.aggregate([{$project:{name:"$title",_id:0 ,type:1 ,author:{name:1 }}}])
$match match 类似于SQL的where,建议把match放到管道最前端 ,过滤后的结果在进行处理,可以有效的提高处理效率。
1 2 # 只查询type = technology的文档 > db.books.aggregate([{$match :{type:"technology"}}])
$count 求和并返回结果
1 2 > db.books.aggregate([{$match :{type:"technology"}},{$count:"type_count"}]){ "type_count" : 6 }
根据查询条件 type = technology 进行求和,并以 type_count字段返回求和结果
管道内,第一阶段 $match 会先匹配数据,并传给下一阶段,第二阶段 $count 会根据结果集 进行求和
$group 根据指定的表达式条件进行分组,并将分组后的结果传给下一阶段。
accumulator操作符
名称
描述
类比 sql
$avg
计算均值
avg
$first
返回每组第一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的第一个文档。
limit 0,1
$last
返回每组最后一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的最后个文档。
-
$max
根据分组,获取集合中所有文档对应值得最大值。
max
$min
根据分组,获取集合中所有文档对应值得最小值。
min
$push
将指定的表达式的值添加到一个数组中。
-
$addToSet
将表达式的值添加到一个集合中(无重复值,无序)。
java 的set
$sum
计算总和
sum
$stdDevPop
返回输入值的总体标准偏差(population standard deviation)
-
$stdDevSamp
返回输入值的样本标准偏差(the sample standard deviation)
-
$group允许最大内存100M,如果超过则会报错,如果要处理超大数据的话,可以设置 allowDiskUse = true,$group操作产生的数据会写入到硬盘中。
book的数量,收藏总数和平均值
1 2 3 4 5 6 > db.books.aggregate([ {$group :{_id:null ,count:{$sum:1 },pop:{$sum:"$favCount"},avg: {$avg:"$favCount"}}} ]) { "_id" : null , "count" : 50 , "pop" : 2258 , "avg" : 45.16 }
$group 分组,_id 为null 意思是 只有一个组,count:{$sum:1} 根据分组结果进行求和
pop:{$sum:”$favCount”} 根据收藏数进行求和
avg:{$avg:”$favCount”} 根据收藏数求平均值
统计每个作者的book收藏总数
1 2 3 4 5 6 # 先根据author.name 作者名称进行分组,分组之后的结果 根据favCount 通过$sum函数进行求和 > db.books.aggregate([ {$group :{_id:"$author.name",pop:{$sum:"$favCount"}}} ]) { "_id" : "author.name", "pop" : 2258 }
统计每个作者的每本book的收藏数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 先根据author.name和title 进行分组(每个作者每本书),然后 进行求和,求和的结果集名称 pop > db.books.aggregate([ {$group :{_id:{name:"$author.name",title:"$title"},pop:{$sum:"$favCount"}}} ]) { "_id" : { "name" : "xx002", "title" : "book-17" }, "pop" : 70 } { "_id" : { "name" : "xx002", "title" : "book-29" }, "pop" : 11 } { "_id" : { "name" : "xx001", "title" : "book-12" }, "pop" : 80 } { "_id" : { "name" : "xx001", "title" : "book-25" }, "pop" : 64 } { "_id" : { "name" : "xx006", "title" : "book-38" }, "pop" : 34 } { "_id" : { "name" : "xx003", "title" : "book-14" }, "pop" : 17 } { "_id" : { "name" : "xx008", "title" : "book-49" }, "pop" : 6 } { "_id" : { "name" : "xx001", "title" : "book-34" }, "pop" : 21 } { "_id" : { "name" : "xx004", "title" : "book-35" }, "pop" : 88 } { "_id" : { "name" : "xx008", "title" : "book-5" }, "pop" : 3 } { "_id" : { "name" : "xx009", "title" : "book-13" }, "pop" : 78 } { "_id" : { "name" : "xx000", "title" : "book-7" }, "pop" : 92 } { "_id" : { "name" : "xx007", "title" : "book-2" }, "pop" : 92 } { "_id" : { "name" : "xx000", "title" : "book-4" }, "pop" : 34 } { "_id" : { "name" : "xx000", "title" : "book-9" }, "pop" : 14 } { "_id" : { "name" : "xx007", "title" : "book-1" }, "pop" : 26 } { "_id" : { "name" : "xx000", "title" : "book-27" }, "pop" : 87 } { "_id" : { "name" : "xx004", "title" : "book-31" }, "pop" : 42 } { "_id" : { "name" : "xx002", "title" : "book-36" }, "pop" : 12 } { "_id" : { "name" : "xx009", "title" : "book-22" }, "pop" : 90 } Type "it" for more
每个作者的book的type合集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 根据author.name进行分组,下一阶段 使用$addToSet 把type类型 添加到一个set 集合(无序 不重复) > db.books.aggregate([ {$group :{_id:"$author.name",type:{$addToSet:"$type"}}} ]) { "_id" : "xx004", "type" : [ "travel", "sociality", "literature", "novel" ] } { "_id" : "xx008", "type" : [ "travel", "sociality", "literature" ] } { "_id" : "xx003", "type" : [ "technology", "sociality" ] } { "_id" : "xx002", "type" : [ "novel", "literature", "sociality", "travel" ] } { "_id" : "xx006", "type" : [ "literature" ] } { "_id" : "xx007", "type" : [ "travel", "technology", "sociality", "novel" ] } { "_id" : "xx005", "type" : [ "sociality" ] } { "_id" : "xx001", "type" : [ "technology", "sociality", "literature", "novel" ] } { "_id" : "xx000", "type" : [ "travel", "sociality", "literature", "novel" ] } { "_id" : "xx009", "type" : [ "novel", "technology", "travel" ] }
$group 根据作者名称进行分组,把分组的结果 传递给下一阶段
$addToSet 将type类型添加到set集合
$unwind
将作者 xx001的book的tag数组拆分成单个文档
1 2 3 4 > db.books.aggregate([ {$match :{"author.name":"xx001"}}, {$unwind:"$tag"} ])
tag 标签参数是个数组,里面存了多个标签,可以通过unwind 拆分成单个文档,其他参数则按照原有的来。
查询出每个作者的book的tag集合
1 2 3 4 > db.books.aggregate([ {$unwind:"$tag"}, {$group :{_id:"$author.name",types:{$addToSet:"$tag"}}} ])
1、将tag 从数组拆分成单个文档
2、根据拆分后的结果,根据作者名称进行分组,分组后 将tag存入到set集合中
$limit 限制传递到管道中下一阶段的文档数,如 limit 20 那么传递给下一阶段的只有20条文档。
1 2 3 4 > db.books.aggregate([ {$match :{"tag":"mongodb"}}, {$limit : 5 } ])
注意:当$sort 和$limit同时出现时,要区分清楚 sort在limit前还是limit后,不同的位置导致结果不同(先排序后分页,还是先分页后排序)。
$skip 跳过指定数量的文档,将剩余文档传递给管道的下一阶段。注意,这里的skip是文档数量,而不是分页数量。
1 2 3 4 5 > db.books.aggregate([ {$match :{"tag":"mongodb"}}, {$limit : 5 }, {$skip : 1 } ])
$sort 对文档进行排序
1 2 3 4 5 6 > db.books.aggregate([ {$match :{"tag":"mongodb"}}, {$limit : 5 }, {$skip : 1 }, {$sort : {favCount:-1 ,title:1 }} ])
对字段设置排序的话,-1 代表降序,1 代表升序
$lookup(关联表查询) 通过$lookup可以实现 多表关联查询。经过$lookup后生成新的数组。
1 2 3 4 5 6 7 8 db.collection.aggregate([{ $lookup: { from : "<collection to join>", localField: "<field from the input documents>", foreignField: "<field from the documents of the from collection>", as : "<output array field>" } })
该方法等同于MySQL的关联表查询,只需要注意 from(被join的表),以及join 条件即可。
属性
作用
from
同一个数据库下等待被Join的集合。 类似于 left join 后面的表名
localField
类似于left join 里面的左边集合关联条件(on a = b) ,如果没有值的话,则为null
foreignField
类似于left join 里面的右边集合关联条件(on a = b) ,如果没有值的话,则为null
as
为输出文档的新增值命名。如果输入的集合中已存在该值,则会覆盖掉
数据准备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 插入用户集合 db.customer.insert({customerCode:1 ,name:"customer1",phone:"13112345678",address:"test1"}) db.customer.insert({customerCode:2 ,name:"customer2",phone:"13112345679",address:"test2"}) # 插入订单,2 条订单记录,订单记录关联 用户1 、用户2 (customerCode) db.order.insert({orderId:1 ,orderCode:"order001",customerCode:1 ,price:200 }) db.order.insert({orderId:2 ,orderCode:"order002",customerCode:2 ,price:400 }) # 插入订单详情,订单详情关联订单id(orderId) db.orderItem.insert({itemId:1 ,productName:"apples",qutity:2 ,orderId:1 }) db.orderItem.insert({itemId:2 ,productName:"oranges",qutity:2 ,orderId:1 }) db.orderItem.insert({itemId:3 ,productName:"mangoes",qutity:2 ,orderId:1 }) db.orderItem.insert({itemId:4 ,productName:"apples",qutity:2 ,orderId:2 }) db.orderItem.insert({itemId:5 ,productName:"oranges",qutity:2 ,orderId:2 }) db.orderItem.insert({itemId:6 ,productName:"mangoes",qutity:2 ,orderId:2 })
查询用户表 并关联 订单表
1 2 3 4 5 6 7 8 9 10 11 12 db.customer.aggregate([ { $lookup: { from : "order", localField: "customerCode", foreignField: "customerCode", as : "customerOrder" } } ]).pretty() # .pretty() 格式化结果集,方便我们观看结果
查询用户表,关联order表,关联字段 customerCode,as 重命名关联结果。
查询订单表并关联 用户表
1 2 3 4 5 6 7 8 9 10 db.order.aggregate([ { $lookup: { from : "customer", localField: "customerCode", foreignField: "customerCode", as : "customer" } } ]).pretty()
聚合操作示例(一) 统计每个分类的book文档数量
1 2 3 4 5 6 7 8 9 > db.books.aggregate([ {$group :{_id:"$type",total:{$sum:1 } }} ]) { "_id" : "technology", "total" : 6 } { "_id" : "travel", "total" : 11 } { "_id" : "literature", "total" : 11 } { "_id" : "sociality", "total" : 12 } { "_id" : "novel", "total" : 10 }
以type 进行group分组,然后 sum 进行累加,参数为1 。 所以会把 type同一个类型的数量统计出来。
根据标签进行拆分,统计每个标签的收藏数(favCount)并进行降序排序
1 2 3 4 5 6 7 8 9 10 11 12 > db.books.aggregate([ {$match :{favCount:{$gt:0 } }}, {$unwind:"$tag"}, {$group :{_id:"$tag",total:{$sum:"$favCount" } } }, {$sort:{total:-1 } } ]) { "_id" : "document", "total" : 1368 } { "_id" : "popular", "total" : 834 } { "_id" : "developer", "total" : 795 } { "_id" : "mongodb", "total" : 790 } { "_id" : "nosql", "total" : 729 }
$match,先过滤掉 小于或等于0的文档
$unwind,由于需要统计每个标签的收藏数,但是标签是个数组,所以需要通过unwind 进行拆分
$group,通过分组查询,根据tag进行分组,$sum 根据favCount进行累加
$sort排序
区间统计book文档收藏数[0,10),[10,60),[60,80),[80,100),[100,+∞)
1 2 3 4 5 6 7 8 9 10 11 12 13 db.books.aggregate([{ $bucket:{ groupBy:"$favCount", boundaries:[0 ,10 ,60 ,80 ,100 ], default :"other", output:{"count":{$sum:1 }} } }]) { "_id" : 0 , "count" : 6 } { "_id" : 10 , "count" : 24 } { "_id" : 60 , "count" : 9 } { "_id" : 80 , "count" : 11 }
$bucket 是关键字,意思是 启动区间统计
boundaries 统计的区间, 0-10,10-60,60-80,80-100等等
聚合操作示例(二)
根据美国的州和城市来做一系列聚合操作实验,所以我们要先导入 州和城市的数据。
导入邮政编码数据集 :https://media.mongodb.org/zips.json
导入工具 mongoimport 下载地址(https://www.mongodb.com/try/download/database-tools)
1 2 3 4 5 6 7 8 9 10 11 12 13 # 下载工具 [root@cvm52851 ~]# wget https://fastdl.mongodb.org/tools/db/mongodb-database-tools-rhel80-x86_64-100.9.4.tgz [root@cvm52851 ~]# tar -xvf mongodb-database-tools-rhel80-x86_64-100.9.4.tgz [root@cvm52851 ~]# cd mongodb-database-tools-rhel80-x86_64-100.9.4/bin # 下载数据集 [root@cvm52851 bin]# wget https://media.mongodb.org/zips.json # 启动工具导入 [root@cvm52851 bin]# ./mongoimport -h localhost -d appDb -u dbXing -p 123123 --authenticationDatabase=appDb -c zips --file zips.json # 导入成功的提示 2024-01-07T17:40:54.286+0800 0 document(s) imported successfully. 0 document(s) failed to import.
-h 主机地址
-u 用户名
-p 密码
-d 数据库名称
-authenticationDatabase 认证数据库
-c 导入的数据库中的集合名称
-file 导入的文件名
返回人口超过1000万的州 1 2 3 4 5 6 7 8 9 10 11 12 db.zips.aggregate([ {$group :{_id:"$state",total:{$sum:"$pop"} }}, {$match :{total:{$gt:1000 * 1000 * 10 } }} ]) { "_id" : "IL", "total" : 11427576 } { "_id" : "CA", "total" : 29754890 } { "_id" : "FL", "total" : 12686644 } { "_id" : "NY", "total" : 17990402 } { "_id" : "PA", "total" : 11881643 } { "_id" : "OH", "total" : 10846517 } { "_id" : "TX", "total" : 16984601 }
$group按state进行分组查询后,$sum根据pop字段进行累加
$match 匹配total 总人口超过1000w的州
返回各州平均城市人口 1 2 3 4 5 db.zips.aggregate([ {$group :{_id:{state:"$state",city:"$city"},total:{$sum:"$pop"} }}, {$group :{_id:"$_id.state",stateAvg:{$avg:"$total"} } }, {$sort:{stateAvg:-1 }} ])
$group按state、city 进行分组查询后,$sum根据pop字段进行累加
$group 按上一阶段的结果的state进行分组,$avg 分组后 求平均值
$sort 根据stateAvg 进行降序
按州返回最大和最小的城市 1 2 db.zips.aggregate( [ { $group : { _id: { state: "$state", city: "$city" }, pop: { $sum: "$pop" } } }, { $sort: { pop: 1 } }, { $group : { _id : "$_id.state", biggestCity: { $last : "$_id.city" }, biggestPop: { $last : "$pop" }, smallestCity: { $first : "$_id.city" }, smallestPop: { $first : "$pop" } } }, { $project: { _id: 0 , state: "$_id", biggestCity: { name: "$biggestCity", pop: "$biggestPop" }, smallestCity: { name: "$smallestCity", pop: "$smallestPop" } } }, { $sort: { state: 1 } } ] )
3)SpringBoot中实现聚合操作 MongoTemplate提供了aggregate方法来实现对数据的聚合操作。
基于聚合管道mongodb提供的可操作的内容:
基于聚合操作Aggregation.group,mongodb提供可选的表达式
聚合操作示例二 - 返回人口超过1000万的州
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RunWith(SpringRunner.class) @org .springframework.boot.test.context.SpringBootTest(classes = {MongoApplication.class})public class SpringBootTest { @Autowired MongoTemplate mongoTemplate; @org .junit.Test public void case1 () { GroupOperation groupOperation = Aggregation.group("state" ).sum("pop" ).as("total" ); MatchOperation matchOperation = Aggregation.match(Criteria.where("total" ).gt(10 *1000 *1000 )); TypedAggregation<Zips> aggregation = Aggregation.newAggregation(Zips.class, groupOperation, matchOperation); AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, Map.class); System.out.println(results.getMappedResults()); } }
AggregationResults 通过mongoTemplate查询之后返回的对象,我们可以封装成其他实体类,这里为了省事用了Map
聚合操作示例二 - 返回各州平均城市人口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RunWith(SpringRunner.class) @org .springframework.boot.test.context.SpringBootTest(classes = {MongoApplication.class})public class SpringBootTest { @Autowired MongoTemplate mongoTemplate; @org .junit.Test public void case2 () { GroupOperation groupOperation = Aggregation.group("state" ,"city" ).sum("pop" ).as("total" ); GroupOperation groupOperation2 = Aggregation.group("_id.state" ).avg("total" ).as("cityAvg" ); SortOperation sortOperation = Aggregation.sort(Sort.Direction.DESC, "cityAvg" ); TypedAggregation<Zips> aggregation = Aggregation.newAggregation(Zips.class, groupOperation, groupOperation2,sortOperation); AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, Map.class); System.out.println(results.getMappedResults()); } }
2. MongoDB索引 MongoDB索引是一种用来快速查询数据的数据结构 。底层使用的是B+Tree来作为索引的数据结构。
MongoDB支持各种丰富的索引类型 ,包括单键索引、复合索引,唯一索引等一 些常用的结构。在一些特殊应用场景,MongoDB还支持地理 空间索引、文本检索索引、TTL索引等不同的特性。
不使用索引的话,会扫描全部文档进行匹配,而使用索引的话通过索引找到对应的文档 ,大大提高了查询效率。
MongoDB索引和MySQL的索引非常相似,基本一致,学习成本很低。
1)索引的分类
按照索引包含字段数,可以分为 单键索引和复合索引(组合索引)
按照索引字段的类型,可以分为主键索引和非主键索引
主键索引又叫 聚簇索引,索引节点 包含了完整的数据记录
非主键索引又叫 稀疏索引,索引节点 只包含了 指向 聚簇索引的地址指针,而不包含数据记录
按照索引的特性不同,又可以分为唯一索引、稀疏索引、文本索引、地理空间索引等
2)索引操作 创建索引 创建索引语法格式
1 db.collection.createIndex(keys, options)
注意:3.0.0 版本前创建索引方法为 db.collection.ensureIndex()
1 2 3 4 5 6 7 8 9 10 11 # 以后台的形式创建索引 > db.books.createIndex({title:1 },{background:true }){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 } # 创建唯一索引,注意:如果存在非唯一值,会创建失败 > db.books.createIndex({type:1 },{unique :true })
查看索引 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 # 查看所有的索引 > db.books.getIndexes()[ { "v" : 2 , "key" : { "_id" : 1 }, "name" : "_id_" }, { "v" : 2 , "key" : { "title" : 1 }, # 索引名称 "name" : "title_1", "background" : true } ] # 查看索引key值 > db.books.getIndexKeys()[ { "_id" : 1 }, { "title" : 1 } ] # 查看索引占用空间,后面的参数 是可选参数,0 - 显示所有索引占用空间 , 1 - 显示所有索引占用空间 > db.books.totalIndexSize(0 )45056 # 显示所有索引占用空间 > db.books.totalIndexSize(1 )_id_ 24576 title_1 20480 45056
查看索引是否生效
我们可以通过explain() 执行计划去查看是否走了索引。
没有创建索引的话,查询title的话 使用的是全集合扫描。
当创建索引后,执行explain的话,发现成功走了索引扫描。
删除索引 1 2 3 4 5 6 7 8 9 10 11 12 # 删除集合的 指定索引,value 是索引名称 > db.books.dropIndex("title_1"){ "nIndexesWas" : 2 , "ok" : 1 } # 删除集合中的所有索引 > db.books.dropIndexes(){ "nIndexesWas" : 1 , "msg" : "non-_id indexes dropped for collection", "ok" : 1 }
注意:删除指定索引时,指定的value 是索引对应的名称(name)
3)索引类型 单键索引 顾名思义,为单个字段创建的索引就是 单键索引。
1 2 3 4 5 6 7 8 # 以后台的形式创建索引 > db.books.createIndex({title:1 },{background:true }){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
对内嵌文档字段 创建索引
1 2 3 4 5 6 7 > db.books.createIndex({"author.name":1 },{background:true }){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 2 , "numIndexesAfter" : 3 , "ok" : 1 }
使用任务计划查询看看是否使用了索引
复合索引(联合索引) 复合索引是多个字段组合而成的索引,可以参考MySQL的复合索引。查询的时候要遵循 最左匹配原则,因为创建索时 是按最左的字段排序的,如果不遵循最左原则的话,会无法走索引。
创建索引
1 2 # 创建复合索引,type和favCount 都以升序排序 > db.books.createIndex({type:1 ,favCount:1 })
最左原则查询
1 2 3 # 符合最左匹配原则查询 > db.books.find({type:"sociality",favCount:{$gt:20 }}).explain()
不符合最左匹配查询
1 2 # 不符合最左匹配查询,直接跳过最左侧的 type字段 > db.books.find({favCount:{$gt:20 }}).explain()
创建联合索引顺序:type,favCount,而查询的时候 直接跳过了 type,自然无法走索引
多键索引(Multikey Index) 在数组的属性上建立索引 ,就叫 多键索引。
准备数据
1 2 3 4 5 6 7 8 9 10 # 批量插入多条测试数据 db.inventory.insertMany([ { _id: 5 , type: "food", item: "aaa", ratings: [ 5 , 8 , 9 ] }, { _id: 6 , type: "food", item: "bbb", ratings: [ 5 , 9 ] }, { _id: 7 , type: "food", item: "ccc", ratings: [ 9 , 5 , 8 ] }, { _id: 8 , type: "food", item: "ddd", ratings: [ 9 , 5 ] }, { _id: 9 , type: "food", item: "eee", ratings: [ 5 , 9 , 5 ] } ]) { "acknowledged" : true , "insertedIds" : [ 5 , 6 , 7 , 8 , 9 ] }
创建多键索引
1 2 3 4 5 6 7 8 # 创建多键索引 > db.inventory.createIndex({ratings:1 }){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
注意:多键索引并非复合索引,不要混淆概念。符合索引是联合多个字段创建的,而多键索引 是在数组的属性上创建索引。
创建 复合索引(含多键索引)
1 2 3 4 5 6 7 8 # 创建复合索引的话,复合索引只能允许存在一个 多键索引 > db.inventory.createIndex({item:1 ,ratings:1 }){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 2 , "numIndexesAfter" : 3 , "ok" : 1 }
注意:复合索引只能允许存在一个 多键索引。
嵌入文档的索引数组
准备数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # 批量插入多条嵌套数组的数据 db.inventory.insertMany([ { _id: 1 , item: "abc", stock: [ { size: "S", color: "red", quantity: 25 }, { size: "S", color: "blue", quantity: 10 }, { size: "M", color: "blue", quantity: 50 } ] }, { _id: 2 , item: "def", stock: [ { size: "S", color: "blue", quantity: 20 }, { size: "M", color: "blue", quantity: 5 }, { size: "M", color: "black", quantity: 10 }, { size: "L", color: "red", quantity: 2 } ] }, { _id: 3 , item: "ijk", stock: [ { size: "M", color: "blue", quantity: 15 }, { size: "L", color: "blue", quantity: 100 }, { size: "L", color: "red", quantity: 25 } ] } ])
在嵌套数组上 创建 多键索引
1 2 3 4 5 6 7 8 # 在嵌套数组上 创建 多键索引 > db.inventory.createIndex( { "stock.size": 1 , "stock.quantity": 1 } ){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 3 , "numIndexesAfter" : 4 , "ok" : 1 }
1 2 # 查询 inventory集合,条件:stock.size = S 并且 quantity 大于25 > db.inventory.find({"stock.size":"S","stock.quantity":{$gt:25 }})
地理空间索引(Geospatial Index) 在互联网中,地理空间检索是非常常用的,而MongoDB地理空间索引(2dsphereindex)就是专门用于实现位置检索的一种特殊索引。
如何实现查询 附近商家 1 2 3 4 5 6 7 8 9 # 插入商家和定位信息 db.restaurant.insert({ restaurantId: 0 , restaurantName:"兰州牛肉面", location : { type: "Point", coordinates: [ -73.97 , 40.77 ] } })
创建一个地理空间索引 - 2d
1 2 3 4 5 6 7 8 # 创建 地里空间索引,字段 location ,类型2 dsphere(地理空间索引) > db.restaurant.createIndex({location : "2dsphere"}){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
根据经纬度 查询附近10000米的商家信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 db.restaurant.find({ location:{ $near:{ $geometry:{ type:"Point", coordinates:[-73.88 , 40.78 ] }, $maxDistance:10000 } } }) # 查询结果 { "_id" : ObjectId("659caea6e268f34c178d2071"), "restaurantId" : 0 , "restaurantName" : "兰州牛肉面", "location" : { "type" : "Point", "coordinates" : [ -73.97 , 40.77 ] } }
全文索引(Text Indexes) MongoDB支持简易的分词检索,且官方仅支持英文检索,不支持中文分词。如果项目中需要用到中文分词,建议还是使用elasticsearch
注意:一个集合只能创建一个 全文索引 ,创建多个会出错。
准备数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 db.stores.insert( [ { _id: 1 , name: "Java Hut", description: "Coffee and cakes" }, { _id: 2 , name: "Burger Buns", description: "Gourmet hamburgers" }, { _id: 3 , name: "Coffee Shop", description: "Just coffee" }, { _id: 4 , name: "Clothes Clothes Clothes", description: "Discount clothing" }, { _id: 5 , name: "Java Shopping", description: "Indonesian goods" } ] ) # 插入成功 BulkWriteResult({ "writeErrors" : [ ], "writeConcernErrors" : [ ], "nInserted" : 5 , "nUpserted" : 0 , "nMatched" : 0 , "nModified" : 0 , "nRemoved" : 0 , "upserted" : [ ] })
创建复合索引 - 全文索引
1 2 3 4 5 6 7 8 # 创建复合 全文检索索引,key 是字段名,value 是固定格式text,代表是全文索引 > db.stores.createIndex({name:"text",description:"text"}){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
1、key 是要创建索引的字段名,value 是固定格式text,代表是全文索引
2、创建了全文索引后,会根据字段名进行分词 进行倒排索引排序,后续使用$text查询时 会直接从索引树里面查找
通过$text操作符 查询数据中包含 “java”、”coffee”、”shop”的文档
1 2 3 4 5 6 7 # $text 全文检索操作符,查询的关键字用空格隔开,之间的关系是 or ,查询包含 java or coffe or shop的文档 > db.stores.find({$text:{$search :"java coffee shop"} })# 查询结果 { "_id" : 3 , "name" : "Coffee Shop", "description" : "Just coffee" } { "_id" : 1 , "name" : "Java Hut", "description" : "Coffee and cakes" } { "_id" : 5 , "name" : "Java Shopping", "description" : "Indonesian goods" }
$text 全文检索操作符,关键字用空格隔开,之间的关系是 or,查询包含 java 或 coffe 或 shop的文档
由于创建是复合索引,关键字会在 name和description 里面进行查询
通过执行计划可以看出,成功走了索引
Hash索引(Hashed Indexes) 哈希索引只支持精确匹配,不支持范围查询。如果确定该索引只用于 等值查询,可以创建哈希索引来提升查询效率。
1 > db.users.createIndex({username : 'hashed' })
通配符索引(Wildcard Indexes) 由于MongoDB文档可以动态改变,存在一些不可预知的字段结构,为了提高这部分字段的查询速度,可以为其创建 通配符索引。
插入准备数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 db.products.insert([ { "product_name" : "Spy Coat", "product_attributes" : { "material" : [ "Tweed", "Wool", "Leather" ], "size" : { "length" : 72 , "units" : "inches" } } }, { "product_name" : "Spy Pen", "product_attributes" : { "colors" : [ "Blue", "Black" ], "secret_feature" : { "name" : "laser", "power" : "1000", "units" : "watts", } } }, { "product_name" : "Spy Book" } ]) # 插入结果 BulkWriteResult({ "writeErrors" : [ ], "writeConcernErrors" : [ ], "nInserted" : 3 , "nUpserted" : 0 , "nMatched" : 0 , "nModified" : 0 , "nRemoved" : 0 , "upserted" : [ ] })
创建通配符索引
1 2 3 4 5 6 7 8 # 规格参数,规格参数是动态可变的,所以我们为其创建通配符 product_attributes.$* * 代表这个字段后面的字段都会被匹配到 > db.products.createIndex( { "product_attributes.$**" : 1 } ){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
product_attributes.$**:代表这个字段后面的字段都会被匹配到
通过匹配符索引 查询 length大于50的数据
1 2 3 4 5 # 查询length 大于50 的数据,由于是product_attributes字段下的,会被通配符索引匹配到 > db.products.find({"product_attributes.size.length":{$gt:50 } })# 查询结果 { "_id" : ObjectId("659ccf6152ef6967235d5313"), "product_name" : "Spy Coat", "product_attributes" : { "material" : [ "Tweed", "Wool", "Leather" ], "size" : { "length" : 72 , "units" : "inches" } } }
通过匹配符索引 查询 material = Tweed 的数据
1 2 3 4 > db.products.find({"product_attributes.material": "Tweed" })# 查询结果 { "_id" : ObjectId("659ccf6152ef6967235d5313"), "product_name" : "Spy Coat", "product_attributes" : { "material" : [ "Tweed", "Wool", "Leather" ], "size" : { "length" : 72 , "units" : "inches" } } }
通过匹配符索引 查询 size.units = inches 的数据
1 2 3 4 > db.products.find({"product_attributes.size.units": "inches" })# 查询结果 { "_id" : ObjectId("659ccf6152ef6967235d5313"), "product_name" : "Spy Coat", "product_attributes" : { "material" : [ "Tweed", "Wool", "Leather" ], "size" : { "length" : 72 , "units" : "inches" } } }
使用通配符索引注意事项
通配符索引不兼容 以下索引,无法与他们一起创建复合索引 。
通配符索引属于稀疏索引,无法索引空字段 ,所以只能查询有字段的文档,不存在字段的文档查询不出来。
通配符索引是为文档或数组 内容本身生成的索引 ,而并非 文档/数组 字段本身。所以无法 精确匹配到文档/数组本身。
4)索引属性 唯一索引(Unique Indexes) 在生产环境中,重复的脏数据必然带来一定的麻烦,为此对于一些要求 唯一的字段,如 订单号,用户名等 可以给他设置成 唯一索引,确保该字段在集合中是唯一的。
插入准备数据
1 2 3 4 5 6 7 8 > db.user.insertMany([{name:"张三"},{name:"李四"}]){ "acknowledged" : true , "insertedIds" : [ ObjectId("659ceff23426724fbba207a6"), ObjectId("659ceff23426724fbba207a7") ] }
创建唯一索引
1 2 3 4 5 6 7 8 # 创建唯一索引的属性是 unique :true > db.user.createIndex({name:1 },{unique :true }){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
再次插入 重复的数据
创建复合唯一索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 准备数据 > db.user.insertMany([{name:"王五",age:23 ,interest:["basketball","computer"]}]){ "acknowledged" : true , "insertedIds" : [ ObjectId("659cf3223426724fbba207ab") ] } # 创建唯一索引的属性是 unique :true > db.user.createIndex({name:1 ,age:20 },{unique :true }){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 2 , "numIndexesAfter" : 3 , "ok" : 1 }
部分索引(Partial Indexes) 创建索引:创建部分索引时,会根据 匹配条件进行匹配,只有满足条件的数据才会建立索引。
使用索引查询:使用部分索引查询,只有满足 创建索引时的匹配条件的情况下才会走 部分索引 。如果强行要求他走部分索引的话,则会出现 丢失数据的情况,因为索引树中 没有 不满足条件的数据。
好处:节省索引树空间和硬盘存储空间,具有更低的创建索引和维护索引成本。
创建部分索引 1 2 3 4 5 6 7 8 9 10 11 12 > db.user.createIndex( { name:1 }, { partialFilterExpression: { age: { $gt: 5 } } } ) # 创建结果 { "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
partialFilterExpression选项接受指定过滤条件:
等式表达式(例如:field: value或使用$eq操作符)
$exists: true
$gt, $gte, $lt , $lte
$type
顶层的$and
查询的时候使用部分索引
1 2 3 4 > db.user.find({name:"王五",age:23 })# 查询结果 { "_id" : ObjectId("659cf3223426724fbba207ab"), "name" : "王五", "age" : 23 , "interest" : [ "basketball", "computer" ] }
在查询的时候,如果想使用部分索引的话,必须满足 创建索引时匹配条件 ,age 大于5 。
不满足匹配条件无法使用部分索引
1 > db.user.find({name:"王五",age:1 })
唯一索引结合部分索引 导致唯一索引失效 注意:当我们创建部分索引时,同时还指定唯一索引的话,只有匹配条件的文档才会触发唯一约束,不满足的话不会触发唯一约束。
1 2 3 4 5 6 7 8 9 10 11 12 > db.user.createIndex( { name:1 }, { unique :true ,partialFilterExpression: { age: { $gt: 5 } } } ) # 创建结果 { "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
查询当前user数据库的所有数据
1 2 3 4 5 6 > db.user.find()# 只有三条文档,张三、李四、王五 { "_id" : ObjectId("659d0eb0ba5c32556ca0c2f1"), "name" : "张三", "age" : 2 } { "_id" : ObjectId("659d0eb5ba5c32556ca0c2f2"), "name" : "李四", "age" : 4 } { "_id" : ObjectId("659d0ebaba5c32556ca0c2f3"), "name" : "王五", "age" : 26 }
插入重复名称的数据
此时我们插入重复name的数据,并且age 匹配条件 小于5,该age 匹配条件匹配不到 创建部分索引的条件,无法触发 唯一约束。
插入重复name,age 满足了 部分索引创建时的匹配条件,所以会触发 唯一约束 ,无法正常插入
插入重复name,age 不满足 匹配条件,则无法触发唯一约束,意外的插入了数据,这不是我们想要的结果
稀疏索引(Sparse Indexes) 稀疏索引,只会对存在的字段创建索引 。查询数据时,走稀疏索引只会查找具有索引的字段。
使用稀疏索引进行查询时,可能会丢失部分数据,因为只对存在的字段创建索引。
如果使用稀疏索引会导致查询和排序 结果集不完整,mongoDB默认不会使用该索引 。除非使用hint()强行指定走索引,但是这样会丢失部分数据。
查询现有数据
1 2 3 4 5 6 > db.user.find(){ "_id" : ObjectId("659d0eb0ba5c32556ca0c2f1"), "name" : "张三", "age" : 2 } { "_id" : ObjectId("659d0eb5ba5c32556ca0c2f2"), "name" : "李四", "age" : 4 } { "_id" : ObjectId("659d0ebaba5c32556ca0c2f3"), "name" : "王五", "age" : 26 } { "_id" : ObjectId("659d0ed1ba5c32556ca0c2f5"), "name" : "王五", "age" : 5 } { "_id" : ObjectId("659d124cba5c32556ca0c2f6"), "name" : "张三", "age" : 6 }
创建稀疏索引
1 2 3 4 5 6 7 8 # 为age创建稀疏索引 > db.user.createIndex( { age: 1 } , { sparse: true } ){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
案例一:
当前的集合中,所有的文档age都是存在数据的,此时去查询必然会走稀疏索引。
案例二:不走稀疏索引
如果在集合中新增几条文档,文档里面不包含age的话,那这几条文档不会走稀疏索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 新增数据,没有age(稀疏索引字段) > db.user.insert({name:"赵六"})WriteResult({ "nInserted" : 1 }) # 查询user 所有文档,不会走稀疏索引,如果强行走稀疏索引,必然会丢失部分数据 > db.user.find() { "_id" : ObjectId("659d0eb0ba5c32556ca0c2f1"), "name" : "张三", "age" : 2 } { "_id" : ObjectId("659d0eb5ba5c32556ca0c2f2"), "name" : "李四", "age" : 4 } { "_id" : ObjectId("659d0ebaba5c32556ca0c2f3"), "name" : "王五", "age" : 26 } { "_id" : ObjectId("659d0ed1ba5c32556ca0c2f5"), "name" : "王五", "age" : 5 } { "_id" : ObjectId("659d124cba5c32556ca0c2f6"), "name" : "张三", "age" : 6 } { "_id" : ObjectId("659d2f1c4baf2e0ec00f658c"), "name" : "赵六" } # 对集合进行排序 > db.user.find().sort({age: -1 }){ "_id" : ObjectId("659d0ebaba5c32556ca0c2f3"), "name" : "王五", "age" : 26 } { "_id" : ObjectId("659d124cba5c32556ca0c2f6"), "name" : "张三", "age" : 6 } { "_id" : ObjectId("659d0ed1ba5c32556ca0c2f5"), "name" : "王五", "age" : 5 } { "_id" : ObjectId("659d0eb5ba5c32556ca0c2f2"), "name" : "李四", "age" : 4 } { "_id" : ObjectId("659d0eb0ba5c32556ca0c2f1"), "name" : "张三", "age" : 2 } { "_id" : ObjectId("659d2f1c4baf2e0ec00f658c"), "name" : "赵六" }
排序结果:
强行指定使用索引
强行指定走索引的话,必然会丢失部分数据,如下图所示,丢失了一条 “赵六” 的数据。因为 “赵六” 的文档没有 age字段。
1 2 3 4 5 6 7 > db.user.find().sort({age: -1 }).hint({age:1 }){ "_id" : ObjectId("659d0ebaba5c32556ca0c2f3"), "name" : "王五", "age" : 26 } { "_id" : ObjectId("659d124cba5c32556ca0c2f6"), "name" : "张三", "age" : 6 } { "_id" : ObjectId("659d0ed1ba5c32556ca0c2f5"), "name" : "王五", "age" : 5 } { "_id" : ObjectId("659d0eb5ba5c32556ca0c2f2"), "name" : "李四", "age" : 4 } { "_id" : ObjectId("659d0eb0ba5c32556ca0c2f1"), "name" : "张三", "age" : 2 }
TTL索引(TTL Indexes) 在生产环境中,并非所有的数据都需要永久存储,对于一些需要定时清理的数据,我们可以通过TTL索引 来实现定期删除功能。
注意:
TTL索引过期之后并不会马上删除 。mongoDB删除过期数据的任务会每60秒执行一次 ,所以会存在时间差。
TTL索引不仅提供了定时删除功能,同时还可以提高 查询效率。
准备数据
1 2 3 4 5 6 7 8 9 10 > db.log.insertOne( { "createTime": new Date (), "logMessage": "Success!" } ) # 插入结果 { "acknowledged" : true , "insertedId" : ObjectId("659e0ca387dfdae46dc69397") }
创建TTL索引
1 2 3 4 5 6 7 8 # 创建 TTL 索引,TTL 值为3600 秒 > db.log.createIndex( { "createTime": 1 }, { expireAfterSeconds: 3600 }){ "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
修改TTL索引过期时间
1 2 3 4 5 6 > db.runCommand({collMod:"log",index:{keyPattern:{createTime:1 },expireAfterSeconds:600 }}){ "expireAfterSeconds_old" : 3600 , "expireAfterSeconds_new" : 600 , "ok" : 1 }
TTL使用注意事项
TTL索引只支持 单键索引(单个字段),并且必须是非 _id字段。
TTL索引过期后不会马上删除,会有60秒的时间差。
TTL删除时 用的是remove命令,如果数据量大的话会对CPU、磁盘产生一定的压力。
隐藏索引(Hidden Indexes) 隐藏索引并不是一个真正意义上的索引,而是索引的一个属性,类似于 停用该索引。
隐藏索引后,查询数据时就不会走该索引。该功能更多的是临时隐藏索引,停止使用该索引,以后需要启用的时候在 取消隐藏。
创建隐藏索引
1 2 3 4 5 6 7 8 # 创建隐藏索引,属性:hidden:true > db.log.createIndex({ logMessage : 1 },{ hidden: true });{ "createdCollectionAutomatically" : false , "numIndexesBefore" : 1 , "numIndexesAfter" : 2 , "ok" : 1 }
隐藏现有索引
1 2 3 4 5 # 隐藏多个,可以用{}隔开 > db.log.hideIndex( { logMessage: 1 } )# 隐藏单个 > db.log.hideIndex( logMessage )
取消隐藏索引
1 2 3 4 5 # 取消隐藏多个,可以用{}隔开 > db.log.unhideIndex( { logMessage: 1 } )# 取消隐藏单个 > db.log.unhideIndex( logMessage )
查看执行计划,发现走索引
5)索引使用建议
为每个集合创建一个合适的索引
对于数据量比较大的集合时,就要合理的创建索引,可以有效的提高查询效率。 如果不创建索引的话,mongoDB会把所有的文档从硬盘读取到内存,这对服务器的压力是很大的。
尽量用复合索引来完成查询,而不是用交叉索引
交叉索引:每个字段都创建单键索引,然后查询时 这些字段都用上去查询就叫交叉索引查询。
尽量通过2-5个复合索引来解决 90%的查询命令,减少单键索引的使用 ,因为单键索引 利用率不高
使用复合索引时,要满足 最左前缀匹配原则 ,否则无法走索引
使用复合索引时,查询时 等值匹配放在前,范围查询放后面
先利用等值匹配筛选结果后完成排序,才通过范围查询,效率比较高
尽可能使用 覆盖索引
覆盖索引并非真正的索引,而是 你需要什么数据,你就返回什么数据,这样 尽可能的不回表在去取数据,等同于MySQL的覆盖索引
创建索引时 尽可能的后台运行
在大数据量的情况下去创建索引时会导致系统阻塞,建议使用后台方式创建索引
避免设计过长的 数组索引
数组索引的内容较多,存储的时候需要更多的空间,影响索引树的查询效率
更新频繁的字段慎用索引
如果字段更新频繁的话,索引树需要频繁的更新信息
对于超长字符串的字段慎用索引
不建议的原因同第七点一样
3. explain执行计划 MongoDB提供了explain命令, 可以让我们直观的看到 查询命令的执行计划,根据实际情况来做出相应优化。
执行计划最主要看的是几个属性:
是否走索引
winningPlan.inputStage.stage = IXSCAN 代表走索引,COLLSCAN 代表走全表扫描
语句执行时间、索引扫描次数
executionStats.executionTimeMillis 语句执行时间
executionStats.totalKeysExamined 索引扫描次数
executionStats.totalDocsExamined 文档扫描次数
注意:聚合管道想要查看执行计划explain,需要在 聚合函数之前先调用explain。其他的命令则是在最末尾加上explain()。
如:db.books.explain().aggregate([ xxx ])
常规语法:
1 db.collection.find().explain(< verbose> )
verbose 为可选参数,默认为queryPlanner。
模式名字
描述
queryPlanner
执行计划的详细信息,包括查询计划、集合信息、查询条件、最佳执行计划、查询方式和服务信息等
exectionStats
最佳执行计划的执行情况和被拒绝的计划等信息
allPlansExecution
选择并执行最佳执行计划,并返回最佳执行计划和其他执行计划的执行情况
queryPlanner 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 # explain不写参数的话,默认就是queryPlanner > db.books.find({title:"book-2"}).explain()# 执行计划返回结果 { "queryPlanner" : { # 执行计划版本号 "plannerVersion" : 1 , # 查询的集合名称 "namespace" : "appDb.books", # 是否使用索引,这里保留个疑问,因为下面明明使用了索引,但是这里却是为false ,初步怀疑该字段的真实性和有效性 "indexFilterSet" : false , "parsedQuery" : { # 查询条件 "title" : { "$eq" : "book-2" } }, "queryHash" : "6E0D6672", "planCacheKey" : "B1CDA929", # 最佳执行计划 "winningPlan" : { # 查询方式 "stage" : "FETCH", "inputStage" : { # 使用的 索引扫描 "stage" : "IXSCAN", "keyPattern" : { "title" : 1 }, "indexName" : "title_1", "isMultiKey" : false , "multiKeyPaths" : { "title" : [ ] }, "isUnique" : false , "isSparse" : false , "isPartial" : false , "indexVersion" : 2 , "direction" : "forward", "indexBounds" : { "title" : [ "[\"book-2 \", \"book-2 \"]" ] } } }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "cvm52851", "port" : 27017 , "version" : "4.4.26", "gitVersion" : "acdc463fe60bdc85aeced25297041b4051a0fc33" }, "ok" : 1 }
字段名称
描述
plannerVersion
执行计划的版本
namespace
查询的集合
indexFilterSet
是否使用索引
parsedQuery
查询条件
winningPlan
最佳执行计划
stage
查询方式
filter
过滤条件
direction
查询顺序
rejectedPlans
拒绝的执行计划
serverInfo
mongodb服务器信息
executionStats executionStats 模式的返回信息中包含了 queryPlanner 模式的所有字段,并且还包含了更为详尽的最佳执行计划执行情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 > db.books.find({title:"book-2"}).explain("executionStats"){ "queryPlanner" : { "plannerVersion" : 1 , "namespace" : "appDb.books", "indexFilterSet" : false , "parsedQuery" : { "title" : { "$eq" : "book-2" } }, "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "title" : 1 }, "indexName" : "title_1", "isMultiKey" : false , "multiKeyPaths" : { "title" : [ ] }, "isUnique" : false , "isSparse" : false , "isPartial" : false , "indexVersion" : 2 , "direction" : "forward", "indexBounds" : { "title" : [ "[\"book-2 \", \"book-2 \"]" ] } } }, "rejectedPlans" : [ ] }, "executionStats" : { "executionSuccess" : true , "nReturned" : 1 , "executionTimeMillis" : 2 , "totalKeysExamined" : 1 , "totalDocsExamined" : 1 , "executionStages" : { "stage" : "FETCH", "nReturned" : 1 , "executionTimeMillisEstimate" : 0 , "works" : 2 , "advanced" : 1 , "needTime" : 0 , "needYield" : 0 , "saveState" : 0 , "restoreState" : 0 , "isEOF" : 1 , "docsExamined" : 1 , "alreadyHasObj" : 0 , "inputStage" : { "stage" : "IXSCAN", "nReturned" : 1 , "executionTimeMillisEstimate" : 0 , "works" : 2 , "advanced" : 1 , "needTime" : 0 , "needYield" : 0 , "saveState" : 0 , "restoreState" : 0 , "isEOF" : 1 , "keyPattern" : { "title" : 1 }, "indexName" : "title_1", "isMultiKey" : false , "multiKeyPaths" : { "title" : [ ] }, "isUnique" : false , "isSparse" : false , "isPartial" : false , "indexVersion" : 2 , "direction" : "forward", "indexBounds" : { "title" : [ "[\"book-2 \", \"book-2 \"]" ] }, "keysExamined" : 1 , "seeks" : 1 , "dupsTested" : 0 , "dupsDropped" : 0 } } }, "serverInfo" : { "host" : "cvm52851", "port" : 27017 , "version" : "4.4.26", "gitVersion" : "acdc463fe60bdc85aeced25297041b4051a0fc33" }, "ok" : 1 }
字段名称
描述
winningPlan.inputStage
用来描述子stage,并且为其父stage提供文档和索引关键字
winningPlan.inputStage.stage
子查询方式
winningPlan.inputStage.keyPattern
所扫描的index内容
winningPlan.inputStage.indexName
索引名
winningPlan.inputStage.isMultiKey
是否是Multikey。如果索引建立在array上,将是true
executionStats.executionSuccess
是否执行成功
executionStats.nReturned
返回的个数
executionStats.executionTimeMillis
这条语句执行时间
executionStats.executionStages.executionTimeMillisEstimate
检索文档获取数据的时间
executionStats.executionStages.inputStage.executionTimeMillisEstimate
扫描获取数据的时间
executionStats.totalKeysExamined
索引扫描次数
executionStats.totalDocsExamined
文档扫描次数
executionStats.executionStages.isEOF
是否到达 steam 结尾,1 或者 true 代表已到达结尾
executionStats.executionStages.works
工作单元数,一个查询会分解成小的工作单元
executionStats.executionStages.advanced
优先返回的结果数
executionStats.executionStages.docsExamined
文档检查数
allPlansExecution allPlansExecution返回的信息包含 executionStats 模式的内容,且包含allPlansExecution,这是最详细的执行计划。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 > db.books.find({title:"book-2"}).explain("allPlansExecution")# 返回结果,这里我故意只显示allPlansExecution部分,其他的我没写进来,因为信息实在太多了 "allPlansExecution" : [ { "nReturned" : < int > , "executionTimeMillisEstimate" : < int > , "totalKeysExamined" : < int > , "totalDocsExamined" :< int > , "executionStages" : { "stage" : < STAGEA> , "nReturned" : < int > , "executionTimeMillisEstimate" : < int > , ... } } }, ... ]
stage状态
状态
描述
COLLSCAN
全表扫描
IXSCAN
索引扫描
FETCH
根据索引检索指定文档
SHARD_MERGE
将各个分片返回数据进行合并
SORT
在内存中进行了排序
LIMIT
使用limit限制返回数
SKIP
使用skip进行跳过
IDHACK
对_id进行查询
SHARDING_FILTER
通过mongos对分片数据进行查询
COUNTSCAN
count不使用Index进行count时的stage返回
COUNT_SCAN
count使用了Index进行count时的stage返回
SUBPLA
未使用到索引的$or查询的stage返回
TEXT
使用全文索引进行查询时候的stage返回
PROJECTION
限定返回字段时候stage的返回
执行计划的stage状态尽量不要出现以下几种情况 :
COLLSCAN 全表扫描
如果在大数据量的集合下,尽量使用索引扫描,不要走 全集合扫描。
SORT(使用了sort 排序 但是没有走index索引)
不合理的SKIP尽量少用
SUBPLA(没有用到index索引的$or 条件)
COUNTSCAN(不使用index索引进行count)