MongoDB指南---十二、使用explain()和hint()、什么时候不该该使用索引

上一篇文章: MongoDB指南---十一、使用复合索引、$操做符如何使用索引、索引对象和数组、索引基数
下一篇文章: MongoDB指南---1三、索引类型

使用explain()和hint()

从上面的内容能够看出,explain()可以提供大量与查询相关的信息。对于速度比较慢的查询来讲,这是最重要的诊断工具之一。经过查看一个查询的explain()输出信息,能够知道查询使用了哪一个索引,以及是如何使用的。对于任意查询,均可以在最后添加一个explain()调用(与调用sort()或者limit()同样,不过explain()必须放在最后)。
最多见的explain()输出有两种类型:使用索引的查询和没有使用索引的查询。对于特殊类型的索引,生成的查询计划可能会有些许不一样,可是大部分字段都是类似的。另外,分片返回的是多个explain()的聚合(第13章会介绍),由于查询会在多个服务器上执行。
不使用索引的查询的exlpain()是最基本的explain()类型。若是一个查询不使用索引,是由于它使用了"BasicCursor"(基本游标)。反过来讲,大部分使用索引的查询使用的是BtreeCursor(某些特殊类型的索引,好比地理空间索引,使用的是它们本身类型的游标)。
对于使用了复合索引的查询,最简单状况下的explain()输出以下所示:数据库

> db.users.find({"age" : 42}).explain()
{
    "cursor" : "BtreeCursor age_1_username_1",
    "isMultiKey" : false,
    "n" : 8332,
    "nscannedObjects" : 8332,
    "nscanned" : 8332,
    "nscannedObjectsAllPlans" : 8332,
    "nscannedAllPlans" : 8332,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 91,
    "indexBounds" : {
        "age" : [
            [
                42,
                42
            ]
        ],
        "username" : [
            [
                {
                    "$minElement" : 1
                },
                {
                    "$maxElement" : 1
                }
            ]
        ]
    },
    "server" : "ubuntu:27017"
}

从输出信息中能够看到它使用的索引是age_1_username_1。"millis"代表了这个查询的执行速度,时间是从服务器收到请求开始一直到发出响应为止。然而,这个数值不必定真的是你但愿看到的值。若是MongoDB尝试了多个查询计划,那么"millis"显示的是这些查询计划花费的总时间,而不是最优查询计划所花的时间。
接下来是实际返回的文档数量:"n"。它没法反映出MongoDB在执行这个查询的过程当中所作的工做:搜索了多少索引条目和文档。索引条目是使用"nscanned"描述的。"nscannedObjects"字段的值就是所扫描的文档数量。最后,若是要对结果集进行排序,而MongoDB没法对排序使用索引,那么"scanAndOrder"的值就会是true。也就是说,MongoDB不得不在内存中对结果进行排序,这是很是慢的,并且结果集的数量要比较小。
如今你已经知道这些基础知识了,接下来依次详细介绍这些字段。ubuntu

  • "cursor" : "BtreeCursor age_1_username_1"

BtreeCursor表示本次查询使用了索引,具体来讲,是使用了"age"和"username"上的索引{"age" : 1, "username" : 1}。若是查询要对结果进行逆序遍历,或者是使用了多键索引,就能够在这个字段中看到"reverse"和"multi"这样的值。segmentfault

  • "isMultiKey" : false

用于说明本次查询是否使用了多键索引(详见5.1.4节)。数组

  • "n" : 8332

本次查询返回的文档数量。缓存

  • "nscannedObjects" : 8332

这是MongoDB按照索引指针去磁盘上查找实际文档的次数。若是查询包含的查询条件不是索引的一部分,或者说要求返回不在索引内的字段,MongoDB就必须依次查找每一个索引条目指向的文档。服务器

  • "nscanned" : 8332

若是有使用索引,那么这个数字就是查找过的索引条目数量。若是本次查询是一次全表扫描,那么这个数字就表示检查过的文档数量。工具

  • "scanAndOrder" : false

MongoDB是否在内存中对结果集进行了排序。优化

  • "indexOnly" : false

MongoDB是否只使用索引就能完成这次查询(详见“覆盖索引”部分)。
在本例中,MongoDB只使用索引就找到了所有的匹配文档,从"nscanned"和"n"相等就能够看出来。然而,本次查询要求返回匹配文档中的全部字段,而索引只包含"age"和"username"两个字段。若是将本次查询修改成({"_id" : 0, "age" : 1, "username" : 1}),那么本次查询就能够被索引覆盖了,"indexOnly"的值就会是true。指针

  • "nYields" : 0

为了让写入请求可以顺利执行,本次查询暂停的次数。若是有写入请求须要处理,查询会周期性地释放它们的锁,以便写入可以顺利执行。然而,在本次查询中,没有写入请求,由于查询没有暂停过。code

  • "millis" : 91

数据库执行本次查询所耗费的毫秒数。这个数字越小,说明查询效率越高。

  • "indexBounds" : {...}

这个字段描述了索引的使用状况,给出了索引的遍历范围。因为查询中的第一个语句是精确匹配,所以索引只须要查找42这个值就能够了。本次查询没有指定第二个索引键,所以这个索引键上没有限制,数据库会在"age"为42的条目中将用户名介于负无穷("$minElement" : 1)和正无穷("$maxElement" : 1)的条目都找出来。
再来看一个稍微复杂点的例子:假若有一个{"user name" : 1, "age" : 1}上的索引和一个 {"age" : 1, "username" : 1}上的索引。同时查询"username"和"age"时,会发生什么状况?呃,这取决于具体的查询:

> db.c.find({age : {$gt : 10}, username : "sally"}).explain()
{
    "cursor" : "BtreeCursor username_1_age_1",
    "indexBounds" : [
        [
            {
                "username" : "sally",
                "age" : 10
            },
            {
                "username" : "sally",
                "age" : 1.7976931348623157e+308
            }
        ]
    ],
    "nscanned" : 13,
    "nscannedObjects" : 13,
    "n" : 13,
    "millis" : 5
}

因为在要在"username"上执行精确匹配,在"age"上进行范围查询,所以,数据库选择使用{"username" : 1, "age" : 1}索引,这与查询语句的顺序相反。另外一方面来讲,若是须要对"age"精确匹配而对"username"进行范围查询,MongoDB就会使用另外一个索引:

> db.c.find({"age" : 14, "username" : /.*/}).explain()
{
    "cursor" : "BtreeCursor age_1_username_1 multi",
    "indexBounds" : [
        [
            {
                "age" : 14,
                "username" : ""
            },
            {
                "age" : 14,
                "username" : {
                }
            }
        ],
        [
            {
                "age" : 14,
                "username" : /.*/
            },
            {
                "age" : 14,
                "username" : /.*/
            }
        ]
    ],
    "nscanned" : 2,
    "nscannedObjects" : 2,
    "n" : 2,
    "millis" : 2
}

若是发现MongoDB使用的索引与本身但愿它使用的索引不一致,可使用hit()强制MongoDB使用特定的索引。例如,若是但愿MongoDB在上个例子的查询中使用{"username" : 1, "age" : 1}索引,能够这么作:

> db.c.find({"age" : 14, "username" : /.*/}).hint({"username" : 1, "age" : 1})

若是查询没有使用你但愿它使用的索引,因而你使用hint强制MongoDB使用某个索引,那么应该在应用程序部署以前在所指定的索引上执行explain()。若是强制MongoDB在某个查询上使用索引,而这个查询不知道如何使用这个索引,这样会致使查询效率下降,还不如不使用索引来得快。

查询优化器

MongoDB的查询优化器与其余数据库稍有不一样。基原本说,若是一个索引可以精确匹配一个查询(要查询"x",恰好在"x"上有一个索引),那么查询优化器就会使用这个索引。否则的话,可能会有几个索引都适合你的查询。MongoDB会从这些可能的索引子集中为每次查询计划选择一个,这些查询计划是并行执行的。最先返回100个结果的就是胜者,其余的查询计划就会被停止。
这个查询计划会被缓存,这个查询接下来都会使用它,直到集合数据发生了比较大的变更。若是在最初的计划评估以后集合发生了比较大的数据变更,查询优化器就会从新挑选可行的查询计划。创建索引时,或者是每执行1000次查询以后,查询优化器都会从新评估查询计划。
explain()输出信息里的"allPlans"字段显示了本次查询尝试过的每一个查询计划。

 什么时候不该该使用索引

提取较小的子数据集时,索引很是高效。也有一些查询不使用索引会更快。结果集在原集合中所占的比例越大,索引的速度就越慢,由于使用索引须要进行两次查找:一次是查找索引条目,一次是根据索引指针去查找相应的文档。而全表扫描只须要进行一次查找:查找文档。在最坏的状况下(返回集合内的全部文档),使用索引进行的查找次数会是全表扫描的两倍,效率会明显比全表扫描低不少。
惋惜,并无一个严格的规则能够告诉咱们,如何根据数据大小、索引大小、文档大小以及结果集的平均大小来判断何时索引颇有用,何时索引会下降查询速度(如表5-1所示)。通常来讲,若是查询须要返回集合内30%的文档(或者更多),那就应该对索引和全表扫描的速度进行比较。然而,这个数字可能会在2%~60%之间变更。
表5-1 影响索引效率的属性

索引一般适用的状况 全表扫描一般适用的状况
集合较大 集合较小
文档较大 文档较小
选择性查询 非选择性查询

假如咱们有一个收集统计信息的分析系统。应用程序要根据给定帐户去系统中查询全部文档,根据从初始一直到一小时以前的数据生成图表:

> db.entries.find({"created_at" : {"$lt" : hourAgo}})

咱们在"created_at"上建立索引以提升查询速度。
最初运行时,结果集很是小,能够当即返回。几个星期过去之后,数据开始多起来了,一个月以后,这个查询耗费的时间愈来愈长。
对于大部分应用程序来讲,这极可能就是那个“错误的”查询:真的须要在查询中返回数据集中的大部份内容吗?大部分应用程序(尤为是拥有很是大的数据集的应用程序)都不须要。然而,也有一些合理的状况,可能须要获得大部分或者所有的数据:也许须要将这些数据导出到报表系统,或者是放在批量任务中。在这些状况下,应该尽量快地返回数据集中的内容。
能够用{"$natural" : 1}强制数据库作全表扫描。6.1节会介绍$natural,它能够指定文档按照磁盘上的顺序排列。特别地,$natural能够强制MongoDB作全表扫描:

> db.entries.find({"created_at" : {"$lt" : hourAgo}}).hint({"$natural" : 1})

使用"$natural"排序有一个反作用:返回的结果是按照磁盘上的顺序排列的。对于一个活跃的集合来讲,这是没有意义的:随着文档体积的增长或者缩小,文档会在磁盘上进行移动,新的文档会被写入到这些文档留下的空白位置。可是,对于只须要进行插入的工做来讲,若是要获得最新的(或者最先的)文档,使用$natural就很是有用了。

上一篇文章: MongoDB指南---十一、使用复合索引、$操做符如何使用索引、索引对象和数组、索引基数
下一篇文章: MongoDB指南---1三、索引类型
相关文章
相关标签/搜索