MongoDB索引

首先,创建测试样例数据,创建一百万个用户名和年龄的文档:

> for(i=0; i<1000000; i++){
    db.testuser.insert(
        {
            "i":i,
            "username":"user"+i,
            "age":Math.floor(Math.random()*120),
            "created":new Date()
        }
    );
}

创建一百万个文档时间比较长,大概需要几分钟,好是在mongo服务器的 shell 中创建,在客户端可能会报超时。

explain函数

在MongoDB 3.0版本之后,explain函数的返回有一定修改,《MongoDB权威指南第二版》不一样,需要带上参数来执行,才能看到执行的过程。

> db.testuser.find({"username" : "user5866"}).explain("executionStats")

输出的结果如下:

{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "testdb.testuser",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "username" : {
                "$eq" : "user5866"
            }
        },
        "winningPlan" : {
            "stage" : "COLLSCAN",
            "filter" : {
                "username" : {
                    "$eq" : "user5866" }
            },
            "direction" : "forward"
        },
        "rejectedPlans" : []
    },
    "executionStats" : {
        "executionSuccess" : true,
        "nReturned" : 1,
        "executionTimeMillis" : 594,
        "totalKeysExamined" : 0,
        "totalDocsExamined" : 1000000,
        "executionStages" : {
            "stage" : "COLLSCAN",
            "filter" : {
                "username" : {
                    "$eq" : "user5866" }
            },
            "nReturned" : 1,
            "executionTimeMillisEstimate" : 542,
            "works" : 1000002,
            "advanced" : 1,
            "needTime" : 1000000,
            "needYield" : 0,
            "saveState" : 7826,
            "restoreState" : 7826,
            "isEOF" : 1,
            "invalidates" : 0,
            "direction" : "forward",
            "docsExamined" : 1000000
        }
    },
    "serverInfo" : {
        "host" : "xxx",
        "port" : 27017,
        "version" : "3.6.1",
        "gitVersion" : "025d4f4fe61efd1fb6f0005be20cb45a004093d1"
    },
    "ok" : 1.0
}

这次查询 nReturned 为 1 ,即返回1个文档,executionTimeMillis 为 594ms, 即将近半秒的时间,totalDocsExamined 为1百万,即说明遍历了全部的所有文档。
可以在 username 字段上建立索引,来使查询加快。索引可以根据给定的字段组织数据,让MongoDB能够非常快的找到目标文档。

> db.testuser.createIndex({"username":1})

创建索引需要一定的时间。
再次执行查询

> db.testuser.find({"username" : "user5866"}).explain("executionStats")

返回结果截取部分

        "executionSuccess" : true,
        "nReturned" : 1,
        "executionTimeMillis" : 0,
        "totalKeysExamined" : 1,
        "totalDocsExamined" : 1,

可以看到,加了索引之后,查询时间极短几乎可以忽略,因为是直接在索引中命中目标文档。
但索引创建后,每次写操作,如插入更新删除等,都会耗费更多的时间,来维护索引。所以挑选创建索引的字段很重要。

复合索引

在多个字段上建立索引

> db.testuser.createIndex({"age":1, "username":1})

在索引中每一个索引条目都有 age 和 username 对应的信息。索引中,age 是严格按照升序排列,在同一个age中,username 是严格按照升序来排列。
使用这个索引的几个主要的查询为:
点查询

> db.testuser.find({"age" : 21}).sort({"username":-1})

用于查找单个值(可能对应多个文档),由于索引中第二个字段是 username,所以根据 age 为 21 的匹配结果中的最后一个索引开始,逆序遍历索引即可。查询时,能直接定位到正确的年龄,而且不需要对结果进行排序,效率特别高效。
耗时:

        "executionSuccess" : true,
        "nReturned" : 8242,
        "executionTimeMillis" : 18,
        "totalKeysExamined" : 8242,
        "totalDocsExamined" : 8242,

多值查询

> db.testuser.find({"age" : {$gte:21, $lte:30}})

多值查询查找到多个值相匹配的文档。将使用索引中第一个键 age 得到匹配的文档,通常来说,如果使用索引进行查询,那么查询结果文档通常是按照索引顺序排列的。

        "executionSuccess" : true,
        "nReturned" : 83012,
        "executionTimeMillis" : 144,
        "totalKeysExamined" : 83012,
        "totalDocsExamined" : 83012,

多值查询对结果排序

> db.testuser.find({"age" : {$gte:21, $lte:30}}).sort({username : -1})

这个查询需要对查询结果进行排序,使用索引得到结果集中 username 是无序的(每个值对应的username有序,多个值对应的整体无序),需要现在内存中对结果进行排序,然后返回。查询速度取决于多少个文档与查询条件匹配,如果个数少排序不需要耗费太多时间。如果结果集中需要排序的文档数量比较多,就会比较慢,如果大于32M,还会报错,拒绝进行排序。
这里对应《MongoDB权威指南第二版》的叙述,进行执行计划分析:

{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "testdb.testuser",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "$and" : [ 
                {
                    "age" : {
                        "$lte" : 30.0 }
                }, 
                {
                    "age" : {
                        "$gte" : 21.0 }
                }
            ]
        },
        "winningPlan" : {
            "stage" : "FETCH",
            "filter" : {
                "$and" : [ 
                    {
                        "age" : { "$lte" : 30.0 } }, 
                    {
                        "age" : { "$gte" : 21.0 } }
                ]
            },
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "username" : 1.0 },
                "indexName" : "username_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "username" : [] },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "backward",
                "indexBounds" : {
                    "username" : [ "[MaxKey, MinKey]" ] }
            }
        },
        "rejectedPlans" : [ 
            {
                "stage" : "SORT",
                "sortPattern" : {
                    "username" : -1.0
                },
                "inputStage" : {
                    "stage" : "SORT_KEY_GENERATOR",
                    "inputStage" : {
                        "stage" : "FETCH",
                        "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "age" : 1.0, "username" : 1.0 }, "indexName" : "age_1_username_1", "isMultiKey" : false, "multiKeyPaths" : { "age" : [], "username" : [] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "age" : [ "[21.0, 30.0]" ], "username" : [ "[MinKey, MaxKey]" ] } } }
                }
            }
        ]
    },
    "executionStats" : {
        "executionSuccess" : true,
        "nReturned" : 83012,
        "executionTimeMillis" : 1185,
        "totalKeysExamined" : 1000000,
        "totalDocsExamined" : 1000000,
        "executionStages" : {
            "stage" : "FETCH",
            "filter" : {
                "$and" : [ 
                    {
                        "age" : { "$lte" : 30.0 } }, 
                    {
                        "age" : { "$gte" : 21.0 } }
                ]
            },
            "nReturned" : 83012,
            "executionTimeMillisEstimate" : 1099,
            "works" : 1000001,
            "advanced" : 83012,
            "needTime" : 916988,
            "needYield" : 0,
            "saveState" : 7870,
            "restoreState" : 7870,
            "isEOF" : 1,
            "invalidates" : 0,
            "docsExamined" : 1000000,
            "alreadyHasObj" : 0,
            "inputStage" : {
                "stage" : "IXSCAN",
                "nReturned" : 1000000,
                "executionTimeMillisEstimate" : 385,
                "works" : 1000001,
                "advanced" : 1000000,
                "needTime" : 0,
                "needYield" : 0,
                "saveState" : 7870,
                "restoreState" : 7870,
                "isEOF" : 1,
                "invalidates" : 0,
                "keyPattern" : {
                    "username" : 1.0 },
                "indexName" : "username_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "username" : [] },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "backward",
                "indexBounds" : {
                    "username" : [ "[MaxKey, MinKey]" ] },
                "keysExamined" : 1000000,
                "seeks" : 1,
                "dupsTested" : 0,
                "dupsDropped" : 0,
                "seenInvalidated" : 0
            }
        }
    },
    "serverInfo" : {
        "host" : "xxx",
        "port" : 27017,
        "version" : "3.6.1",
        "gitVersion" : "025d4f4fe61efd1fb6f0005be20cb45a004093d1"
    },
    "ok" : 1.0
}

可以看到结果 queryPlanner 中
winningPlan,查询优化器针对该query所返回的最优执行计划的详细内容。
rejectedPlans, 其他执行计划(非最优而被查询优化器reject的)的详细返回。
这里可以看到,MongoDB使用的 winningPlan 中,使用的是 username_1 这个索引,拒绝使用 age_1_username_1 这个复合索引。这样也就引证了执行结果 “totalDocsExamined” : 1000000 ,说明查询使用了 username 这个索引,先进行全文档的排序,然后在其中取得 age 在 21 和 30 之间的文档。从而避免了内存的排序过程。(username 是之前添加过的一个单字段索引,这里如果要达到权威指南书中的结果,需要将 username 的单字段索引删除。)

复合索引

键的方向

如果需要在两个或更多的查询条件上进行排序,可能需要让索引键的方向不同。如果需要年龄从小到大,用户名从Z到A进行排序,由于之前的排序在每一个年龄分组内都是按照 username 升序排列的,之前的索引对这个查询没有用。可以建立与方向相匹配的索引。如 {“age”:1, “username”:-1}。另外,相互反转的索引是等价的,比如排序的两个键都乘以 -1,其实只需要反转方向即可。
单一条件查询对索引的方向是无所谓的,只是正序或逆序遍历的问题。

覆盖索引

在上边的例子中,查询只是用来查找正确的文档,然后按照指示获取实际的文档。如果查询中只需要查找索引中包含的字段,那就没有必要去获取实际的文档。当一个索引包含用户请求的所有字段,可以认为这个索引覆盖了本次查询。在实际中,应该优先使用覆盖索引,而不是去获取实际的文档。这样可以保证工作集比较小。
为了确保只使用索引就可以完成,应该使用投射来指定不要返回 _id 字段(除非它是索引的一部分)。可能还需要对不需要查询的字段做索引,因此要在编写时在所需的查询速度和这种方式带来的开销之间做好权衡。

隐式索引

复合索引,例如 {“age”:1,”username”:1} 的索引,同时也等于是一个 {“age”:1} 的索引。如果有一个N个键的索引,{a:1, b:1, c:1, … z:1} 就等同于 N 个键的前缀组成的索引。有一系列组合, {a:1}, {a:1,b:1}, {a:1, b:1, c:1}…

$操作符对索引的使用

低效率的操作符

如 $where 查询和检查一个键是否存在的查询
{“key”:{“exists”: true}},完全无法使用索引。ne 的效率也是比较低,可以使用索引,但是必须要查看所有的索引条目,不得不扫描整个索引。
not使使退  nin 就总是进行全表扫描。
如果要进行这些查询,可以试着找到另一个能够使用索引的语句,将其添加到查询中,这样可以在进行无索引匹配时,先将结果集的文档数量减到一个比较小的水平。

范围

设计基于多个字段的索引时,应该将会用于精确匹配的字段,放在索引的前面,将用于范围匹配的字段,放在最后。这样,查询可以先使用第一个索引键进行精确匹配,然后再使用第二个索引范围在这个结果集内部进行搜索。
比如查询 age 为 21 的条目,在其中搜索用户名介于 user5 和 user8 的条目,使用{age , username}的复合索引是非常合适的。可以直接定位。如果使用{username, age}索引的话,查询就先要找到 user5 到 user8 之间的所有用户,然后在从中挑选 age 为 21 的条目。在一个查询中使用两个范围通常会导致低效的查询计划。

OR查询

OR查询可以对每个子句都使用索引,因为OR查询实际上是执行两次查询后将结果集合并。通常来说,执行两次查询再将结果合并的效率不如单次查询效率高,因此,应该尽可能使用 in 而不是 or。

索引对象和数组

索引嵌套文档

MongoDB允许深入文档内部,对嵌套字段和数组建立索引。嵌套对象和数组字段可以与复合索引中的顶级字段一起使用。
对子字段建立索引,可以提高其查询速度。注意,对嵌套文档本身建立索引,和嵌套文档某个字段的索引是不同的。

索引数组

对数组建立索引,可以高效的搜索数组中的特定元素。数组的每一个元素都是一个索引条目,因此数组索引的代价比单值索引的代价高很多。
无法对整个数组做一个实体建立索引。只能对每个元素建立索引。

多键索引

对于某个索引键,如果在某个文档中是一个数组,那么这个索引就会被标为多键索引,explain 中 “isMultikey”字段如果是true,说明使用了多键索引。索引一旦被标记为多键索引,就无法再变成非多键索引了,即时字段为数组的所有文档都被删除也不行,除非是将索引删除,再重建。多键索引比非多键索引慢一些,因为可能会有多个索引条目指向同一个文档,在返回结果时必须要先除去重复的内容。

索引基数

基数指集合中某个字段拥有不同值的数量,比如username 和 email 等基数很高,而重复值很多的字段,基数比较低。
通常一个字段基数越高,这个键上的索引就越有用,因为索引能够将搜索范围缩小到一个比较小的结果集上。
应该在基数比较高的键上建立索引,或者要把基数较高的键放在复合索引的前面(低基数的键之前)。

hint

如果发现MongoDB使用的索引和自己希望它使用的不一致,可以使用hint来强制使用特定的索引。

> db.testuser.find({"age" : 21}).hint({"username":1,"age":1})

使用 hint 时应该执行 explain 来查看效率,强制使用的索引,对于查询如果不知道如何使用,那么效率会降低,还不如不使用索引。

查询优化器

如果一个索引能够精确匹配一个查询,那么查询优化器就会使用这个索引。如果几个索引都适合查询,MongoDB会从这些可能的索引子集中为每次查询计划选择一个,这些查询计划是并行执行的。最早返回100个结果的就是胜者,其他的查询计划被中止。
这个查询计划会被缓存,这个查询接下来都会使用它,直到集合数据发生了比较大的变动。如果发生了大变动,查询优化器会重新挑选可行的查询计划。建立索引时,或者每执行 1000 次查询之后,查询优化器都会重新评估查询计划。

何时不应该使用索引

提取较小的子数据集时,索引非常高效。也有一些查询不使用索引会更快。结果集在原集合中所占的比例越大,索引的速度就越慢,因为使用索引需要进行两次查找:一次是查找索引条目,一次是根据索引指针去查找相应的文档。而全表扫描只需要进行一次查找:查找文档。在最坏的情况下(返回集合中的所有文档),使用索引进行的查找次数会是全表扫描的两倍,效率明显比全表扫描低很多。
但没有严格的规则来确定,如何根据数据大小、索引大小、文档大小以及结果集的平均大小来判断什么时候索引更有用,什么时候索引会降低查询速度。一般来说,如果查询需要返回集合内 30% 的文档,或者更多,那么就应该对两种方法进行比较。
hint({$natural : 1}) 可以强制数据库做全表扫描,按照文档在磁盘的顺序排列。

索引类型

唯一索引

唯一索引可以确保集合中的每一个文档的指定键都有唯一值。创建唯一索引时:

> db.testuser.createIndex({"username":1}, {"unique": ture})

唯一索引在新增文档时对有重复值的键时抛出异常。
_id 索引,就是一个唯一索引,会在创建集合时自动创建。
有的情况下,一个值可能无法被索引。索引储桶的大小是有限制的,如果某个索引条目超过了限制,那么这个条目就不会包含在索引里。使用这个索引时,会有一个文档凭空消失不见。所有的字段都必须小于 1024 字节,才能包含到索引里。如果一个文档的字段由于太大不能包含在索引里,那么唯一索引不会报错,即不会收到唯一索引的约束。

复合唯一索引

复合唯一索引,单个键的值可以相同,但所有键的组合值必须是唯一的。

去除重复

在现有的集合上,创建唯一索引可能会失败,因为可能已经存在重复值了。通常需要先对已有的数据进行处理,找出重复的数据,想办法处理。
如果希望直接删除重复的值,创建索引时使用 dropDups 选项,遇到重复的值时,第一个保留,之后的全删除。

> db.testuser.createIndex({"username":1}, {"unique": ture, "dropDups": true})

对于重要的数据,千万不要使用这种粗暴的方式。

稀疏索引

唯一索引对 null 也会看作值,所以无法将多个缺少唯一索引键的文档插入到集合中。如果有一个可能存在也可能不存在的字段,当他存在时必须是唯一的,这时可以将 unique 和 sparse 选项组合使用。
MongoDB 中的稀疏索引,与关系型数据库中的稀疏索引是完全不同的概念。MongoDB中的稀疏索引只是不需要将每个文档都作为索引条目。

> db.testuser.createIndex({"email":1}, {"unique": ture, "sparse": true})

email地址字段,可以不提供,如果提供就必须唯一,就可以这样创建索引。
去掉 unique ,就可以创建 非唯一的稀疏索引。
是否使用稀疏索引,同一个查询返回结果可能会不同。有些没有索引键的文档,在全表扫描可能返回,但稀疏索引由于没有包含这些文档,就不会返回。可以使用强制全表扫描。

索引管理

所有的数据库索引信息存储在 system.indexes 集合中。这是一个保留集合,不能在其中插入或者删除文档。

> db.testuser.getIndexes()
[
    {
        "v" : 2,
        "key" : {
            "_id" : 1
        },
        "name" : "_id_",
        "ns" : "testdb.testuser"
    },
    {
        "v" : 2,
        "key" : {
            "username" : 1.0
        },
        "name" : "username_1",
        "ns" : "testdb.testuser"
    },
    {
        "v" : 2,
        "key" : {
            "age" : 1.0,
            "username" : 1.0
        },
        "name" : "age_1_username_1",
        "ns" : "testdb.testuser"
    }
]

key 和 name 是很重要的,key 中键的顺序,name 可以被当作标识符使用。v 代表索引版本只在内部使用。

标识索引

如果不想用默认的 key1_dir1_key2_dir2 这样的名字的话,索引的名字可以指定

> > db.testuser.createIndex({"a":1,"b":1,"c":1}, {"name": "lalala"})

索引名字是有长度限制的,所以新建多键的复杂索引时,可能需要自定义索引名称。

修改索引

索引删除时,使用 dropIndex() 函数:

> db.testuser.dropIndex("age_1_username_1")

这里要用索引名称“name”字段的值指定要删除的索引。
新建索引是一个费时又浪费资源的事情。默认情况下,MongoDB会阻塞所有对数据库的读写请求,一直到索引创建完成。如果不希望阻塞,创建索引时可以加上 background 选项,如果有新的请求,那么创建索引的过程就会暂停一下,但是仍然会对应用程序的性能有比较大的影响,而且会慢很多。
另外,在已有的文档上创建索引,会比新创建索引再插入文档要快一些。

一些特殊的索引和集合

固定集合

MongoDB中有一种固定集合,事先创建好大小固定的,如果已经没有空间了,此时新文档插入,会将最老的文档删除来释放空间。类似队列。
数据被顺序写入磁盘中的固定空间,因此写入速度非常快。固定集合可以记录日志,但不能被分片。无法控制什么时候数据被覆盖。比如日志文件,聊天记录,通话信息记录等只需保留最近某段时间内的应用场景,都会使用到MongoDB的固定集合。

创建固定集合

> db.createCollection("fixcollection", {"capped":true, "size",100000})

> db.createCollection("fixcollection", {"capped":true, "size",100000, "max":100})

size 大小为 100000 字节,文档数量最大为 100 个。创建后就不能改变了,除非删除重建,所以固定集合创建之前应该仔细考虑大小。
如果指定了数量,就必须指定 size,不管先达到哪个限制,都会触发删除老文档的操作。
也可以将普通集合转为固定集合。

> db.runCommand({"convertToCapped" : "test", "size" : 10000})

自然排序

对固定集合的自然排序,返回就是文档在磁盘上的顺序。也可以逆序,如:

> db.fixcollection.find().sort({$natural : -1})

对大多数集合,自然排序的意义不大,因为文档的位置经常变动,但固定集合的文档是按照文档被插入的顺序保存的。

循环游标

固定集合有循环游标,循环游标的结果被取光后,游标并不会被关闭,当有新文档插入到集合中后,循环游标会继续取到结果。和 tail -f 的命令类似。

TTL索引

TTL索引,允许对为每个文档设置一个超时时间。一个文档达到寿命后就会被删除。这种类型的索引对于缓存问题非常有用。

> db.foo.createIndex({"lastUpdated":1}, {"expireAfterSecs":60*60*24})

在 lastUpdated 字段上建立的这个索引,如果一个文档的该字段存在并且是日期类型,如果服务器时间比文档该字段时间晚了24hours后,文档将会被删除。
为了防止活跃的会话被删除,可以在会话有活动发生时更新为当前时间,这样就不会误删活动的会话。
MongoDB每分钟对TTL索引进行一次清理,所以不应该依赖秒为单位的时间保证索引的存活状态。
在一个给定的集合上可以有多个TTL索引,但不能是复合索引。可以用来优化排序和查询。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值