MongoDB查询优化之路:认识索引并使用

在使用MongoDB查询大量数据时,适当的添加索引可以极大的提高查询效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。本文将带你认识索引,并介绍它的简单使用。

一、索引操作

1、创建索引

1
db.collection.createIndex(keys, options)

MongoDB中1代表升序,-1代表降序。

创建name字段的升序索引:

1
db.collection.createIndex({ 'name': 1 })

创建name字段的升序和createTime字段的降序索引:

1
db.collection.createIndex({ 'name': 1,'createTime': -1 })

第二个参数options接收如下参数:

参数 类型 描述
background Boolean 建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 “background” 可选参数。 “background” 默认值为false。
unique Boolean 建立的索引是否唯一。指定为true创建唯一索引。默认值为false.
name string 索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。如:key_1、key_-1、key_text。
sparse Boolean 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false.
expireAfterSeconds integer 指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。
v index version 索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。
weights document 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。
default_language string 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语
language_override string 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language.

其中需要关注的参数:backgrounduniqueexpireAfterSeconds,并且索引默认是区分大小写的。

在后台创建索引:

1
db.collection.createIndex({ 'name': 1,'createTime': -1 }, { background: true })

前台操作,它会阻塞用户对数据的读写操作直到索引构建完毕;后台模式,不阻塞数据读写操作,独立的后台线程异步构建索引,此时仍然允许对数据的读写操作。创建索引时一定要写{ background: true }

创建唯一索引:

1
db.collection.createIndex({ 'name': 1 }, { unique: true })

唯一索引是索引具有的一种属性,让索引具备唯一性,确保这张表中,该条索引数据不会重复出现。在每一次insert和update操作时,都会进行索引的唯一性校验,保证该索引的字段组合在表中唯一。

在创建索引后,180 秒左右删除。

1
db.collection.createIndex({ 'createTime': -1 }, { expireAfterSeconds: 180 })

需要注意,使用expireAfterSeconds选项时候,索引关键字段必须是 Date 类型,只支持单字段索引,删除操作非立即执行,默认60秒扫描一次Document数据。

2、查看索引

1
db.collection.getIndexes()

返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "db.collection"
},
{
"v" : 2,
"key" : {
"name" : 1
},
"name" : "name_1",
"ns" : "db.collection",
"background" : true
}
]

说明db.collection中有两个索引,一个是_id升序索引,另一个是name升序索引。

查看索引大小

1
db.collection.totalIndexSize()

索引是会占据磁盘空间,大数据的索引往往需要查看空间大小。

3、删除索引

1
2
3
4
5
db.collection.dropIndexes() //删除所有所有, _id 索引会除外

db.collection.dropIndex('name') //删除上述name索引

db.collection.dropIndex({ 'name': 1}) //删除name升序索引

4、重建索引

1
db.collection.reIndex()

一般是在collection经过很多次修改后,导致collection的文件产生空洞,这时候就会使用到这个方法,通过索引的重建,减少索引文件碎片,并提高索引的效率。

但是重建索引需要遍历整个collection,在数据量很大的情况下,这个过程会非常的慢。

5、修改索引

若要修改现有索引,则需要删除现有索引并重新创建索引。

二、索引类型

1、单键索引(Single Field)

单键索引(Single Field)

1
{ key: 1 }

在默认情况下,所有collection在 _id 字段上都有一个索引,应用程序和用户可以添加额外的索引来支持重要的查询操作。

对于单字段索引和排序操作,索引键的排序顺序(即升序或降序)无关紧要,因为 MongoDB 可以在任意方向上遍历索引。

2、复合索引(Compound Index)

复合索引(Compound Index)

1
{ key1: 1, key2: 1 }

复合索引就是多个字段一起匹配,需要注意的是,在建立复合索引的时候一定要注意顺序的问题,顺序不同将导致查询的结果也不相同

3、多键值索引(Multikey Index)

多键值索引(Multikey Index)

1
{ 'key.sub_key': 1 }

也被称为”数组索引”,可以对包含数组的字段建立索引。

MongoDB会为数组中的每个元素创建索引键,这些多键值索引支持对数组字段的高效查询。

对数组建立索引的代价是非常高的,它实际上是会对数组中的每一项都单独建立索引,就相当于假设数组中有十项,那么就会在原基础上,多出十倍的索引大小。所以在MongoDB中是禁止对两个数组添加复合索引的,对两个数组添加索引那么索引大小将是爆炸增长。

4、地理位置索引(Geospatial Index)

1
2
{ key: '2d' }
{ key: '2dsphere' }

对于保存的经纬度数据字段中,建立地理位置索引可以高效的实现查询,比如说”查找附近的人”,”查找附近的商家”等。

MongoDB提供两种索引:2D索引和2D球面索引,一种是以平面几何的结果输出,另一种是以球面几何的结果输出。目前这类索引在工作中使用较少,就不展开介绍了。

5、全文索引(Text Indexes)

1
{ key: 'text' }

全文检索对每一个词建立一个索引,指明该词在collection中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

使用全文索引查找关键词:mongodb

1
db.collection.find({ $text: { $search: 'mongodb' } })

全文索引可以支持中文,但是对于分词的能力太弱,想要实现中文模糊搜索,还是建议使用elasticsearch或者Sphinx,或者lucene。

例如有这样一条记录:(省略_id)

1
{ author: '李白', title: '静夜思', article: '床前明月光,疑是地上霜。 举头望明月,低头思故乡。' }

建立全文索引:

1
db.collection.createIndex({ author: 'text', description: 'text' })

搜索”李白”:

1
2
db.collection.find({ $text: { $search: '李白' } })
// 返回:{ author: '李白', title: '静夜思', article: '床前明月光,疑是地上霜。 举头望明月,低头思故乡。' }

搜索”李”:

1
2
db.collection.find({ $text: { $search: '李' } })
// 无结果返回

6、哈希索引(Hashed Indexes)

1
{ key: 'hashed' }

是指按照某个字段的hash值来建立索引,它的速度比普通索引快,但是无法进行范围查询进行优化,适宜于随机性强的散列。

哈希索引可以用作哈希分片键来对数据进行分片。基于哈希的分片将字段的哈希索引用作分片键,以跨分片群集对数据进行分区,使用哈希分片键对集合进行分片使数据分布更随机。

三、MongoDB索引的自我优化规则

1、查询优化器

MongoDB自带了一个查询优化器会为我们选择最合适的查询方案。

如果一个索引能够精确匹配一个查询,那么查询优化器就会使用这个索引。

如果不能精确匹配,可能会有几个索引都适合你的查询,那MongoDB会做下列选择:

  • MongoDB的查询计划会将多个索引并行的去执行,最先返回第101个结果的就是胜者,其他查询计划都会被终止,执行优胜的查询计划
  • 这个查询计划会被缓存,接下来相同的查询条件都会使用它

2、查询计划缓存改变时机

  • 在计划评估之后表发生了比较大的数据波动,查询优化器就会重新挑选可行的查询计划
  • 建立索引时
  • 每执行1000次查询之后,查询优化器就会重新评估查询计划

3、联合索引的优化

当你查询条件的顺序和你索引的顺序不一致的话,MongoDB会自动的调整查询顺序,保证你可以使用上索引。

例如:你的查询条件是(a,c,b)但是你的索引是(a,b,c)mongo会自动将你的查询条件调整为abc,寻找最优解。

4、聚合管道的优化

  • 如果管道中不需要使用一个完整的文档的全部字段的话,管道不会将多余字段进行传递
  • $sort和$limit合并,在内存中只会维护limit个数量的文档,不需要将所有的文档维护在内存中,大大降低内存中sort的压力

四、索引的使用建议

索引的优点

  • 减少数据扫描:避免全表扫描代价
  • 减少内存计算:避免分组排序计算
  • 提供数据约束:唯一和时间约束性

索引固然不全是优点,如果不能了解到索引可能带来的危害滥用索引,后果也是非常严重的。

索引依赖内存

索引虽然是持久化在磁盘中存储的,但为了确保索引的速度,实际上需要将索引加载到内存中使用,使用过后还会进行缓存。内存资源相比磁盘空间那是非常的珍贵了,当内存不足以承载索引的时候,就会出现内存与磁盘交换的情况,这时会大大降低索引的性能。

索引在每次查询中只使用一次

虽然可以建立多个索引,但是MongoDB在查询时候每次只会使用一次索引。只有$or或查询特殊,它才会给每一个或分支使用索引然后再合并。比如说你对name和count分别做了两个索引,你目标是查找name的值,然后再进行count排序,索引只会对name进行索引查询,并不会再次对count索引排序。

索引不一定会更快

有一些查询不使用索引会更快。结果集在原集合中所占的比例越大,查询效率越慢。因为使用索引需要进行两次查找:一次查找索引条目,另一次根据索引指针去查找相应的文档。而全表扫描只需要进行一次查询。在最坏的情况,使用索引进行查找次数会是全表扫描的两倍。效率会明显比全表扫描低。而相反在提取较小的子数据集时,索引就非常有效,这就是我们为什么会使用分页。

索引过多会增加insert和update代价

每次的增改操作都会触发MongoDB去重新建立索引,对于频繁修改的字段不建议使用索引。

避免效率极低的操作符

  • $where和$exists这两个操作符,完全不能使用索引
  • $ne和$not通常来说取反和不等于,可以使用索引,但是效率极低,不是很有效,往往也会退化成扫描全表
  • $nin这个操作符也总是会全表扫描

aggregate管道中索引只作用在最开始

在aggregate中使用索引时,只有在管道最开始时的$match和$sort可以使用到索引,一旦发生过$project投射,$group分组,$lookup表关联,$unwind打散等操作后,就完全无法使用索引。