不论是关系型数据库还是NoQSL数据库,要获取足够高的查询效率,都需要通过索引来控制,我们首先通过一个电影库的实例来分析一下索引的基础概念。
索引的基础概念
电影库的文档结构如下所示
{
"_id" : ObjectId("5a3672cecff3930a19f5703c"),
"name" : "异形1",
"type" : [
"科幻",
"惊悚"
],
"year" : 1989.0,
"nation" : "美国",
"score" : 4,
"info" : {
"director" : "斯科特",
"stars" : [
"西格妮·韦弗",
"汤姆·斯凯里特"
]
},
"reviews" : [
{
"author" : "jake",
"content" : "good movie",
"score" : 4.0
},
{
"author" : "leon",
"content" : "bad movie",
"score" : 2.0
}
]
}
第一个要明白的问题是:为什么要使用索引,假设电影库中存在了50000条电影信息,我如果希望找到《七宗罪》这个电影,而这个电影刚好在地30222多条,那意味着,我们需要一条条的查找,直到找到这部电影,等于需要查找3w多次,这样的效率显然是很低的,但是如果我们在电影
库中加入一个索引,有如下的信息
异形 0x11
阿甘正传 0x13
.....
七宗罪 0x23
低俗小说 0x43
第一个值是name第二个值是硬盘的位置,这样我们就可以快速的找到《七宗罪》这个电影,索引其实和我们的书的目录是一个道理,但是对于印刷书籍而言,主要的索引都只是基于章节内容来的(但现在的部分书籍,特别是计算机类的书籍,在最后都会加入关键字的索引)。对于数据库而言,我们可以增加很多的索引,对于电影库而言,我们除了需要根据名字来检索,还会涉及到根据类型来检索,所以我们可以为type来增加索引,一个type会对应多个硬盘地址,索引的样子如下所示
悬疑 0x12,0x22,0x33,0x03,...,0x42
惊悚 0x12,0x22,0x37,0x2D,...,0x45
爱情 0x13,0x56,0x77,0xa3,...,0x44
....
这样我们要根据电影的类型来查询就容易得多。已上索引根据电影的名称或者类型来查询都非常的容易,但实际应用中可能存在如下一种可能:我知道这个电影的类型,但是忘记的了电影的名字,但是如果让我看到名字我就可以想起这个电影,如果基于以上的索引,会存在一些问题。
首先如果根据name的索引,由于我们记不住名称,所以查询需要翻遍整个名称,显然是不合理的,由于记住了类型,可以使用基于type的索引,但是我们不得不找到索引的位置之后,还得一条条的读取列表的信息,效率虽然会比基于name的高一些,但依然有改进的空间。
我们可以对两个字段同时建立索引,type和name,先建立type之后建立name,此时就会得到如下的索引信息
悬疑
七宗罪 0x23
致命id 0x36
爱情
阿甘正传 0x13
怦然心动 0xA2
...
这样就可以快速的检索到我们想要的信息,这种索引称之为复合索引,需要注意的是这种索引的顺序一定要注意,如果先添加name之后添加type,就会得不到想要的结果,因为我们根据不清楚电影的名字,所以究竟该创建什么索引一定要根据查询需要来分析和设计,如果盲目的添加太多的索引,会增加内容的维护的成本,效率反而会降低,我们要确保驻留在内存的所有索引都是有效的才能提高查询效率。
最后需要大家了解另外一点,按照上述实例,由于已经创建了复合索引,而且是以type开始,那我们还有没有必要再为type创建一个单独索引呢?显然是不需要的,因为通过这个复合索引已经可以获取type的值的,但是有没有必要为name创建一个单独索引呢,这就是需要的,因为这个复合索引没有办法根据name来检索信息。
索引的建立和效率
索引分为单键索引和复合索引,单键索引只会为一个key创建索引,如果我们为电影的导演建立了索引,又为电影的评分建立了索引,此时我们需要检索某个导演的电影评分高于4分的电影。单键索引如下所示
索引名称info.director | 磁盘地址 | 向下遍历 | 索引名称 score | 磁盘地址 |
---|---|---|---|---|
斯皮尔伯格 | 0x12 | 3 | 0x12 | |
大卫芬奇 | 0xA2 | 4 | 0x11 | |
克里斯托弗诺兰 | 0xB1 | 5 | 0xA5 | |
大卫林奇 | 0x22 | 3 | 0xA0 | |
... | ... | ... | .... |
当执行db.movies.find({info.director:"大卫林奇",scroe:{$gte:3}})
查询时在具体查询的时候,查询优化器首先会根据info.director进行排序,之后根据score排序,然后取两个的交集。
如果使用导演名称和分数来建立复合索引,结构如下所示
索引名称(director-score) | 硬盘地址 |
---|---|
斯皮尔伯格-3 | 0xAA |
大卫芬奇-3 | 0xA2 |
大卫芬奇-4 | 0xB1 |
克里斯托弗诺兰-5 | 0xB2 |
.... |
此时查询优化器通过director很快就可以定位到导演名称,之后从这个位置开始检索分数,效率就高很多,但如果索引的顺序反过来是先建立score再建立info.director,效率就会低得多,如下所示
索引名称(director-score) | 硬盘地址 |
---|---|
3-斯皮尔伯格 | 0xAA |
3-大卫芬奇 | 0xA2 |
4-大卫芬奇 | 0xB1 |
5-克里斯托弗诺兰 | 0xB2 |
.... |
查询优化器首先会找到大于等于3分的所有数据,然后一条条去获取info.director中的数据,这种效率比单键索引还要低很多,所以再次证明,如果要使用复合索引,一定要确定好顺序,否则只会使你的查询效率变得更低。
MongoDB的索引类型
了解了索引的基本知识之后,我们需要了解MongoDB支持的几种索引类型:
1、唯一索引
唯一索引用来确保文档中的key的唯一性,如果为某个字段设置了唯一索引之后,添加了相同的信息,会抛出duplicate key的异常,创建索引的命令
db.user.createIndex({username:1},{unique:true})
2、稀疏索引
按道理来说,索引应该都是密集型的,特别对于关系数据库而言,由于有schema的限制,但是对于MongoDB而言,由于没有schema的限制,每个文档中可能有一些值是null的,有些key也是不存在的,此时如果为字段创建索引,会为所有的null值都创建索引,这样会增加索引的大小,一个比较特别的例子就是一些网站的留言,如果开启了匿名留言,此时有很多用户的id都是null,如果为用户的留言信息增加索引,将会存储大量的null值的多余索引。这种方式就需要创建稀疏索引
db.movies.createIndex({"reviews.author":1},{unique:false,sparse:true})
第二种情况是如果我们为某个key增加了唯一索引,但是这个key有可能存在null的情况,此时如果添加一个文档,第一个该key为null的可以添加,但是第二个为null的就违反了这个约束,就无法添加。诸如用户中如果有个字段foo是唯一的,但是有可能存在null的情况,此时如果希望添加唯一索引,必须设置该索引的sparse为true
db.user.createIndex({foo:1},{unique:true,sparse:true})
3、多键索引
MongoDB支持在一个数组上创建索引,此时会为每个数组中的元素都创建索引,只要检索其中任意一个元素会得到多个索引入口。
{
"name" : "异形1",
"type" : [
"科幻",
"惊悚"
]
}
{
"name" : "七宗罪",
"type" : [
"惊悚",
"犯罪",
"悬疑"
]
}
为type创建了索引之后,当检索"惊悚"这个type时会得到多个索引入口。
4、哈希索引
在MongoDB中默认是使用字符来进行排序的,MongoDB的索引存储结构是基于B-Tree的数据结构,这种结构类似于二叉查找树,但是却支持多个接点,这种存储方式如果整棵树偏向某一个子节点,会使得查询效率变低,如:假设我们一username做了唯一索引,但结果这些用户中基本都是s-z开头的人特别多,这就会使得这颗子树的节点偏多,查询效率会有所降低,此时我们就可以设置这个索引为哈希索引,哈希索引会将每个值利用哈希算法来重新编码,让整棵树平衡,这样可以提高查询的效率。
另外就是对于objectId而言,由于都是基于时间来生成的,看下面这些id
{
"_id" : ObjectId("5a3672cecff3930a19f5703c")
}
{
"_id" : ObjectId("5a3672cecff3930a19f5703d")
}
{
"_id" : ObjectId("5a3672cecff3930a19f5703e")
}
这种id非常类似,在后面介绍的分布式时,这些数据会存储到一台机器上,这是非常有危害的,如果某个时刻有大量的插入请求,此时就意味着是一台机器来承受所有的压力,而哈希索引可以解决这种问题
db.users.createIndex({"_id":'hashed'})
5、地理空间索引
MongoDB支持基于位置的经纬度来建立索引,诸如在找位置相关的信息时有所帮助。
索引管理
MongoDB使用createIndex()方法创建索引,索引创建完成之后通过db.collection.getIndexes()可以查询该collection中存在的索引信息。
>db.user.createIndex({username:1},{unique:true})
>db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "document.user"
},
{
"v" : 2,
"unique" : true,
"key" : {
"username" : 1.0
},
"name" : "username_1",
"ns" : "document.user"
}
]
我们发现有两个索引,一个是基于_id的,v表示版本信息,key表示对哪个字段添加索引,name是索引的名称,ns表示索引的名称空间,是基于document数据库中的user这个collection来创建索引。
使用dropIndex(indexName)可以删除一个索引
>db.user.dropIndex("username_1")
>db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "document.user"
}
]
下面我们将讨论一下,究竟该在什么时候构建索引,当然最理想的肯定是在创建表的时候创建索引,这样就会以增量的方式递增,但是事实往往不是这样理想,因为查询的问题都只会在数据量大的时候才会体现出问题,所以很多时候都需要在后期的项目运营过程之中来调整和优化索引,这所带来的问题就是,新构建索引会占用掉大量的时间,一般都建议在访问量较小的时候来处理这个操作,一般构建索引的时候会占用写锁,此时如果希望用户可以继续访问数据,可以选择后台构建索引。
db.test.createIndex({foo:1,bar:1},{background:true})
构建索引时会消耗大量的内存,对项目的运行的性能影响很大,此时我们可以考虑使用离线索引,离线索引一般用在分布式的环境中,通常可以将数据复制到一个接点,在那个节点上进行离线索引的构建,构建完成之后将此接点切换为主节点,继续在另外一台服务器上进行索引的构建。这些知识在后面的章节再来详细介绍。
另外就是如果进行了大量的修改,删除操作,难免会存在很多索引碎片,这些索引碎片没有用,但依然会占用内存,所以此时可以通过reIndex重建索引,重建索引时也是写锁定的。索引使用时也需要格外慎重。
db.test.reIndex()
索引的基本操作就是这么多,但是我们需要掌握的技术是,如何根据性能来设计和优化索引,下一部分将会详细介绍一套查询优化的方法。