百度百科上对于索引的定义大概是这样的:在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。
索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文档并选取那些符合查询条件的记录。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。
总结一下:索引是特殊的数据结构,存储在一个易于遍历读取的数据集合中(system.index),是对集合中一列或多列的值进行排序的一种结构。
MongoDB的索引与传统关系型数据库的索引几乎是一模一样,索引的结构都是B树,同样符合最左前缀匹配原则,都是用来提升查询效率的。
前面的文章我们曾说过,每个数据库里有一个保留的集合:system.index,这个集合就是索引的存放位置,用来存放当前数据库库下所有的索引。通过db.system.index查看数据库里的所有索引。
db.system.indexes.find()
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.blog" }
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.article" }
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.student" }
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.version" }
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.ydm" }
上面是我环境test数据库下的所有索引,大家看到除了主键索引,没有其他索引了。下面我们会以该数据库为基准,进行索引的创建、修改、删除。
一、索引的分类
1、主键索引
对于每个集合而言,都有一个_id主键索引,一般情况下,一个集合只会有一个主键索引。
2、唯一索引
唯一索引用来标识该集合下唯一索引的字段值只能有一个,主键索引也是唯一索引。
3、一般索引
在某个列上创建的索引
4、稀疏索引
5、全文索引
二、索引的创建
通过createIndex来完成索引的创建,索引创建的时候可以设置是否为唯一索引、是否在后台创建、组合索引键的排序方式等选项。
索引的创建有两种时机:(1)刚建立集合,集合中的文档数据为空 (2)已经存在文档数据
对于第二种建立时机,会出现索引创建失败的情况,下面会举例说明。
OK,我们通过例子来实践一下。
1、一般索引
db.ydm.createIndex({"name":1,'sex':1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.ydm.getIndexes()
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.ydm" }
{ "v" : 1, "key" : { "name" : 1, "sex" : 1 }, "name" : "name_1_sex_1", "ns" : "test.ydm" }
上面的例子建立了一个{"name":1,"sex":1}的组合索引,这里的1可以理解为升序,-1理解为降序。MongoDB通过设置的-1/1来组织数据。另外,创建索引时如果没有指定索引名字,系统会通过连接索引的字段名和排序顺序生成一个索引名称。索引的名字有大小的限制:不能超过127个字节。对于较复杂的索引,强烈建议自定义索引的名字。
db.ydm.createIndex({"name":1,'sex':1},{'name':'demo'})
2、唯一索引
创建唯一索引较一般索引再加一个选项即可:{'unique':true}
db.ydm.find()
{ "_id" : ObjectId("5b4056fcd3c72e6c8d190cf8"), "name" : "lily", "sex" : 1 }
{ "_id" : ObjectId("5b4056fcd3c72e6c8d190cf9"), "name" : "xiaoming", "sex" : 0 }
{ "_id" : ObjectId("5b40578ad3c72e6c8d190cfa"), "name" : "xiaohong", "sex" : 0, "address" : "beijing", "phone" : "187*****" }
{ "_id" : ObjectId("5b4057ccd3c72e6c8d190cfb"), "name" : "xiaohong", "sex" : 0, "address" : "beijing", "phone" : "187*****" }
{ "_id" : ObjectId("5b4057dbd3c72e6c8d190cfc"), "name" : "xiaohong", "sex" : 0, "address" : "beijing", "phone" : "187*****" }
> db.ydm.createIndex({"phone":1},{'unique':true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"errmsg" : "exception: E11000 duplicate key error index: test.ydm.$phone_1 dup key: { : null }",
"code" : 11000,
"ok" : 0
}
上面我们创建唯一索引出现了错误,如果大家仔细观察数据库里原有数据会发现,第一二条文档数据里是没有phone字段的。MongoDB对于不存在的字段会默认插入null,这样在数据库里就存在两个phone为null的文档了,而我们创建的又是唯一索引,自然也就会造成创建失败。这里我们先删除一条数据重新进行唯一索引的创建。
db.ydm.find()
{ "_id" : ObjectId("5b4056fcd3c72e6c8d190cf9"), "name" : "xiaoming", "sex" : 0 }
{ "_id" : ObjectId("5b40578ad3c72e6c8d190cfa"), "phone" : "131**" }
{ "_id" : ObjectId("5b4057ccd3c72e6c8d190cfb"), "phone" : "152**" }
{ "_id" : ObjectId("5b4057dbd3c72e6c8d190cfc"), "phone" : "183**" }
> db.ydm.createIndex({"phone":1},{'unique':true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}
> db.ydm.getIndexes()
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.ydm" }
{ "v" : 1, "key" : { "name" : 1, "sex" : 1 }, "name" : "name_1_sex_1", "ns" : "test.ydm" }
{ "v" : 1, "unique" : true, "key" : { "phone" : 1 }, "name" : "phone_1", "ns" : "test.ydm" }
上面我们为了演示的方便是采用手动删除具有重复phone的方式。生产环境中如果是在已有文档中创建唯一索引还是有很大可能出现重复值的,这样造成失败的又要如何做呢?难道也是一条一条的删除数据吗?答案是可以采用dropDups选项保留发现的第一个文档而删除接下来有重复值的文档。具体操作如下:
db.ydm.createIndex({"phone":1},{'unique':true,'dropDups':true})
上面我们是对phone字段创建唯一索引,实际中可能对多个字段建立复合唯一索引,这时,只要组合的值是唯一的就可以,单个字段的值文档间允许重复。这里就不在展开了,下面列出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. |
OK,上面我们已经学会了通过system.index集合查看某个数据库下所有的索引,那如果我想看某集合下的索引呢?答案是通过db.集合.getIndexes()完成
db.ydm.getIndexes()
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.ydm"
},
{
"v" : 1,
"key" : {
"name" : 1,
"sex" : 1
},
"name" : "name_1_sex_1",
"ns" : "test.ydm"
},
{
"v" : 1,
"unique" : true,
"key" : {
"phone" : 1
},
"name" : "phone_1",
"ns" : "test.ydm"
}
]
三、查询分析
OK,我们上面已经创建了索引,索引是否真正起到了加速查询的作用呢?有没有工具呢?学过关系型数据库的同学对explain应该不陌生,同样,MongoDB也支持通过explain分析数据查询性能、分析是否用到了索引、扫描行数、检索出来的文档数,通过分析对查询进行优化改进。
我们通过上述创建的索引来学习一下explain的使用。
db.ydm.find({'name':'xiaoming'}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.ydm",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "xiaoming"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : 1,
"sex" : 1
},
"indexName" : "name_1_sex_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"xiaoming\", \"xiaoming\"]"
],
"sex" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "VM_0_14_centos",
"port" : 27017,
"version" : "3.0.6",
"gitVersion" : "1ef45a23a4c5e3480ac919b28afcba3c615488f2"
},
"ok" : 1
}
这个时候有些同学可能会有疑问了:一个集合中可能有多个索引,加入针对某个查询条件有多个索引可以match,MongoDB是如何确定采用哪个索引进行query的呢?我们需要强制MongoDB采用某个索引吗?
其实,MongoDB的查询优化器非常智能,会自动替我们选择哪个索引。初次做某个查询时,查询优化器会同时尝试各种查询方案,最先完成的被确定使用,其他的则被终止掉。查询方案会被记录下来,以备日后应对相同键的查询。查询优化器会定期重试其他方案,以保证方案的最优。
MongoDB的查询优化器已经为我们最好了各种调优,是不是很NB?
四、索引的删除
随着业务的发展,现有的索引不再满足业务诉求,我们需要删除(重建)某集合下的索引。
索引的删除有两种方式:(1)删除集合(2)dropIndex/dropIndexex
第一种方式在之前的文章中有介绍,这里就不再累述了。
dropIndex()方法用于删除指定的索引,dropIndexes()方法用于删除全部的索引。下面举例说明。
db.ydm.dropIndex({'sex':1});
{ "nIndexesWas" : 4, "ok" : 1 }
如果业务变化某个索引不再起作用,一定要及时drop掉。
关于索引,有以下几点需要注意:
(1)每个索引占据一定的存储空间,在进行插入,更新和删除操作时也需要对索引进行操作。如果你很少对集合进行读取操作,建议不使用索引。
(2)索引的创建一定要建在区分度高的字段上。
(3)索引不能被以下的查询使用:正则表达式、算术运算符、where子句
(4)如果没有对没有建立索引的键进行sort,MongoDB会将进行pai所有数据提取到内存中排序,一旦集合大到不能子在内存中排序,MongoDB就会报错。
(5)集合索引个数不能超过64个,索引名称不能多于128个字符
(6)要定期check索引,响应业务变化,及时删除无效索引,重建索引。