Mongodb索引

1.参考

MongoDB权威指南(第2版)

Mongodb Docs

2.前言

建立索引对于任何需要提高查询速度的数据库来说都非常重要,那么索引究竟是一个什么?首先来看看下面是《区块链:技术驱动金融》这本书的前两章的目录。

第1章密码学及加密货币概述----------1
1.1密码学哈希函数----------4
1.2哈希指针及数据结构----------14
1.3数字签名----------19
1.4公钥即身份----------24
1.5两种简单的加密货币----------26
第2章比特币如何做到去中心化----------35
2.1中心化与去中心化----------37
2.2分布式共识----------39
2.3使用区块链达成没有身份的共识----------44
2.4奖励机制与工作量证明----------51
2.5总结----------59

通过目录,我们能很快很清楚的知道这本书写了什么,而我们也能很快从中查找到我们感兴趣的内容在哪一页,如果没有目录,我们将会一篇一篇的去翻阅我们想了解的内容,而索引可以比作数据库的目录。

3.效率

explain()是官方提供的一个用于返回当前查询过程信息的一个方法,通过这个命令,我们可以知道查询的过程,以便于我们进行优化,explain()支持这些操作的过程查询:

  1. aggregate()
  2. count()
  3. distinct()
  4. find()
  5. group()
  6. remove()
  7. update()

explain()方法接受三种可选字符串作为参数和两种布尔值,官方是这样来介绍的:

可选的。指定说明输出的详细程度模式。该模式会影响explain()返回信息的数量和行为。可能的模式有:”queryPlanner”, “executionStats”,和”allPlansExecution”。

默认模式是”queryPlanner”。

为了向后兼容早期版本 cursor.explain(),MongoDB解释true为 “allPlansExecution”和false “queryPlanner”。

aggregate()忽略verbosity参数并在queryPlanner模式下执行。

首先我们插入100000条数据,可以通过下面代码来循环插入:

for(var i = 0; i < 100000; i++) {
  db.test.insert({
    id: i,
    username: 'user' + i
  });
}

然后我们通过explain()方法看看查询含有username:user108键值的文档过程:

db.test.find({username: 'user108'}).explain(true);

返回的结果大概为:

{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "blog.test",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "username" : {
                "$eq" : "user4"
            }
        },
        "winningPlan" : {
            "stage" : "COLLSCAN",
            "filter" : {
                "username" : {
                    "$eq" : "user4"
                }
            },
            "direction" : "forward"
        },
        "rejectedPlans" : [ ]
    },
    "executionStats" : {
        "executionSuccess" : true,
        "nReturned" : 1,
        "executionTimeMillis" : 48,
        "totalKeysExamined" : 0,
        "totalDocsExamined" : 100000,
        "executionStages" : {
            "stage" : "COLLSCAN",
            "filter" : {
                "username" : {
                    "$eq" : "user4"
                }
            },
            "nReturned" : 1,
            "executionTimeMillisEstimate" : 37,
            "works" : 100002,
            "advanced" : 1,
            "needTime" : 100000,
            "needYield" : 0,
            "saveState" : 782,
            "restoreState" : 782,
            "isEOF" : 1,
            "invalidates" : 0,
            "direction" : "forward",
            "docsExamined" : 100000
        },
        "allPlansExecution" : [ ]
    },
    "serverInfo" : {
        "host" : "YizhoudeMacBook-Pro.local",
        "port" : 27017,
        "version" : "3.4.6",
        "gitVersion" : "c55eb86ef46ee7aede3b1e2a5d184a7df4bfb5b5"
    },
    "ok" : 1
}

在上面返回的过程中,我们只需要关注executionStats对象里面的值中的:

  • totalDocsExamined: 文档扫描总数
  • executionTimeMillis: 执行时间(毫秒)
  • nReturned: 返回的文档数量

一般通过这三个值就可以判断文档执行需不需要优化,比如上面返回的信息中表示这次查询文档扫描总数100000,执行时间48毫秒,返回的文档数量为1

通过上面的返回信息其实我们可以看出,find()方法其实是扫描了整个集合来查询我们需要的,即使找到了我们需要的文档,但还会继续往下查询,因为find()方法是查询所有符合条件的文档,这里其实是浪费了服务器的资源。

我们已知username是唯一值,那么我们可以findOne,或者通过limit(1)来限制返回的数量,限制了返回的数量后,Mongodb内部查询是查询到符合的第一条文档就停止,比如我们要查询user5,那么查找的文档总数将是查询到第一条符合的文档数止到第一条文档的总和,这样看上去是没有问题了,但是一旦我们查询的是user99995,那么扫描的文档数量将是99995,这样的方式属于治标不治本。

4.建立索引

Mongodb建立索引是通过createIndex()方法建立,参数为一个对象,包含了需要建立索引的键:

db.test.createIndex({username: 1});

建立的索引其实相当于保存在Mongodb内部的一个单独的文档,这个文档存放了所有username的值和对应储存的物理位置,大概结构为(下面数据是比喻,并不代表真实性):

username Index

['user1', 0x00000001],
['user2', 0x00000002],
['user3', 0x00000003],
['user4', 0x00000004],
['user5', 0x00000005],
........
['user100000', 0x00100000]

5.索引选项

db.collection.createIndex(key,options);

我们可以通过多个选项对索引的文档进行限制,其中很有用的一个就是unique,使用方法如下:

db.test.createIndex({username: 1},{unique: 1});

通过上面的设置username键的值在整个集合中必须是唯一的,如果你试图插入两个username键值相等的文档,那么将会报错,比如下面这样:

db.test.createIndex({username: 1},{unique: 1});

db.test.insert({username: 'user200'});

db.test.insert({username: 'user200'});
----Error!!

它同样适用于复合索引,在对复合索引使用唯一值选项后,如果你试图插入两个及以上索引键的值都一样的两个文档,那么将报错,如果是两个文档的其中一个值不一样同样可以插入,比如下面代码所示:

db.test.createIndex({username: 1, age: 1},{unique: 1});

db.test.insert({username: 'user200', age:18});

db.test.insert({username: 'user200', age:20});

以上两个方式都是可以正常插入的,因为插入的这两个文档中的age不一样,但是如果像下面这样就会报错了:

db.test.createIndex({username: 1, age: 1},{unique: 1});

db.test.insert({username: 'user200', age:18});

db.test.insert({username: 'user200', age:18});
----Error!!

当然除了唯一值选项的设置之外,还有很多选项的设置,如果大家有兴趣可以到官方文档查看。

6.使用索引

建立了索引后,我们不需要特别的方式去查询,我们可以像普通的查询方式一样的去查询:

db.test.find({username: 'user9999'});

在查询的时候Mongodb会自动的查找我们是否为username键建立索引,如果有则扫描username的索引文档,找到相应的值后,然后在根据相应的物理地址去扫描对应的文档,如果没有则扫描test集合的所有文档。

建立了索引后,我们来看看查找username: 'user9999'键值对的文档需要的时间以及查询上的效率:

db.test.find({username: 'user9999'}).explain(true);

返回的过程信息(只拿出executionStats属性):

"executionStats" : {
        "executionSuccess" : true,
        "nReturned" : 1,
        "executionTimeMillis" : 0,
        "totalKeysExamined" : 1,
        "totalDocsExamined" : 1,
        "executionStages" : {
            "stage" : "FETCH",
            "nReturned" : 1,
            "executionTimeMillisEstimate" : 0,
            "works" : 2,
            "advanced" : 1,
            "needTime" : 0,
            "needYield" : 0,
            "saveState" : 0,
            "restoreState" : 0,
            "isEOF" : 1,
            "invalidates" : 0,
            "docsExamined" : 1,
            "alreadyHasObj" : 0,
            "inputStage" : {
                "stage" : "IXSCAN",
                "nReturned" : 1,
                "executionTimeMillisEstimate" : 0,
                "works" : 2,
                "advanced" : 1,
                "needTime" : 0,
                "needYield" : 0,
                "saveState" : 0,
                "restoreState" : 0,
                "isEOF" : 1,
                "invalidates" : 0,
                "keyPattern" : {
                    "username" : 1
                },
                "indexName" : "username_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "username" : [ ]
                },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "username" : [
                        "[\"user9999\", \"user9999\"]"
                    ]
                },
                "keysExamined" : 1,
                "seeks" : 1,
                "dupsTested" : 0,
                "dupsDropped" : 0,
                "seenInvalidated" : 0
            }
        },
        "allPlansExecution" : [ ]
    }

通过上面的返回信息中,我们可以看到

  • totalDocsExamined: 1 //扫描的文档数量为1
  • executionTimeMillis: 0 //执行的时间小于0毫秒
  • nReturned: 1 //返回的文档数为1

7.复合索引

符合索引指的是一个索引文档中存在两个值及以上的值,比如下面这样:

db.test.createIndex({username: 1, id: 1});

通过上面的语句可以创建符合索引,当我们每次查询这两个键的时候,都会从索引文档中查询,如果你经常会以两个键值对查询文档,那么符合索引非常适合你。

复合索引的第一个值可以单独查询,但是第二个值无法单独查询,什么意思呢,你可以理解成一个索引文档以第一个索引键命名,这样就很清楚了,当我们单独查询username的时候会找到这个索引文档,但是当我们单独查询id的时候无法找到这个索引文档。

那么根据上面的理论可以得出,只要第一个值匹配到索引文档,那么在复合索引中不管你跟的是复合索引中添加的哪个值都可以进行索引,比如下面的代码都可以进行复合索引:

db.test.createIndex({username: 1, id: 1, age: 1, adreess: 1});

db.test.find({username: 'user400'});

db.test.find({username: 'user400', age: 18});

db.test.find({username: 'user400', adreess: 'xxxx'});

8.对象索引

Mongodb可以支持对象索引,比如下面这样:

var a = {
    b: 1,
    c: 2
}

db.test.createIndex({a: 1});

或者对某个子键索引

db.test.createIndex({a.b: 1});

需要注意的是上面两种方法的索引效果截然不同,第一个建立索引是建立一个对象索引,对象中的所有值都会提高查询效率。而第二个建立的索引是建立一个子键索引,只对子键提高查询效率。

9.数组索引

Mongodb支持对数组索引,比如像下面这样:

var a = [
    {
        b: 1,
        c: 2
    },
    {
        b: 3,
        c: 4
    }
];

db.test.createIndex({a: 1});

或者对某个子键索引

db.test.createIndex({a.b: 1});

上面两种方式也是不同的,第一种是建立一个数组索引,并且Mongodb会对数组的每一个成员建立索引,这于对象是不一样的。而第二种是建立一个数组子成员的子键索引。

Mongodb只允许复合索引中出现一个数组,如果出现了一个以上的数组将是非法的,应当尽可能的不去使用整个数组索引。

10.获取索引

我们可以通过getIndexes()方法来获取当前集合所建立的所有索引信息,默认会返回一个_id索引,这个索引是Mongodb自动建立的。

getIndexes()使用方法:

db.test.getIndexes();

返回的大概信息:

[
    {
        "v" : 2,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "blog.test"
    },
    {
        "v" : 2,
        "key" : {
            "username" : 1
        },
        "name" : "username_1",
        "ns" : "blog.test"
    }
]

返回的信息当中,各个键值表示的是:

  • v:表示索引版本
  • key: 表示索引的键,值为表示正序倒序 1 或者 -1
  • name: 索引的标识符
  • ns: 作用于的集合

11.删除索引

Mongodb提供了dropIndex()来删除索引,它接受一个字符串参数,这个参数是索引的name值。删除索引可以根据getIndexes()方法查询到的name的值来删除,比如:

db.test.dropIndex('username_1');

12.注意事项

索引建立后,每次添加、修改、更新、删除数据,Mongodb都会更新索引文档,这也带来了一个问题,就是每当我们操作数据的时候,会比以前慢一点,因为操作数据的同时,Mongodb还会自动更新索引文档,为了不影响效率,一个集合最多只能存在64个索引

文档信息