MongoDB 索引

1、索引简介

1.1 建立索引

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

db.collection.createIndex( <key and index type specification>, <options> )
复制代码
  • <key and index type specification>:用于指定创建索引的字段和升降序等属性;
  • <options>:可选配置,一般用于指定索引的性质。

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

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() 查看集合的全部索引信息,示例以下:mongodb

db.user.getIndexes()
复制代码

从输出中能够看到默认的索引名为:字段名+排序规则。这里除了咱们为 name 字段建立的索引外,集合中还有一个 _id 字段的索引,这是程序自动建立的,用于禁止插入相同 _id 的文档:shell

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

{
    "v" : 2,
    "key" : {
        "name" : -1
    },
    "name" : "name_-1",
    "ns" : "test.user"
}
复制代码

2、索引的类型

当前 MongoDB 4.x 支持如下六种类型的索引:数据库

2.1 单字段索引

支持为单个字段创建索引,这是最基本的索引形式,上面咱们针对 name 字段建立的索引就是一个单字段索引。须要特别说明的是,在为 name 字段建立索引时,咱们为其指定了排序规则。但实际上,在涉及单字段索引的排序查询中,索引键的排序规则是可有可无,由于 MongoDB 支持在任一方向上遍历索引。即如下两个查询均可以使用 name_-1 索引进行排序:json

db.user.find({}).sort({name:-1})
db.user.find({}).sort({name:1})
复制代码

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

2.2 复合索引

支持为多个字段建立索引,示例以下:bash

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 的性能,因此须要谨慎使用。

3、索引的性质

建立索引时,能够传入第二个参数 <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 会使用数组中的最先的日期值来计算到期时间。
  • 若是文档中的索引字段不是日期或包含日期值的数组,则文档将不会过时。
  • 若是文档不包含索引字段,则文档不会过时。

4、删除索引

删除索引的语法比较简单,只须要调用 dropIndex 方法,能够传入索引的名称也能够传入索引的定义,示例以下:

db.user.dropIndex("name_-1")
db.user.dropIndex({ name: -1,birthday: 1})
复制代码

若是想要删除所有的索引,则能够调用 dropIndexes 方法,须要注意的是创建在 _id 上的默认索引是不会被删除的。

db.collection.dropIndexes()
复制代码

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

5、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]"
        ]
    }
}
复制代码

参考资料

  1. 官方文档:Indexessort-on-multiple-fields
  2. Kristina Chodorow . MongoDB权威指南(第2版). 人民邮件出版社 . 2014-01

更多文章,欢迎访问 [全栈工程师手册] ,GitHub 地址:github.com/heibaiying/…

相关文章
相关标签/搜索