0. 概述

MongoDB 是我这几年用得比较多的数据库了,在这之前我也写过一些关于 MongoDB 的内容,例如:

但是用得多不代表懂得多,光是索引这一块我就觉得知识量有限,所以这一篇我就先对我有限的关于 MongoDB 索引的知识做个总结,如果有下一篇,那么我希望我能够学习到一些我不知道的知识,期待一下。

对于索引,那么不用多描述,都知道是用来加速查询,减少 IO 操作和延迟的一种技术,这些都是常规的索引,肯定会聊到的;但是除了简单的索引之外,MongoDB 还有一些不常规的索引,以及常规索引也有一些不常规的做法,值的来聊一聊。

1. 常规索引的常规操作

在默认情况下,MongoDB 会为每个 collection 添加一个 _id 的索引,所以默认情况下,我们单独通过 _id 来查询,总是能获得最好的性能体验,但是这在工业中却经常没那么容易,很多时候我们会使用其他字段的查询,所以如果这个时候要用到索引,就需要自己来创建了,MongoDB 创建索引的语句为: createIndex,例如,我为我的博客中的文章 collection 创建一个文章名称的索引:

liqiang.io:PRIMARY> db.post.createIndex({"name": 1})
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 2,
    "numIndexesAfter" : 3,
    "ok" : 1
}

这样就表示我创建完索引了,当然,可以通过:getIndexes 来获取 collection 中创建的索引列表:

liqiang.io:PRIMARY> db.post.getIndexes();
[
    {
        "v" : 1,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "blog.post"
    },
    {
        "v" : 1,
        "key" : {
            "name" : 1
        },
        "name" : "name_1",
        "ns" : "blog.post"
    }
]

如果发现创建错了,没关系,dropIndex 可以给你后悔的机会:

liqiang.io:PRIMARY> db.col.dropIndex("name_1")
{ "nIndexesWas" : 3, "ok" : 1 }

2. 普通索引的一些规则

看上去索引操作就是这么简单,但是,却是有一些不普通的规则会影响着我们创建索引,下面就来介绍几种情况:

2.1. 最左原则

所谓的最左原则是相对于一个索引包含两个或者多个字段来说的,例如我创建了这个一个索引:

db.post.createIndex({"name": 1, "status": 1, "created_at": 1})

那么隐含着 MongoDB 帮我创建了另外两个索引:

db.post.createIndex({"name": 1})
db.post.createIndex({"name": 1, "status": 1})

也就是说,当我们使用一个查询的时候,如果它们满足这三个索引中的任何一个,都是可以被这个索引优化到的,否则是不会使用到索引的:

db.post.find({"name": "mongo 索引"})        ->>>>>>>>>   会使用到索引
db.post.find({"status": “PUBLISHED”})    ->>>>>>>>>   不会使用索引

这就是所谓的最左原则了。

2.2. 排序原则

在前面你可能也发现了,创建索引的时候,需要在 field 的后面加上一个数字,这个数字可能是 1 也可能是 -1,其实它代表的意思就是创建这个索引的时候的顺序是升序(1)还是降序(-1),这个在单个字段的索引的时候影响不是很大,但是,当这个索引是一个 组合索引 的时候,事情就不一样了,可能会出现不使用索引的情况,因为 MongoDB 的要求是索引的排序要与查询的顺序一致。注意:这里是一致,而不是一样,怎么说,例如我创建了这么一个索引:

db.post.createIndex({"id": 1, "created": -1})

那么下面这两个查询是会使用索引的:

db.post.find({}).sort({id: 1, created: -1});
db.post.find({}).sort({id: -1, created: 1});

这里的顺序要么和创建索引的顺序一样,要么刚好相反;但是,下面这个查询却无法触发索引:

db.post.find({}).sort({id: 1, created: 1});

2.3. 稀疏索引

在创建索引的时候,还有一种场景需要关注,那就是如果这个索引对应的字段没有值会怎么办,是依然为它创建一个空值的索引呢,还是说直接就忽略它,这不能一概而论,得根据自己的业务需要进行,所以 MongoDB 也是以选项得形式提供支持。

需要注意的是,如果你选择了忽略这个空值,那么你通过索引是没法查询到这个空值字段所对应的记录的!!!切记!!!

2.4. 组合索引的 Tips

在使用组合索引的时候,有个原则,就是尽早缩小范围,根据最左原则,如果一个组合索引的第一个 field 就可以大大得减少后续的记录,那么就会加块后续的过滤,例如,我要做这个一个查询:

db.post.find({id: {$gt: 10}, created: {$lt: data01, $gt: date2})

一般来说,ID 的选择范围会比 created 的大得多,所以在创建索引的时候,如果考虑到这种情况会比较多,那么可以将 created 放在 ID 之前:

db.post.createIndex({created: -1, id: 1});

因为根据最左原则,直接使用 id 是不会使用到索引的,所以可以再单独创建一条新的 ID 索引:

db.post.createIndex({id: 1})

3. 特殊的索引

3.1. TTL

除了上面的常规索引之外,MongoDB 还有一个特殊的索引—TTL 索引,它是针对某个 date 字段或者 date 列表来创建的,作用就是当时间超过了这个字段的值加上 TTL 索引的设置的 expireAfterSeconds 的值之后,这条记录就会自动被 MongoDB 回收。例如下面这个例子:

db.notices.createIndex( { "createdAt": 1 }, { expireAfterSeconds: 3600 } )

表示的就是我在我的博客中创建一条通告,然后这条通告的有效期是 3600 秒,当时间到了之后,就自动删除。

4. 索引效果

前面说了那么多关于索引的内容,那么当我们提交一个查询的时候,怎么知道是否使用了索引呢?MongoDB 很贴心得为我们提供了一个 explain 函数,当你想判别一个语句是否使用了索引得时候,可以简单地通过 explain 查看,例如下面这样:

liqiang.io:PRIMARY> db.post.find({“id": 123}).explain("executeStats")
{
    "cursor" : "BtreeCursor id_1",
    ... ...