MongoDB索引

什么是索引

索引最常用的比喻就是书籍的目录,查询索引就像查询一本书的目录。本质上目录是将书中一小部分内容信息(比如题目)和内容的位置信息(页码)共同构成,而由于信息量小(只有题目),所以我们可以很快找到我们想要的信息片段,再根据页码找到相应的内容。同样索引也是只保留某个域的一部分信息(建立了索引的 field 的信息),以及对应的文档的位置信息。

索引查找过程

假设我们有如下文档(每行的数据在 MongoDB 中是存在于一个 Document 当中):

姓名 id 部门 city score
张三 2 xxx Beijing 90
李四 1 xxx Shanghai 70
王五 3 xxx guangzhou 60

假如我们想找 id 为 2 的 document(即张三的记录),如果没有索引,我们就需要扫描整个数据表,然后找出所有为 2 的 document。当数据表中有大量 documents 的时候,这个时间就会非常长(从磁盘上查找数据还涉及大量的 IO 操作)。建立索引后会有什么变化呢?MongoDB 会将 id 数据拿出来建立索引数据,如下:

索引值 位置
1 pos2
2 pos1
3 pos3

这样我们就可以通过扫描这个小表找到 document 对应的位置。查找过程示意图如下:

01_mongodb索引.png

为什么这样速度会快呢?这主要有几方面的因素:

  1. 索引数据通过 B 树来存储,从而使得搜索的时间复杂度为 O(logdN) 级别的(d 是 B 树的度, 通常 d 的值比较大,比如大于 100),比原先 O(N) 的复杂度大幅下降。这个差距是惊人的,以一个实际例子来看,假设 d=100,N=1 亿,那么 O(logdN) = 8, 而 O(N) 是 1 亿。是的,这就是算法的威力。
  2. 索引本身是在高速缓存当中,相比磁盘 IO 操作会有大幅的性能提升。(需要注意的是,有的时候数据量非常大的时候,索引数据也会非常大,当大到超出内存容量的时候,会导致部分索引数据存储在磁盘上,这会导致磁盘 IO 的开销大幅增加,从而影响性能,所以务必要保证有足够的内存能容下所有的索引数据)。

当然,事物总有其两面性,在提升查询速度的同时,由于要建立索引,所以写入操作时就需要额外的添加索引的操作,这必然会影响写入的性能,所以当有大量写操作而读操作比较少的时候,且对读操作性能不需要考虑的时候,就不适合建立索引。当然,目前大多数互联网应用都是读操作远大于写操作,因此建立索引很多时候是非常划算和必要的操作。

MongoDB索引类型

MongoDB 提供了不同的索引类型支持在不同的业务场景进行查询。

_id索引

绝大多数集合默认建立索引,对于每个插入的数据,MongoDB 都会生成一条唯一的 _id 字段,例如新创建一个集合时:

db.demo_admin2.insert({x:1}) db.demo_admin2.getIndexes() # 查看集合索引,可看到_id索引

单键索引

是最普通的索引,单键索引不会自动创建,例如一条记录形式为:{x:1,y:2,z:3},只要在 x 字段上建立索引之后,就可以用 x 为条件进行查询:

db.demo_admin2.ensureIndex({x:1}) # 创建索引 db.demo_admin2.find({x:1}); # 使用索引查询

多键索引

多键索引与单键索引区别在于多键索引的值具有多个记录,是一个数组:

db.demo_admin2.insert({x:[1,2,3,4]})

复合索引

当查询条件为多个时,需要建立复合索引,插入记录 {‘x’:1,‘y’:2,‘z’:3}:

db.demo_admin2.insert({'x':1,'y':2,'z':3});

创建索引:

db.demo_3.ensureIndex({x:1,y:1}) # 1升序,-1降序

使用 {x:1,y:1} 作为条件进行查询:

db.demo_admin2.find({x:1,y:2})

过期索引

指在一段时间后会过期的索引,此索引过一段时间会过期,索引过期后,相应的数据会被删除,适合存储一些在一段时间之后会失效的数据,比如用户登录信息,这样就不需要用到 session 了。

创建过期索引,建立索引的时候需要多用一个参数,指定索引的有效时间——expireAfterSeconds,单位为秒。如下示例为 time 字段创建过期索引:

db.demo_3.ensureIndex({time:1},{expireAfterSeconds:10})

过期索引限制:

  • 过期索引字段值必须是指定的时间类型,必须是 ISODate 或 ISODate 数组,不能使用时间戳,否则不能被自动删除。
  • 如果指定了 ISODate 数组,则按照最小的时间进行删除。 过期索引不能是复合索引。
  • 删除时间不是精确的,删除过程是由后台程序每 60s 跑一次,而且删除也需要一些时间,所以存在误差。
db.demo_3.ensureIndex({time:1},{expireAfterSeconds:30} db.demo_3.insert({time:new Date()});

全文索引

在 mongodb 中每个集合只允许创建一个索引,因此不用担心存在多个索引造成冲突的问题。全文索引创建方法与创建单键索引、复合索引类似。value 换成 ’text’,$** 匹配集合下所有。

为一个字段创建全文索引:

db.articles.ensureIndex({key:"text"})

为多个字段创建全文索引:

db.articles.ensureIndex({key_1:"text"},{key_2:"text"})

为集合中所有的字段创建全文索引:

db.articles.ensureIndex({"$**":"text"})

全局索引限制:

  1. 每次查询,只能指定一个 $text 查询。
  2. $text 查询不能出现在 $nor 查询中。
  3. 查询中如果包含了 $text, hint 不再起作用。
  4. MongoDB 全文索引还不支持中文。

地理位置索引

2d 索引,用于存储和查找平面上的点。2dsphere 索引,用于存储和查找球面上的点。

索引属性

MongoDB 除了支持多种不同类型的索引,还能对索引定制一些特殊的属性:

  • 唯一索引 (unique index): 保证索引对应的字段不会出现相同的值,比如 _id 索引就是唯一索引。
  • TTL索引: 可以针对某个时间字段,指定文档的过期时间(经过指定时间后过期或在某个时间点过期)。
  • 部分索引 (partial index): 只针对符合某个特定条件的文档建立索引,3.2 版本才支持该特性。
  • 稀疏索引(sparse index): 只针对存在索引字段的文档建立索引,可看做是部分索引的一种特殊情况。

索引的优缺点

优点

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

缺点

  1. 增加容量消耗:创建时需额外存储索引数据
  2. 增加修改代价:增删改都需要维护索引数据
  3. 索引依赖内存:会占用极其宝贵的内存资源