02.索引
MongoDB 索引
一、索引简介
1.1 创建索引
和大多数关系型数据库一样,MongoDB 支持使用索引来进行查询优化,采用类似 B-Tree 的数据结构来储存索引和文档的位置信息,同样也支持前缀索引和覆盖索引。在当前最新的 MongoDB 4.0 中,索引的创建语法如下:
db.collection.createIndex( <key and index type specification>, <options> )
<key and index type specification>
:用于指定建立索引的字段和升降序等属性;<options>
:可选配置,通常用于指定索引的性质。
为方便后面的演示,这里先插入部分测试数据,并针对 name 字段创建一个索引:
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()
查看集合的所有索引信息,示例如下:
db.user.getIndexes()
从输出中可以看到默认的索引名为:字段名+排序规则。这里除了我们为 name 字段创建的索引外,集合中还有一个 _id
字段的索引,这是程序自动创建的,用于禁止插入相同 _id
的文档:
{
"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
索引进行排序:
db.user.find({}).sort({name:-1})
db.user.find({}).sort({name:1})
当前大多数数据库都支持双向遍历索引,这和存储结构有关 (如下图)。在 B-Tree 结构的叶子节点上,存储了索引键的值及其对应文档的位置信息,而每个叶子节点间则类似于双向链表,既可以从前往后遍历,也可以从后往前遍历:
2.2 复合索引
支持为多个字段创建索引,示例如下:
db.user.createIndex( { name: -1,birthday: 1} )
需要注意的是 MongoDB 的复合索引具备前缀索引的特征,即如果你创建了索引 { a:1, b: 1, c: 1, d: 1 }
,那么等价于在该集合上还存在了以下三个索引,这三个隐式索引同样可以用于优化查询和排序操作:
{ 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 使用多键索引来优化查询存储在数组中的内容,示例如下:
db.user.createIndex( { Hobby: 1 } )
2.4 哈希索引
为了支持哈希分片,MongoDB 提供了哈希索引,通过对索引值进行哈希运算然后计算出所处的分片位置。语法如下:
db.collection.createIndex( { _id: "hashed" } )
采用哈希运算得到的结果值会比较分散, 所以哈希索引不能用于范围查询,只能用于等值查询。
2.5 地理空间索引
为了支持对地理空间坐标数据的有效查询,MongoDB 提供了两个特殊索引:
- 使用平面几何的 2d 索引,主要用于平面地图数据 (如游戏地图数据)、连续时间的数据;
- 使用球形几何的 2dsphere 索引,主要用于实际的球形地图数据。
这些数据通常是用于解决实际的地理查询,如附近的美食、查询范围内所有商家等功能。其创建语法如下:
db.<collection>.createIndex( { <location field> : "2d" ,
<additional field> : <value> } ,
{ <index-specification options> } )
db.collection.createIndex( { <location field> : "2dsphere" } )
2.6 文本索引
MongoDB 支持全文本索引,用于对指定字段的内容进行全文检索。其创建语法如下:
db.<collection>.createIndex( { field: "text" } )
需要注意的是一个集合最多可以有一个文本索引,但一个文本索引可以包含多个字段,语法如下:
db.<collection>.createIndex(
{
field0: "text",
field1: "text"
}
)
创建文本索引是一个非常昂贵的操作,因为创建文本索引时需要对文本进行语义分析和有效拆分,还需要将拆分后的关键词存储在内存中,这对设备的运算能力和存储空间都有非常高的要求,同时也会降低 MongoDB 的性能,所以需要谨慎使用。
三、索引的性质
创建索引时,可以传入第二个参数 <options>
用于指定索引的性质,常用的索引性质如下:
3.1 唯一索引
唯一索引可以确保在同一个集合中唯一索引列的值只出现一次。 示例如下:
db.user.createIndex( { name: -1,birthday: 1}, { unique: true })
此时再执行下面的操作就会报错,因为 name = heibai
并且 birthday = new Date(1998,08,23)
的数据已经存在:
db.user.insertOne({
name: "heibai",
birthday: new Date(1998,08,23)
})
上面这种情况比较明显,但是如果你执行下面这个操作两次,你会发现只有第一次能够插入成功,第二个就会报 duplicate key 异常。这是因为在唯一索引的约束下,name 不存在的这种状态也会被当做一种唯一状态:
db.user.insertOne({
age: 12
})
想要解决这个问题,就需要用到索引的稀疏性。
3.2 稀疏性
为了解决上面的问题,我们需要为索引添加稀疏性。由于索引不能修改,所以只能先将上面的索引先删除,然后再创建,并为其指定 sparse
属性为 true,具体的创建语句如下:
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 操作符。
使用示例如下:
db.user.createIndex(
{ name: -1 },
{ partialFilterExpression: { age: { $gt: 30 } } }
)
3.4 TTL 索引
TTL 索引允许为每个文档设置一个超时时间,当一个文档达到超时时间后,就会被删除。TTL 索引的到期时间等于索引字段的值 + 指定的秒数,这里的索引字段的值只能是 Date 类型,示例如下:
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
上的默认索引是不会被删除的:
db.collection.dropIndexes()
另外这个命令会获取对应数据库的写锁,并会阻塞其他操作,直到索引删除完成。
五、EXPLAIN
5.1 输出参数
MongoDB 的 explain()
方法和 MySQL 的 explain 关键字一样,都是用于显示执行计划的相关信息。示例如下:
db.user.find({name:"heibai"},{name:1,age:1}).sort({ name:1}).explain()
此时执行计划的部分输出如下:
"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
字段,语句如下:
db.user.find({name:"heibai"},{_id:0, name:1}).sort({ name:1 }).explain()
此时输出结果如下。可以看到该查询少了一个 FETCH
阶段。代表此时只需要扫描索引就可以获取到所需的全部信息,这种情况下 name_-1_birthday_1
索引就是这一次查询操作的覆盖索引。
"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]"
]
}
}
参考资料
- 官方文档:Indexes 、sort-on-multiple-fields
- Kristina Chodorow . MongoDB 权威指南(第 2 版). 人民邮件出版社 . 2014-01