索引是特殊的数据结构,它以易于遍历的形式存储部分集合数据集。索引存储特定字段或字段集的值,按字段值排序。
MongoDB的索引几乎与传统的关系型数据库索引一模一样,第二章提到的_id
实际上也是一个索引,MongoDB的数据按照_id
的顺序存储在内存页与磁盘块上。但是,_id
与业务毫无关联,在业务相关的条件查询时,还是需要进行全表扫描才能找到对应页,效率并不高。
- 为了避免性能瓶颈,可以根据常用的查询建立索引
- 索引的值是按照一定的顺序排列的,因此,使用索引键对文档进行排序效率非常高。
不过,使用索引也是有代价的,不仅会增加磁盘与内存的消耗,对于添加的每一个索引,每次写操作(插入、更新、删除)都会耗费更多时间,这是因为,数据发生变动时,还需要额外的开销更新索引。
聚簇索引与非聚簇索引
在介绍索引之前,先了解下聚簇索引与非聚簇索引。
磁盘上的数据某一时刻只能有一种排序方式,而聚簇索引的特点是:索引顺序与数据存储顺序一致,所以聚簇索引只能有一个。
《数据库原理》中对聚簇索引的定义:聚簇索引的叶子节点是数据节点,非聚簇索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。
所以Mysql的InnoDB引擎的主键索引是聚簇索引、MyIsam引擎使用的是非聚簇索引。
MongoDB不会将_id
索引与文档内容放在一起,所以MongoDB的_id
索引不是聚簇索引,mogoDB将数据与索引分开存放,通过RecordId间接引用。假设为字段”name“
创建了索引,主键id
为主键索引,那么该集合就通过索引查找RecordId,再查找数据。
主键索引
前面提到的_id
索引是默认的主键索引,与业务相关联的项不适合用作主键(难以保障全局唯一、非null),建议使用_id
作为主键。
单字段索引
即对单个filed建立索引,也是常说的“普通索引”;建立索引时可以指定索引数据的order:正序还是倒序。MongoDB 3.0后的版本,使用createIndex
、ensureIndex
是一样的,均是创建索引的命令。
1 | db.mycollection.ensureIndex({"name":1}) //对score字段建立索引、1表示正序、-1表示倒序 |
复合索引
两个或两个以上的键建立索引,可以减小检索的范围。复合索引与Mysql一样,也是按照左侧匹配规则,这里不赘述,主要介绍下复合索引与排序共用的情况。
首先,在集合”myc“
上创建一个复合索引:
1 | db.myc.ensureIndex({"age":1,"name":1}); // 索引1 |
再创建一个:
1 | db.myc.ensureIndex({"name":1,"age":1}); // 索引2 |
这两个复合索引的唯一区别就是键顺序不同,排序规则都是正序(1表示正序、-1表示倒序)。
由于存在了多个索引,使用hint
命令指明使用哪个索引。
1 | // 查询1使用索引1 |
- 对于查询1,先根据索引
age
查找复合条件的结果集,然后在内存中排序(age
索引是有序的,但是排序规则用不到) - 对于查询2,遍历整个索引树,找出所有匹配的文档,不需要排序(
name
索引本身就是有序的),按正序遍历即可。
查询1和查询2究竟哪个性能更强,取决于结果集的大小,一般的,结果集越大,在内存中排序耗时越久,超过一定大小(32MB)后,MongoDB会抛出异常,拒绝对如此多的数据排序。一般的:
- 结果集只有几条、十几条,使用查询1,排序的开销跟遍历树的开销相比并不大
- 结果集有几百条、甚至几千条,使用查询2,排序的开销显得过大。
- 结果集有几万条,使用查询1或查询2建议具体比较一下
结果集的大小可以使用limit
关键字人为限制:
1 | db.myc.find({"age":{"$gte":21, "$lte":30}}).sort({"name":1}).limit(1000).hint({"name":1,"age":1}); \\使用查询2,并限制结果集 |
最后,具体使用哪种查询,使用explain
关键字在shell中比较一下再做选择。
1 | db.myc.find({"age":{"$gte":21, "$lte":30}}).sort({"name":1}).hint({"age":1,"name":1}).explain()[`millis`]; \\获取查询1耗时 |
唯一索引
唯一索引用来确保集合的每一个文档的指定键都有唯一值,允许null值。例如:在集合mycollection中,给”name“键建立唯一索引,试图插入重复name的值时,会抛出异常,也会影响效率。
1 | db.mycollection.ensureIndex({"name":1}, {"unique":true}); |
使用场景:应对偶尔可能会出现重复的键重复问题,而不是在运行时对重复键进行过滤。比如:为避免消息重复消费,可以为”消息id“键创建唯一索引。
复合唯一索引
复合的唯一索引,单个键的值可以相同,但所有键的组合值必须是唯一的。
例如,如果有一个{”username”:1, “age”:100}上的唯一索引,下面的插入是合法的,不会报错。
1 | db.mycollection.insert({"username":"bob"}); |
去除重复
在已有的集合上创建唯一索引时,可能会失败,因为集合中可能已经存在重复的值了。此时,有三种办法:
找出重复数据,想办法去除
直接删除重复的值,创建索引时使用
”dropDups“
选项,如果遇到重复的值,只会保留第一个值。正是由于这种不确定性(不确定哪条记录被删除),MongoDB 3.0以后移除了该选项。新建一个集合,建立索引,然后把旧集合的数据拷贝至新集合
1
db.mycollection.ensureIndex({"username":"bob"},{"unique":true,"dropDups":true})
稀疏索引
唯一索引会把null看做值,假如集合中有以下两个文档,假设对键”age“
建立唯一索引,则文档2中的"age"
就是null
1 | {"name":"bob", "age":23} // 文档1 |
现在想新增文档3,是无法添加的,因为文档3中”age“
也是null
,与文档2冲突了,违反了唯一性。
1 | {"name":"bob", "addresss":"sz"} // 文档3 |
此时,应该使用稀疏索引(sparse index),就可以插入文档3,同时也能保证文档4无法插入,满足唯一性。
1 | // 创建稀疏索引 |
稀疏索引定义如下:如果集合中的文档存在索引键,则必须是唯一的,如果文档不存在索引键,则不要求该文档的唯一性。
注意事项:
根据是否使用稀疏索引,查询结果可能有所不同。例如:对于下面的查询,查询1和查询2是完全相同的语句,不同的是,查询1对应未创建稀疏索引的情况,查询2对应创建稀疏索引的情况。
1 | db.mycollection.find({"age":{"$ne":23}}) // 查询1,未创建稀疏索引 |
查询结果如下,查询2没有查询到文档,这是因为建立了稀疏索引后,查询只根据索引查询,不再全表扫描,因此,会遗漏那些没有索引键的文档。如果一定要获取与查询1相同的结果,通过hint
命令指明不使用索引,执行全表扫描。
1 | // 查询1的查询结果 |
TTL索引
TTL(Time-to-live index)索引指具有生命周期的索引,这种索引会为文档设置一个超时时间,一旦文档存活时间超过该时间就会被删除。这种类型的索引可以用在:消息日志、服务器会话等具有时效性的场景。
在"createdTime"
字段上创建TTL索引:
1 | db.mycollection.createIndex({"createdTime":1}, "expireAfterSecs": 60*60*24) |
"createdTime"
字段必需是日期类型,一般设置为当前时间,
记录被删除的时间点="createdTime"
字段对应的时间点+"expireAfterSecs"
对应的单位为秒的时间段
为了避免活跃的会话被删除,可以在会话上有活动发生时,更新"createdTime"
为当前时间。
一个集合上可以创建多个TTL索引。
全文索引
与Mysql一样,MongoDB也支持全文检索。创建全文索引的开销较大,MongoDB本身就很耗内存,在一个操作频繁的集合上创建全文索引更容易导致内存不足,全文本索引的集合写入性能更差、分片时迁移速度更慢,一般的,如果不是特别强烈的业务需要,不建议使用全文索引。
在"mytext"
字段上创建全文索引:
1 | db.mycollection.ensureIndex({"mytext":"text"}) |
使用全文索引检索关键字"keyword"
:
1 | db.mycollection.find({$text:{$search:"keyword"}}) |
地理空间索引
MongoDB支持几种类型的索引,最常见的是2dsphere索引(用于球面图)和2d索引(用于平面图)。这里只简单介绍下这两种索引的创建:
1 | db.mycollection.ensureIndex({"myloc":"2dsphere"}) |
优化
如果数据库中已有大量数据,此时建立索引将会导致大量的IO操作(内存,磁盘读写),耗时较长。MongoDB提供了2种方式:foreground和background。
- foreground即前台操作,它会阻塞用户对数据的读写操作直到index构建完毕,即任何需要获取read、write锁的操作都会阻塞,默认情况下为foreground;
- background即后台模式,不阻塞数据读写操作,独立的后台线程异步构建索引,此时仍然允许对数据的读写操作;其中background比foreground更加耗时。
查询优化
对频繁访问的查询,尽量使用覆盖索引,如果一个索引包含(或者说覆盖)所有需要查询的数据,就称为“覆盖索引”,使用覆盖索引时,需要强制不显示objectId字段。
1
2db.mycollection.createIndex({"name", 1})
db.mycollection.find({"name":bob, "_id":0}) // 0表示不显示该字段选用差异性较强的字段作为索引,不要选用类似于性别、国家这种字段作为索引键。
需要哪些字段查询哪些字段,尽量不要查询整个文档
使用hint强制使用特定的索引
使用explain对比分析多种查询方式的性能
写操作优化
- 尽量不要创建过多的索引,索引会增加该集合写入、更新、删除的开销,因为要额外维护索引
- 合理设置journal相关参数,journal日志实现日志预写功能,开启journal保证了数据持久化,但也会存在一定的性能消耗,合理的设置commitIntercalMs控制journal写入磁盘的频率,该参数过大,影响MongoDB写操作的性能,该参数过小,MongoDB意外宕机期间预写日志未持久化的可能增大。