一,什么是索引
数据库保存记录的机制是建立在文件系统上的,索引也是以文件的形式存储在磁盘上,数据库中用到的最多的索引结构就是B树。
索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。
这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。
MongoDB索引的数据结构也是B+树,它能存储一小部分集合的数据,具体来说就是存储集合中建有索引的一个或多个字段的值,而且按照值的升序或降序排列。对于一个查询来说,如果存在合适的索引,MongoDB能够利用这个索引减少文档的扫描数量。
如图所示,查询users所有score值小于30的文档
但是在一个表上建立过多的索引会带来一些问题,索引的建立要花费系统时间,同时索引文件也会占用磁盘空间。如果并发写入的量很大,每个插入的文档都要建立索引,可想而知,性能会较低。因此合理地建立索引时关键,搞清楚哪些字段上需要建立索引、索引以什么样的方式建立,需要对每个查询过程进行分析,才能得出合理的结论。
二,单字段索引
MongoDB默认为所有集合都创建了一个_id
字段的单字段索引,而且这个索引是唯一的,不能被删除,_id
字段作为一个集合的主键,值是唯一的。
ObjectId 是一个12字节 BSON 类型数据,有以下格式:
- 前4个字节表示时间戳
- 接下来的3个字节是机器标识码
- 紧接的两个字节由进程id组成(PID)
最后三个字节是随机数。
MongoDB中存储的文档必须有一个”_id”键。这个键的值可以是任何类型的,默认是个ObjectId对象。
在一个集合里面,每个文档都有唯一的”_id”值,来确保集合里面每个文档都能被唯一标识。
MongoDB采用ObjectId,而不是其他比较常规的做法(比如自动增加的主键)的主要原因,因为在多个 服务器上同步自动增加主键值既费力还费时。
由于 ObjectId 中存储了 4 个字节的时间戳,所以你不需要为你的文档保存时间戳字段,你可以通过 getTimestamp 函数来获取文档的创建时间:
>ObjectId("5349b4ddd2781d08c09890f4").getTimestamp()
//以上代码将返回 ISO 格式的文档创建时间:
ISODate("2014-04-12T21:49:17Z")
“_id”索引是无法删除的。
//如果你执行删除_id字段索引,会返回找不到对应的索引
db.test.dropIndex( "_id" )
{
"nIndexesWas" : 1,
"ok" : 0.0,
"errmsg" : "index not found with name [_id]",
"code" : 27,
"codeName" : "IndexNotFound"
}
现在通过脚本插入测试数据
for(var i=0;i<1000;i++)db.test.insert({name:"gz"+i,city:"gz"});
for(var i=0;i<1000;i++)db.test.insert({name:"hz"+i,city:"hz"});
for(var i=0;i<1000;i++)db.test.insert({name:"bj"+i,city:"bj"});
for(var i=0;i<1000;i++)db.test.insert({name:"sh"+i,city:"sh"});
for(var i=0;i<1000;i++)db.test.insert({name:"sz"+i,city:"sz"});
通过explan()方法进行查询性能分析,分别通过非索引字段和索引字段搜索下面这条记录。
{
"_id" : ObjectId("595bb1087aedeb7d07dc2bb2"),
"name" : "sz991",
"city" : "sz"
}
不使用索引字段查询
db.getCollection('test').find({"name" : "sz991"}).explain("executionStats")
//结果
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.test",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "sz991"
}
},
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$eq" : "sz991"
}
},
"direction" : "forward"
},
"rejectedPlans" : []
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1, //查询选择器匹配的文档数量
"executionTimeMillis" : 11, //查询所需的时间,单位是毫秒
"totalKeysExamined" : 0, //查询过程中扫描的索引数量
"totalDocsExamined" : 5000, //查询过程中扫描的文档数量
"executionStages" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$eq" : "sz991"
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 23,
"works" : 5002,
"advanced" : 1,
"needTime" : 5000,
"needYield" : 0,
"saveState" : 39,
"restoreState" : 39,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 5000
}
},
"serverInfo" : {
"host" : "VICTOR-PC",
"port" : 27017,
"version" : "3.4.4",
"gitVersion" : "888390515874a9debd1b6c5d36559ca86b44babd"
},
"ok" : 1.0
}
可以看到,此时,不使用索引,查询过程中数据库扫描了集合中所有文档,下面通过索引字段搜索。
db.getCollection('test').find({"_id" : ObjectId("595bb1087aedeb7d07dc2bb2")}).explain("executionStats")
//结果
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.test",
"indexFilterSet" : false,
"parsedQuery" : {
"_id" : {
"$eq" : ObjectId("595bb1087aedeb7d07dc2bb2")
}
},
"winningPlan" : {
"stage" : "IDHACK"
},
"rejectedPlans" : []
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 0, //速度很快,将近0
"totalKeysExamined" : 1, //扫描到一个索引
"totalDocsExamined" : 1, //只扫描了一个文档,即直接定位到对应的文档
"executionStages" : {
"stage" : "IDHACK",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keysExamined" : 1,
"docsExamined" : 1
}
},
"serverInfo" : {
"host" : "VICTOR-PC",
"port" : 27017,
"version" : "3.4.4",
"gitVersion" : "888390515874a9debd1b6c5d36559ca86b44babd"
},
"ok" : 1.0
}
现在通过createIndex为name字段创建唯一索引。
createIndex接收可选参数,可选参数列表如下
Parameter | Type | Description |
---|---|---|
background | Boolean | 建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 “background” 可选参数。 “background” 默认值为false。 |
unique | Boolean | 建立的索引是否唯一。指定为true创建唯一索引。默认值为false。 |
name | string | 索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。 |
dropDups | Boolean | 在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 false。 |
sparse | Boolean | 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false。 |
expireAfterSeconds | integer | 指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。 |
v | index version | 索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。 |
weights | document | 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。 |
default_language | string | 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语。 |
language_override | string | 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language。 |
执行命令
db.test.createIndex({name:1},{unique:true})
//结果
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 2,
"note" : "all indexes already exist",
"ok" : 1.0
}
//再次通过name字段搜索上面搜索的文档
db.getCollection('test').find({"name" : "sz991"}).explain("executionStats")
//结果
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.test",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "sz991"
}
},
"winningPlan" : {
//省略部分内容
},
"rejectedPlans" : []
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 3, //查询速度有了很大的提高
"totalKeysExamined" : 1, //使用到了索引
"totalDocsExamined" : 1, //直接找到对应的文档
"executionStages" : {
//省略部分内容
}
},
"serverInfo" : {
"host" : "VICTOR-PC",
"port" : 27017,
"version" : "3.4.4",
"gitVersion" : "888390515874a9debd1b6c5d36559ca86b44babd"
},
"ok" : 1.0
}
现在查看集合中的索引
db.test.getIndexes()
//结果
[
{
"v" : 2, //索引的版本
"key" : { //key表示索引建立在哪个字段上
"_id" : 1 //1表示索引按照升序排列
},
"name" : "_id_", //表示索引唯一的名称
"ns" : "test.test" //索引记录所在的命名空间
},
{
"v" : 2,
"unique" : true,
"key" : {
"name" : 1.0
},
"name" : "name_1",
"ns" : "test.test"
}
]
三,复合索引
MongoDB支持多个字段的复合索引,复合索引支持匹配多个字段的查询。
如下创建复合索引
db.test.createIndex({name:1,city:1})
1,选择键的方向
到目前为止,上面的所有示例使用的索引都是升序的(或者是从最小到最大)。但是,如果在两个(或者更多)查询条件上进行排序,可能需要让索性键的方向不同。对于这个问题,假设要根据name从小到大,city从大到小对集合进行排序。对于这个问题,可是使用复合索引db.test.createIndex({name:1,city:-1})
,这样就可以优化排序。只有基于多个查询条件进行排序时,索引方向才是比较重要的。如果只是基于单一键进行排序,MongoDB可以简单地从相反方向读取索引。只有基于多建排序时,方向才变得重要。
2,使用覆盖索引
覆盖查询是以下的查询:
- 所有的查询字段是索引的一部分
- 所有的查询返回字段在同一个索引中
由于所有出现在查询中的字段是索引的一部分, MongoDB 无需在整个数据文档中检索匹配查询条件和返回使用相同索引的查询结果。
因为索引存在于RAM中,从索引中获取数据比通过扫描文档读取数据要快得多。
db.users.find({name:"gz0"},{name:1,_id:0})
对于上述查询,MongoDB的不会去数据库文件中查找。相反,它会从索引中提取数据,这是非常快速的数据查询。
由于我们的索引中不包括 _id 字段,_id在查询中会默认返回,我们可以在MongoDB的查询结果集中排除它。
以下的查询,不能使用覆盖索引查询:
- 所有索引字段是一个数组
- 所有索引字段是一个子文档
3,隐式索引
复合索引具有双重功能,而且对不同的索引的查询可以表现为不同的索引。如果有一个{name:1,city:1}
索引,name字段会被自动排序,就好像有一个{name:1}
索引一样。因此,这个复合索引就可以当做{name:1}
索性一样使用。
这个就可以根据需要推广到尽可能多的键:如果有一个拥有N个键的索引,那么就可以同时“免费”得到所有这N个键的前缀组成的索引。举例来说,如果有一个{a:1,b:1,c:1,...z:1}
索引,那么,实际上我们也可以使用 {a:1}
、{a:1,b:1}
、{a:1,b:1,c:1}
等一系列索引。
注意:这些键的任意子集所组成的索引并不一定可用。例如,使用{b:1}
或者{a:1,c:1}
作为索引的查询是不会被优化的,只有能够使用索引前缀的查询才能从中受益。
4,操作符使用索引
有一些查询完全无法使用索引,比如$where
查询和检查一个键是否存在的查询({key:{$exists:true}})
。也有其他一些操作不能高效地使用索引。
复合索引时MongoDB能够高效地执行拥有多个语句的查询。设计基于多个字段的索引时,应该将会用于精确匹配的字段(比如{x:"foo"}
)放在索引的前面,将用于范围匹配的字段(比如{y:{$gt:3,$lt:5}}
)放在最后。这样,查询就可以先使用第一个索引键进行精确匹配,然后再使用第二个索引范围在这个结果集内部进行搜索。
5,or查询
or查询其实由两次独立的查询组成。通常来说,执行两此查询再将结果合并的效率不如单次查询高,因此,应该尽可能使用$in
而不是$or
。
四,数组的多键索引
对于某个索引的键,如果这个键在某个文档中是一个数组,那么这个索引就会被标记为多键索引。
如果对一个值为数组类型的字段创建索引,则会默认对数组中的每一个元素创建索引。
因此数组索引的代价比单值索引高:对于单词插入、更新或者删除,每一个数组条目可能都需要更新。
//文档集合
{
"_id" : ObjectId("595bc2a67aedeb7d07dc2bbe"),
"name" : "name",
"age" : 1.0,
"friend" : [
"tom",
"andy",
"leo"
],
"score" : {
"math" : 90.0,
"english" : 80.0,
"chinese" : 90.0
}
}
//对两种不同类型的数组都创建索引
db.users.createIndex({friend:1})
//结果
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 2,
"note" : "all indexes already exist",
"ok" : 1.0
}
db.users.createIndex({score:1})
//结果
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1.0
}
db.users.getIndexes()
//结果
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.users"
},
{
"v" : 2,
"key" : {
"friend" : 1.0
},
"name" : "friend_1",
"ns" : "test.users"
},
{
"v" : 2,
"key" : {
"score" : 1.0
},
"name" : "score_1",
"ns" : "test.users"
}
]
五,查询优化器
MongoDB的查询优化器与其他数据库稍有不同。基本来说,如果一个索引能够精确匹配一个查询(要查询“x”,刚好在“x”上有一个索引),那么查询优化器就会使用这个索引。不然的话,可能会有几个索引都适合你的查询。MongoDB会从这些可能的索引子集中为每次查询计划选择一个,这些查询计划是并行执行的。当挑选出最优的计划后,其他查询的查询计划就会被中止。
这个被选中的查询计划会被缓存,这个查询接下来都会使用它,直到集合数据发生了较大的变动。如果在最初的计划评估之后集合发送了比较大胆数据变动,查询优化器就会重新挑选可行的查询计划。建立索引时,或者是执行多次查询之后,查询优化器都会重新评估查询计划。
explain()输出信息里的rejectedPlans显示了本次查询中被拒绝的每个查询计划。
六,删除索引
索引的删除通过执行集合上的命令dropIndex来删除,比如删除上面的复合索引
db.test.dropIndex("name_1_city_1")
其中,name_1_city_1
为索引的名称。
七,小结
MongoDB可以在一个集合上建立一个或多个索引,而且必须为在字段_id
建立一个索引,建索引的目的与关系数据库一样,就是为了提高对数据库的查询效率;一旦索引创建好,MongoDB会自动地根据数据的变化维护索引,如果索引太大而不能全部保存在内存中,将被移到磁盘文件上,这样会影响查询性能,因此要时刻监控索引的大小,保证合适的索引在内存找那个;监控一个查询是否使用到索引,可以在查询语句后用explain命令。并不是所有的字段都要建立索引,应该根据实际业务所设计的查询,建立合适的索引。
如果系统有大量的写操作,由于需要维护索引的变化,会导致系统性能降低。在对大数据建立索引时最好在后台进行,否则会导致数据库停止响应。要注意虽然在某些字段上建了索引,但是查询时可能用不上索引,如使用$ne
或$num
表示式等。
八,参考资料
菜鸟教程 MongoDB
《大数据存储 MongoDB实战指南》
《MongoDB权威指南》