# MongoDB 索引

<nav>
<a href="#一索引简介">一、索引简介</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-创建索引">1.1 创建索引</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-查看索引">1.2 查看索引</a><br/>
<a href="#二索引的类型">二、索引的类型</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#21-单字段索引">2.1 单字段索引</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#22-复合索引">2.2 复合索引</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#23-多键索引">2.3 多键索引</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#24-哈希索引">2.4 哈希索引</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#25-地理空间索引">2.5 地理空间索引</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#26-文本索引">2.6 文本索引</a><br/>
<a href="#三索引的性质">三、索引的性质</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#31-唯一索引">3.1 唯一索引</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#32-稀疏性">3.2 稀疏性</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#33-部分索引">3.3 部分索引</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#34-TTL-索引">3.4 TTL 索引</a><br/>
<a href="#四删除索引">四、删除索引</a><br/>
<a href="#五EXPLAIN">五、EXPLAIN</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#51-输出参数">5.1 输出参数</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#52-覆盖索引">5.2 覆盖索引</a><br/>
</nav>

## 一、索引简介

### 1.1 创建索引

和大多数关系型数据库一样，MongoDB 支持使用索引来进行查询优化，采用类似 B-Tree 的数据结构来储存索引和文档的位置信息，同样也支持前缀索引和覆盖索引。在当前最新的 MongoDB 4.0 中，索引的创建语法如下：

```shell
db.collection.createIndex( <key and index type specification>, <options> )
```

+ `<key and index type specification>`：用于指定建立索引的字段和升降序等属性；
+ ` <options> `：可选配置，通常用于指定索引的性质。

为方便后面的演示，这里先插入部分测试数据，并针对 name 字段创建一个索引：

```shell
db.user.insertMany([
    {
        name: "heibai",
        age: 26,
        birthday: new Date(1998,08,23),
        createTime: new Timestamp(),
        Hobby: ["basketball", "football", "tennis"]
    },
    {
        name: "hei",
        age: 32,
        birthday: new Date(1989,08,23),
        createTime: new Timestamp(),
        Hobby: ["basketball", "tennis"]
    },
    {
        name: "ying",
        age: 46,
        birthday: new Date(1978,08,23),
        createTime: new Timestamp(),
        Hobby: ["tennis"]
    }
])


# 创建索引, -1表示以降序的顺序存储索引
db.user.createIndex( { name: -1 } )
```

### 1.2 查看索引

创建索引后可以使用 `getIndexes()` 查看集合的所有索引信息，示例如下：

```shell
db.user.getIndexes()
```

从输出中可以看到默认的索引名为：字段名+排序规则。这里除了我们为 name 字段创建的索引外，集合中还有一个 `_id` 字段的索引，这是程序自动创建的，用于禁止插入相同 `_id` 的文档：

```json
{
    "v" : 2,
    "key" : {
        "_id" : 1
    },
    "name" : "_id_",
    "ns" : "test.user"
},

{
    "v" : 2,
    "key" : {
        "name" : -1
    },
    "name" : "name_-1",
    "ns" : "test.user"
}
```

## 二、索引的类型

当前 MongoDB 4.x 支持以下六种类型的索引：

### 2.1 单字段索引

支持为单个字段建立索引，这是最基本的索引形式，上面我们针对 name 字段创建的索引就是一个单字段索引。需要特别说明的是，在为 name 字段创建索引时，我们为其指定了排序规则。但实际上，在涉及单字段索引的排序查询中，索引键的排序规则是无关紧要，因为 MongoDB 支持在任一方向上遍历索引。即以下两个查询都可以使用 `name_-1` 索引进行排序：

```shell
db.user.find({}).sort({name:-1})
db.user.find({}).sort({name:1})
```

当前大多数数据库都支持双向遍历索引，这和存储结构有关 (如下图)。在 B-Tree 结构的叶子节点上，存储了索引键的值及其对应文档的位置信息，而每个叶子节点间则类似于双向链表，既可以从前往后遍历，也可以从后往前遍历：

<div align="center"> <img src="../pictures/b-tree.png"/> </div>

### 2.2 复合索引

支持为多个字段创建索引，示例如下：

```shell
db.user.createIndex( { name: -1,birthday: 1} )
```

需要注意的是 MongoDB 的复合索引具备前缀索引的特征，即如果你创建了索引 `{ a:1, b: 1, c: 1, d: 1 }`，那么等价于在该集合上还存在了以下三个索引，这三个隐式索引同样可以用于优化查询和排序操作：

```shell
{ a: 1 }
{ a: 1, b: 1 }
{ a: 1, b: 1, c: 1 }
```

所以应该尽量避免创建冗余的索引，冗余索引会导致额外的性能开销。即如果你创建了索引 `{ name: -1, birthday: 1} `，那么再创建 `{name:-1}` 索引，就属于冗余创建。

对于复合索引还需要注意它在排序上的限制，例如索引 `{a：1, b：-1}` 支持 `{a：1, b：-1}` 和 `{a：-1, b：1}` 形式的排序查询，但不支持 `{a： - 1, b：-1} ` 或 `{a：1, b：1}` 的排序查询。即字段的排序规则要么与索引键的排序规则完全相同，要么完全相反，此时才能进行双向遍历查找。

### 2.3 多键索引

如果索引包含类型为数组的字段，MongoDB 会自动为数组中的每个元素创建单独的索引条目，这就是多键索引。MongoDB 使用多键索引来优化查询存储在数组中的内容。创建示例如下：

```shell
db.user.createIndex( { Hobby: 1 } )
```

### 2.4 哈希索引

为了支持基于哈希分片，MongoDB 提供了哈希索引，通过对索引值进行哈希运算然后计算出所处的分片位置。语法如下：

```shell
db.collection.createIndex( { _id: "hashed" } )
```

采用哈希运算得到的结果值会比较分散， 所以哈希索引不能用于范围查询，只能用于等值查询。

### 2.5 地理空间索引

为了支持对地理空间坐标数据的有效查询，MongoDB提供了两个特殊索引：

- 使用平面几何的 2d 索引，主要用于平面地图数据 (如游戏地图数据)、连续时间的数据；
- 使用球形几何的 2dsphere 索引，主要用于实际的球形地图数据。

这些数据通常是用于解决实际的地理查询，如附近的美食、查询范围内所有商家等功能。其创建语法如下：

```shell
db.<collection>.createIndex( { <location field> : "2d" ,
                               <additional field> : <value> } ,
                             { <index-specification options> } )
db.collection.createIndex( { <location field> : "2dsphere" } )
```

### 2.6 文本索引

MongoDB 支持全文本索引，用于对指定字段的内容进行全文检索。其创建语法如下：

```shell
db.<collection>.createIndex( { field: "text" } )
```

需要注意的是一个集合最多可以有一个文本索引，但一个文本索引可以包含多个字段，语法如下：

```shell
db.<collection>.createIndex(
   {
     field0: "text",
     field1: "text"
   }
 )
```

创建文本索引是一个非常昂贵的操作，因为创建文本索引时需要对文本进行语义分析和有效拆分，还需要将拆分后的关键词存储在内存中，这对设备的运算能力和存储空间都有非常高的要求，同时也会降低 MongoDB 的性能，所以需要谨慎使用。

## 三、索引的性质

创建索引时，可以传入第二个参数 ` <options> ` 用于指定索引的性质，常用的索引性质如下：

### 3.1 唯一索引

唯一索引可以确保在同一个集合中唯一索引列的值只出现一次。 示例如下：

```shell
db.user.createIndex( { name: -1,birthday: 1}, { unique: true })
```

此时再执行下面的操作就会报错，因为 `name = heibai` 并且 `birthday = new Date(1998,08,23)` 的数据已经存在：

```shell
db.user.insertOne({
        name: "heibai",
        birthday: new Date(1998,08,23)
})
```

上面这种情况比较明显，但是如果你执行下面这个操作两次，你会发现只有第一次能够插入成功，第二个就会报 duplicate key 异常。这是因为在唯一索引的约束下，name 不存在的这种状态也会被当做一种唯一状态：

```shell
db.user.insertOne({
        age: 12
})
```

想要解决这个问题，就需要用到索引的稀疏性。

### 3.2 稀疏性

为了解决上面的问题，我们需要为索引添加稀疏性。由于索引不能修改，所以只能先将上面的索引先删除，然后再创建，并为其指定 `sparse` 属性为 true，具体的创建语句如下：

```shell
db.user.dropIndex("name_-1_birthday_1")
db.user.createIndex( { name: -1,birthday: 1}, { unique: true,sparse: true})
```

此时你再多次执行上面的插入语句就能插入成功。原因是对于稀疏索引而言，它仅包含具有索引字段的文档的索引信息，即使索引字段的值为 null 也可以，但不能缺少相应的索引字段。如果缺少，则相应的文档就不会被包含在索引信息中。

### 3.3 部分索引

部分索引主要用于为符合条件的部分数据创建索引，它必须与 `partialFilterExpression` 选项一起使用。 `partialFilterExpression` 选项可以使用以下表达式来确定数据范围：

- 等式表达式（即 `字段: 值` 或使用 $eq 运算符）；
- `$exists: true` 表达式；
- $gt、$gte、$lt、$lte 操作符；
- $type 操作符；
- 处于顶层的 $and 操作符。

使用示例如下：

```shell
db.user.createIndex(
   { name: -1 },
   { partialFilterExpression: { age: { $gt: 30 } } }
)
```

### 3.4 TTL 索引

TTL 索引允许为每个文档设置一个超时时间，当一个文档达到超时时间后，就会被删除。TTL索引的到期时间等于索引字段的值 + 指定的秒数，这里的索引字段的值只能是 Date 类型，示例如下：

```shell
db.user.createIndex( { "birthday": 1 }, { expireAfterSeconds: 60 } )
```

这里我们在 birthday 字段上建立 TTL 索引只是用于演示，实际上 TTL 索引主要是用于那些只需要在特定时间内保存的数据，如会话状态、临时日志等。在使用 TTL 索引时，还有以下事项需要注意：

- TTL 属性只能用于单字段索引，不支持复合索引。
- 建立 TTL 索引的字段的类型只能是 Date 类型，时间戳类型也不可以。
- 如果字段是数组，并且索引中有多个日期值，则 MongoDB 会使用数组中的最早的日期值来计算到期时间。
- 如果文档中的索引字段不是日期或包含日期值的数组，则文档将不会过期。
- 如果文档不包含索引字段，则文档不会过期。  

## 四、删除索引

删除索引的语法比较简单，只需要调用 `dropIndex` 方法，可以传入索引的名称也可以传入索引的定义，示例如下：

```
db.user.dropIndex("name_-1")
db.user.dropIndex({ name: -1,birthday: 1})
```

如果想要删除全部的索引，则可以调用 `dropIndexes` 方法，需要注意的是建立在 `_id` 上的默认索引是不会被删除的。

```shell
db.collection.dropIndexes()
```

另外这个命令会获取对应数据库的写锁，并会阻塞其他操作，直到索引删除完成。

## 五、EXPLAIN

### 5.1 输出参数

MongoDB 的 `explain()` 方法和 MySQL 的 explain 关键字一样，都是用于显示执行计划的相关信息。示例如下：

```shell
db.user.find({name:"heibai"},{name:1,age:1}).sort({ name:1}).explain()
```

此时执行计划的部分输出如下：

```json
"inputStage" : {
    "stage" : "FETCH",
    "inputStage" : {
        "stage" : "IXSCAN",
        "keyPattern" : {
            "name" : -1,
            "birthday" : 1
        },
        "indexName" : "name_-1_birthday_1",
        "isMultiKey" : false,
        "multiKeyPaths" : {
            "name" : [ ],
            "birthday" : [ ]
        },
        "isUnique" : true,
        "isSparse" : true,
        "isPartial" : false,
        "indexVersion" : 2,
        "direction" : "backward",
        "indexBounds" : {
            "name" : [
                "[\"heibai\", \"heibai\"]"
            ],
            "birthday" : [
                "[MaxKey, MinKey]"
            ]
        }
    }
}
```

输出结果中内层的 `inputStage.stage` 的值为 `IXSCAN`，代表此时用到了索引进行扫描，并且 `indexName` 字段显示了对应的索引为 `name_-1_birthday_1`。而外层 `inputStage.stage` 的值为 `FETCH`，代表除了从索引上获取数据外，还需要去对应的文档上获取数据，因为 age 信息并不存储在索引上。这个输出可以证明 MongoDB 是支持前缀索引的，且单键索引支持双向扫描。

### 5.2 覆盖索引

这里我们对上面的查询语句略做修改，不返回 age 字段和默认的 `_id` 字段，语句如下：

```shell
db.user.find({name:"heibai"},{_id:0, name:1}).sort({ name:1 }).explain()
```

此时输出结果如下。可以看到该查询少了一个 `FETCH` 阶段。代表此时只需要扫描索引就可以获取到所需的全部信息，这种情况下  `name_-1_birthday_1` 索引就是这一次查询操作的覆盖索引。

```json
"inputStage" : {
    "stage" : "IXSCAN",
    "keyPattern" : {
        "name" : -1,
        "birthday" : 1
    },
    "indexName" : "name_-1_birthday_1",
    "isMultiKey" : false,
    "multiKeyPaths" : {
        "name" : [ ],
        "birthday" : [ ]
    },
    "isUnique" : true,
    "isSparse" : true,
    "isPartial" : false,
    "indexVersion" : 2,
    "direction" : "backward",
    "indexBounds" : {
        "name" : [
            "[\"heibai\", \"heibai\"]"
        ],
        "birthday" : [
            "[MaxKey, MinKey]"
        ]
    }
}
```



## 参考资料

1. 官方文档：[Indexes](https://docs.mongodb.com/manual/indexes/) 、[sort-on-multiple-fields](https://docs.mongodb.com/manual/tutorial/sort-results-with-indexes/#sort-on-multiple-fields)
2. Kristina Chodorow . MongoDB权威指南（第2版）. 人民邮件出版社 . 2014-01

