Skip to content

Latest commit

 

History

History
377 lines (282 loc) · 14.3 KB

MongoDB_索引.md

File metadata and controls

377 lines (282 loc) · 14.3 KB

MongoDB 索引

一、索引简介
        1.1 创建索引
        1.2 查看索引
二、索引的类型
        2.1 单字段索引
        2.2 复合索引
        2.3 多键索引
        2.4 哈希索引
        2.5 地理空间索引
        2.6 文本索引
三、索引的性质
        3.1 唯一索引
        3.2 稀疏性
        3.3 部分索引
        3.4 TTL 索引
四、删除索引
五、EXPLAIN
        5.1 输出参数
        5.2 覆盖索引

一、索引简介

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]"
        ]
    }
}

参考资料

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