MongoDB索引
一、Mongo语句分析
为集合选择合适的索引是提高性能的途径之一,但是在搞明白如何正确使用索引之前必须知道一条Mongo语句是如何被执行的。
使用 db.collection.find().explain() 可以查看MongoDB在查询过程中做的事情。该语句并不会真的去执行Mongo语句,而是针对语句进行执行计划分析并选出最优计划。
db.order_data.find().explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "order_data.order_data",
"indexFilterSet" : false,
"parsedQuery" : {
},
"winningPlan" : {
"stage" : "COLLSCAN",
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "localhost",
"port" : 27017,
"version" : "4.0.9",
"gitVersion" : "fc525e2d9b0e4bceff5c2201457e564362909765"
},
"ok" : 1
}
可以看到这个查询分析大致分为三个部分
{
"queryPlanner" : {},
"serverInfo" : {},
"ok" : 1
}
参数 含义 描述 queryPlanner 查询计划 queryPlanner.plannerVersion 查询计划版本 queryPlanner.namespace 要查询的集合 queryPlanner.indexFilterSet 是否使用索引过滤器 queryPlanner.parsedQuery 解析查询 由于本例未指定查询条件故为空 queryPlanner.winningPlan 查询优化器选择的查询计划 一般来说一个索引能精确匹配一个查询,那么查询优化器就会使用这个索引,即该参数描述的是查询优化器最终选择的查询计划 queryPlanner.winningPlan.stage 最优查询计划的阶段 这个翻译有些生硬,但是我们看到这个参数的值就不难理解了,本例中为"COLLSCAN",即全集合扫描(一看就不是什么好stage),这个值非常重要并且拥有多个模式,将在后文中进行详解 queryPlanner.winningPlan.direction 语句查询顺序 本例子未指定升序/降序,默认为升序(修改时间) queryPlanner.rejectedPlans 查询优化器放弃的查询计划 如果好几个索引适合一条查询,那么查询优化器会并行这些查询计划,最早返回100个结果的即为优胜的查询计划,其他计划即为rejectedPlans,本例中没有被终止的查询计划所以为空 serverInfo 服务器信息 serverInfo.host 主机 serverInfo.port 端口号 serverInfo.version MongoDB版本号 serverInfo.gitVersion git版本信息 ok 说明这个查询计划是没毛病的
db.collection.find().explain()有三个参数(默认是queryPlanner),分别是
queryPlanner 概要模式
executionStats 执行状态模式
allPlansExecution 所有信息模式
栗子
db.order_data.find().explain('queryPlanner')
重点关注下条语句
db.order_data.find().explain('executionStats')
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "order_data.order_data",
"indexFilterSet" : false,
"parsedQuery" : {
},
"winningPlan" : {
"stage" : "COLLSCAN",
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 61759,
"executionTimeMillis" : 26,
"totalKeysExamined" : 0,
"totalDocsExamined" : 61759,
"executionStages" : {
"stage" : "COLLSCAN",
"nReturned" : 61759,
"executionTimeMillisEstimate" : 20,
"works" : 61761,
"advanced" : 61759,
"needTime" : 1,
"needYield" : 0,
"saveState" : 482,
"restoreState" : 482,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 61759
}
},
"serverInfo" : {
"host" : "localhost",
"port" : 27017,
"version" : "4.0.9",
"gitVersion" : "fc525e2d9b0e4bceff5c2201457e564362909765"
},
"ok" : 1
}
参数 含义 描述 executionStats 查询计划 executionStats.executionSuccess 语句执行结果 executionStats.nReturned 返回的集合数量 executionStats.executionTimeMillis 执行耗时(单位:毫秒) executionStats.totalKeysExamined 扫描的总索引条目数 executionStats.totalDocsExamined 查询执行期间检查的总文档数 executionStats.executionStages 最优查询计划完成执行细节阶段树 executionStats.executionStages.stage 最优查询计划的阶段 executionStats.executionStages.nReturned 返回的集合数量 executionStats.executionStages.executionTimeMillisEstimate 预估执行耗时(单位:毫秒) executionStats.executionStages.works 查询执行阶段执行的“工作单元”的数量 查询执行阶段将其工作分为小的“工作单元”。“工作单元”可能包括检查单个索引键,从集合中提取单个文档,将投影应用于单个文档或执行内部记账。 executionStats.executionStages.advanced 返回到父阶段的结果数 executionStats.executionStages.needTime 未将中间结果推进到其父级的工作循环数 executionStats.executionStages.needYield 本次查询暂停的次数 为了能让写入的请求顺利执行,查询会周期性地释放它的锁,本例中没有写入,所以查询没有暂停过 executionStats.executionStages.saveState 查询阶段挂起处理并保存其当前执行状态的次数 executionStats.executionStages.restoreState 查询阶段恢复保存的执行状态的次数 executionStats.executionStages.isEOF 执行阶段是否已到达流的结尾 executionStats.executionStages.invalidates 我猜是在查询期间被删除的文档 executionStats.executionStages.direction 语句查询顺序 executionStats.executionStages.docsExamined 指定在查询执行阶段扫描的文档数
db.order_data.find().explain('allPlansExecution')
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "order_data.order_data",
"indexFilterSet" : false,
"parsedQuery" : {
},
"winningPlan" : {
"stage" : "COLLSCAN",
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 62162,
"executionTimeMillis" : 27,
"totalKeysExamined" : 0,
"totalDocsExamined" : 62162,
"executionStages" : {
"stage" : "COLLSCAN",
"nReturned" : 62162,
"executionTimeMillisEstimate" : 20,
"works" : 62164,
"advanced" : 62162,
"needTime" : 1,
"needYield" : 0,
"saveState" : 485,
"restoreState" : 485,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 62162
},
"allPlansExecution" : [ ]
},
"serverInfo" : {
"host" : "localhost",
"port" : 27017,
"version" : "4.0.9",
"gitVersion" : "fc525e2d9b0e4bceff5c2201457e564362909765"
},
"ok" : 1
}
参数 含义 描述 executionStats.executionStats.allPlansExecution 所有查询计划执行情况 本例中没有被终止的查询计划
这里我们截取几个对性能分析比较直观的参数
executionStats.executionTimeMillis 执行耗时(单位:毫秒) executionStats.totalKeysExamined 扫描的总索引条目数 executionStats.totalDocsExamined 查询执行期间检查的总文档数 executionStats.executionStages.stage
我们可以用以下方式直接查询我们需要的参数
db.order_data.find().explain('executionStats').executionStats.executionTimeMillis
30
二、MongoDB索引
1、索引操作
使用 db.collection.getIndexes() 可以查看该集合中建立了哪些索引
db.order_data.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "order_data.order_data"
},
{
"v" : 2,
"key" : {
"hotel_id" : 1
},
"name" : "hotel_id_1",
"ns" : "order_data.order_data",
"background" : true
},
{
"v" : 2,
"key" : {
"channel_id" : 1
},
"name" : "channel_id_1",
"ns" : "order_data.order_data",
"background" : true
},
{
"v" : 2,
"key" : {
"check_in_date" : -1
},
"name" : "check_in_date_-1",
"ns" : "order_data.order_data",
"background" : true
},
{
"v" : 2,
"key" : {
"check_out_date" : -1
},
"name" : "check_out_date_-1",
"ns" : "order_data.order_data",
"background" : true
},
{
"v" : 2,
"key" : {
"newest" : 1
},
"name" : "newest_1",
"ns" : "order_data.order_data",
"background" : true
},
{
"v" : 2,
"key" : {
"book_flow" : 1
},
"name" : "book_flow_1",
"ns" : "order_data.order_data",
"background" : true
},
{
"v" : 2,
"key" : {
"order_id" : 1
},
"name" : "order_id_1",
"ns" : "order_data.order_data",
"background" : true
},
{
"v" : 2,
"key" : {
"order_status_key" : 1
},
"name" : "order_status_key_1",
"ns" : "order_data.order_data",
"background" : true
},
{
"v" : 2,
"key" : {
"num_person" : 1
},
"name" : "num_person_1",
"ns" : "order_data.order_data",
"background" : true
},
{
"v" : 2,
"key" : {
"rent_price" : 1
},
"name" : "rent_price_1",
"ns" : "order_data.order_data",
"background" : true
}
]
使用 db.collection.createIndex({“key”:1}) 可以创建创建一个普通索引
db.order_data.createIndex({"order_id":1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
使用 db.collection.dropIndex() 可以删除指定索引
db.order_data.dropIndex("order_id_1")
{ "nIndexesWas" : 2, "ok" : 1 }
使用 db.collection.dropIndexes() 可以删除所有索引
db.order_data.dropIndexes()
{
"nIndexesWas" : 14,
"msg" : "non-_id indexes dropped for collection",
"ok" : 1
}
但是无论怎么删都是无法删除_id建立的索引
2、索引类型
普通索引
普通索引也被称为单键索引,是MongoDB索引中最普通的索引,使用方式在索引基本操作中已经介绍了,这里不再赘述。值得一提的是 db.collection.createIndex({“key”:1}) 中key的值,使用"1"代表升序,使用"-1"代表降序。
[
{
"v" : 2,
"key" : {
"check_in_date" : -1
},
"name" : "check_in_date_-1",
"ns" : "order_data.order_data",
"background" : true
},
{
"v" : 2,
"key" : {
"check_out_date" : -1
},
"name" : "check_out_date_-1",
"ns" : "order_data.order_data",
"background" : true
}
]
这是刚才索引操作中的栗子,根据查询订单入离时间建立的倒序普通索引,依照需求,查询的订单往往都是下单时间离现在比较近的,也就是说在大部分场景下的查询倒序可以更快的返回我们需要的结果。
唯一索引
唯一索引顾名思义,可以确保集合中每个文档的指定键都是唯一的,最最典型的就是"_id"字段,可以使用 db.collection.createIndex({“key”:1},{“unique”:true}) 创建。
db.order_data.createIndex({"order_id":1},{"unique":true})
复合索引
使用 db.collection.createIndex({“key1”:1, “key2”:1}) 就创建了复合索引,当然如果加上唯一限制 db.collection.createIndex({“key1”:1, “key2”:1},{“unique”:true}) 就变成了复合唯一索引
db.order_data.createIndex({"channel":1, "order_id":1})
db.order_data.createIndex({"channel":1, "order_id":1},{"unique":true})
讲到这里顺便一提隐式索引
如果你创建了一个形如…{“a”:1},{“b”:1},{“c”:1}…的复合索引,那同时你也喜提…{“a”:1}…/…{“a”:1},{“b”:1}…
别问我…{“b”:1},{“c”:1}…和…{“a”:1},{“c”:1}…能不能用,问就不能,除非你建
地理空间索引
MongoDB支持的最常见的地理空间索引是2dsphere索引和2d索引,前者用于地球表面类型的地图(曲面),后者用于平面地图和时间连续的数据(平面),2d索引用于球体表面会出现大量的扭曲变形。
GeoJSON
点,可以用形如 [longitude,latitude] ([经度,纬度])表示
{
"name":"zhonghe",
"loc":{
"type":"Point",
"coordinates":[50,2]
}
}
{
"name":"fu River",
"loc":{
"type":"Line",
"coordinates":[[5,2],[3,2]]
}
}
{
"name":"chengdu",
"loc":{
"type":"Polygon",
"coordinates":[[5,2],[3,2],[3,9]]
}
}
建立2dsphre索引
db.city.createIndex({"loc":"2dsphere"})
地理空间查询的类型
可以使用多种不同类型的地理空间查询:交集(intersection)、包含(within)以及接近(nearness)。查询时,需要将希望查找的内容指定为形如
{"$geometry" : geoJsonDesc}
的GeoJSON对象。
现在我们试着查询一下
var zhonghe = {
"type":"Polygon",
"coordinates":[
[-73.9917900,40.7254100],
[-73.9913900,40.7211900],
[-73.9927900,40.7212100],
[-73.9931100,40.7253300],
[-73.9952200,40.7251100]
]
}
db.city.find({"loc":{"$geoIntersects":{"$geometry":zhonghe}}})
db.city.find({"loc":{"$within":{"$geometry":zhonghe}}})
db.city.find({"loc":{"$near":{"$geometry":zhonghe}}})
注:“
g
e
o
I
n
t
e
r
s
e
c
t
s
"
和
"
geoIntersects"和"
g e o I n t e r s e c t s " 和 " within"的使用不需要创建地理空间索引,但是”$near"需要。建议用于表示地理位置的字段都建立地理空间索引来提高查询速度。
复合地理空间索引
地理空间索引可以和其他字段一起使用,例如,中和的商户类型用"tags"字段维护,我们想查询中和的KTV
db.city.createIndex({"tags":1, "location":"2dsphere"})
db.city.find({"loc":{"$within":{"$geometry":zhonghe}},"tags":"KTV"})
如果你想了解更多关于地理空间索引的内容可以点击:http://www.mongoing.com/mongodb-geo-index-1/
多键索引
对于某个索引的键,如果在某个文档中是一个数组,那么这件就会被标记为多键索引
全文索引
使用 db.collection.createIndex({“key1”:“text”}) 可以创建全文索引
使用 db.collection.createIndex({“key1”:“text”,“key2”:“text”}) 可以创建复合全文索引,与复合普通索引不一样,复合全文索引字段顺序并不重要,每个字段都被同等对待,可以为每个字段分配不同权重来控制字段的相对重要性
db.order_data.createIndex({"channel":"text","order_date":"text"},{"weights":{"channel":1,"order_date":2}})
行,这个索引就介绍这么多,主要是不支持中文
TTL索引
TTL(time to live)索引,即具有生命周期的索引,这种索引允许为每一个文档设置一个超时时间,时间一到就会被自动删除,这种索引一般用于解决缓存问题(如会话)。
db.magazine.createIndex({"create_time":1},{"expireAfterSeconds": 3600})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}
使用时应该注意的问题
TTL索引不能基于已存在的索引创建 TTL索引不能以非日期字段创建(可以创建成功但不会生效) TTL索引不支持定长集合(固定集合,集合大小是固定的,类似循环队列,如果集合被占满,插入新文档会将最老的文档从集合中移除) TTL索引不支持多字段创建(复合索引)
3、索引属性
覆盖索引
什么是覆盖索引?覆盖索引可以用来干什么?
先思考一个问题?我们使用MySQL的时候常常说不要" SELECT * ",要指定被查询的字段,那么在MongoDB中是否也是如此呢?
语句一:不指定字段
db.order_data.find({"channel_id":4}).explain("executionStats").executionStats.executionTimeMillis
语句二:指定所有字段
db.order_data.find(
{"channel_id":4},
{
"_id" : 1,
"username" : 1,
"channel_id" : 1,
"book_flow" : 1,
"order_status_key" : 1,
"hotel_id" : 1,
"order_id" : 1,
"landlord_id" : 1,
"check_in_date" : 1,
"check_out_date" : 1,
"order_date" : 1,
"last_update_time" : 1,
"total_price" : 1,
"online_pay_price" : 1,
"rent_price" : 1,
"clean_price" : 1,
"damages_price" : 1,
"online_service_price" : 1,
"submit_mobile" : 1,
"submit_mobile_area_code" : 1,
"submit_name" : 1,
"guest_name" : 1,
"room_num" : 1,
"num_person" : 1,
"other_info" : 1,
"newest" : 1,
"crawl_time" : 1
}
).explain("executionStats").executionStats.executionTimeMillis
对比语句一和语句二的执行时间
\ 语句一执行时间(单位:毫秒) 语句二执行时间(单位:毫秒) 1 44 97 2 40 90 3 42 90 4 44 92 5 47 100 6 42 74 7 37 74 8 42 83 9 34 73 10 38 87 平均 41 86
可以看到在MongoDB中指定字段并没有让查询速度更快,反而更慢了
语句三:随意指定一个字段
db.order_data.find(
{"channel_id":4},
{
"_id" : 0,
"order_date" : 1
}
).explain("executionStats").executionStats.executionTimeMillis
对比三条语句执行时间对比
\ 语句一执行时间(单位:毫秒) 语句二执行时间(单位:毫秒) 语句三执行时间(单位:毫秒) 1 44 97 94 2 40 90 80 3 42 90 82 4 44 92 79 5 47 100 90 6 42 74 98 7 37 74 101 8 42 83 105 9 34 73 95 10 38 87 79 平均 41 86 90.3
可以看到指定一个字段查询和指定所有字段查询没有什么区别甚至还更慢了
语句四:指定索引字段
db.order_data.find(
{"channel_id":4},
{
"_id" : 0,
"channel_id" : 1
}
).explain("executionStats").executionStats.executionTimeMillis
\ 语句一执行时间(单位:毫秒) 语句二执行时间(单位:毫秒) 语句三执行时间(单位:毫秒) 语句四执行时间(单位:毫秒) 1 44 97 94 26 2 40 90 80 23 3 42 90 82 22 4 44 92 79 23 5 47 100 90 22 6 42 74 98 22 7 37 74 101 22 8 42 83 105 23 9 34 73 95 26 10 38 87 79 24 平均 41 86 90.3 23.3
可以看到只指定索引字段的查询速度惊人,那么这是为什么呢?
db.order_data.find(
{"channel_id":4},
{
"_id" : 0,
"channel_id" : 1
}
).explain("executionStats")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "order_data.order_data",
"indexFilterSet" : false,
"parsedQuery" : {
"channel_id" : {
"$eq" : 4
}
},
"winningPlan" : {
"stage" : "PROJECTION",
"transformBy" : {
"_id" : 0,
"channel_id" : 1
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"channel_id" : 1
},
"indexName" : "channel_id_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"channel_id" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"channel_id" : [
"[4.0, 4.0]"
]
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 30722,
"executionTimeMillis" : 23,
"totalKeysExamined" : 30722,
"totalDocsExamined" : 0,
"executionStages" : {
"stage" : "PROJECTION",
"nReturned" : 30722,
"executionTimeMillisEstimate" : 20,
"works" : 30723,
"advanced" : 30722,
"needTime" : 0,
"needYield" : 0,
"saveState" : 240,
"restoreState" : 240,
"isEOF" : 1,
"invalidates" : 0,
"transformBy" : {
"_id" : 0,
"channel_id" : 1
},
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 30722,
"executionTimeMillisEstimate" : 20,
"works" : 30723,
"advanced" : 30722,
"needTime" : 0,
"needYield" : 0,
"saveState" : 240,
"restoreState" : 240,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"channel_id" : 1
},
"indexName" : "channel_id_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"channel_id" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"channel_id" : [
"[4.0, 4.0]"
]
},
"keysExamined" : 30722,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
},
"serverInfo" : {
"host" : "localhost",
"port" : 27017,
"version" : "4.0.9",
"gitVersion" : "fc525e2d9b0e4bceff5c2201457e564362909765"
},
"ok" : 1
}
...
"totalKeysExamined" : 30722, // 扫描的索引数量
"totalDocsExamined" : 0, // 扫描的文档数量
...
说明这条查询语句仅仅根据索引就返回了查询的结果,并没有查询任何文档
所谓覆盖索引就是无需实际查看文档内部就能匹配查询条件和返回使用相同索引的结果
使用覆盖索引也是有条件的
查询所有的字段是索引的一部分 查询的所有返回字段都在同一个索引中
假设有个需求是根据渠道订单号查询入住人信息,那么给渠道订单编号和入住人信息建立一个复合索引就非常合适了。
稀疏索引
稀疏索引就是只包含有索引字段的文档的条目,跳过索引键不存在的文档
举个栗子。假如你有一张用户表,用户注册后需要实名认证(实名认证必须唯一),但是有部分用户在注册完成之后并没有实名认证,在MongoDB中该字段存储为null,有很多个null导致创建唯一索引失败了,此时使用稀疏索引可以完美解决这个问题。
db.user.createIndex({real_name_authentication:1},{"sparse":true})
后台创建索引
新建索引是一件既费时又费资源的事情,创建索引过程中,MongoDB会阻塞数据库其他操作一直到索引创建完成。如果希望MongoDB在创建索引的同时又能够处理读写请求,可以指定background后台方式创建索引。
db.user.createIndex({"key":1},{"background":true})
3、正确地使用索引
可以参照的stage组合
以下是常见stage(不全)
stage description COLLSCAN 集合扫描 IXSCAN 索引扫描 FETCH 根据索引去检索指定文档 SHARD_MERGE 合并分片中结果 SHARDING_FILTER 分片中过滤掉孤立文档 LIMIT 使用limit 限制返回数 SORT 在内存中进行了排序 PROJECTION 使用 skip 进行跳过 IDHACK 针对_id进行查询 COUNT 利用db.coll.explain().count()之类进行count运算 COUNTSCAN count不使用Index进行count时的stage返回 COUNT_SCAN count使用了Index进行count时的stage返回 SUBPLA 未使用到索引的$or查询的stage返回 TEXT 使用全文索引进行查询时候的stage返回 PROJECTION 限定返回字段时候stage的返回
explain() 希望看到的阶段
FETCH+IDHACK FETCH+IXSCAN LIMIT+(FETCH+IXSCAN) PROJECTION+IXSCAN SHARDING_FILTER+IXSCAN COUNT_SCAN
explain() 不希望看到的阶段
COLLSCAN(全表扫描) SORT(使用sort但是没有使用索引) 不合理的SKIP SUBPLA(不能使用索引的情形) COUNTSCAN
不要使用低效率的操作符
不能使用索引的操作符尽量不要使用,例如,“
w
h
e
r
e
"
以
及
where"以及
w h e r e " 以 及 nin”(不在什么里)等 不能高效地使用索引的操作符尽量少用,例如,“
e
x
i
s
t
s
"
(
存
在
)
、
"
exists"(存在)、"
e x i s t s " ( 存 在 ) 、 " ne”(取反)、
n
o
t
"
(
不
是
)
以
及
范
围
查
询
的
操
作
符
(
not"(不是)以及范围查询的操作符(
n o t " ( 不 是 ) 以 及 范 围 查 询 的 操 作 符 ( gt,$lte)"等等
一些建议
并没有一个严格的规则告诉我们如何根据数据大小、索引大小和文档大小等等因素判断什么时候使用索引有用,什么时候使用索引没用,非要有个标准可以参照下表, 实际请根据业务情况选择合适的索引
索引适用的情况 索引不适用的情况 集合较大 集合较小 文档较大 文档较小 选择性查询 非选择性查询
通常,索引基数(集合中某个字段拥有不同值的数量)高的字段建立索引的价值较高 绝大多数MySQL/Oracle/SQLite索引的技巧也同样适用与MongoDB