资讯专栏INFORMATION COLUMN

MongoDB指南---12、使用explain()和hint()、何时不应该使用索引

LiangJ / 1823人阅读

摘要:表示本次查询使用了索引,具体来说,是使用了和上的索引,。建立索引时,或者是每执行次查询之后,查询优化器都会重新评估查询计划。上一篇文章指南使用复合索引操作符如何使用索引索引对象和数组索引基数下一篇文章指南索引类型

上一篇文章:MongoDB指南---11、使用复合索引、$操作符如何使用索引、索引对象和数组、索引基数
下一篇文章:MongoDB指南---13、索引类型
使用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不得不在内存中对结果进行排序,这是非常慢的,而且结果集的数量要比较小。
现在你已经知道这些基础知识了,接下来依次详细介绍这些字段。

"cursor" : "BtreeCursor age_1_username_1"

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

"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

为了让写入请求能够顺利执行,本次查询暂停的次数。如果有写入请求需要处理,查询会周期性地释放它们的锁,以便写入能够顺利执行。然而,在本次查询中,没有写入请求,因为查询没有暂停过。

"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指南---11、使用复合索引、$操作符如何使用索引、索引对象和数组、索引基数
下一篇文章:MongoDB指南---13、索引类型

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/19579.html

相关文章

  • MongoDB指南---12使用explain()hint()、何时应该使用索引

    摘要:表示本次查询使用了索引,具体来说,是使用了和上的索引,。建立索引时,或者是每执行次查询之后,查询优化器都会重新评估查询计划。上一篇文章指南使用复合索引操作符如何使用索引索引对象和数组索引基数下一篇文章指南索引类型 上一篇文章:MongoDB指南---11、使用复合索引、$操作符如何使用索引、索引对象和数组、索引基数下一篇文章:MongoDB指南---13、索引类型 使用explain...

    DTeam 评论0 收藏0
  • MongoDB指南---13、索引类型、索引管理

    摘要:复合唯一索引也可以创建复合的唯一索引。中的稀疏索引与关系型数据库中的稀疏索引是完全不同的概念。但是这里不会指明索引是否是多键索引。上一篇文章指南使用和何时不应该使用索引下一篇文章指南特殊的索引和集合固定集合索引全文本索引 上一篇文章:MongoDB指南---12、使用explain()和hint()、何时不应该使用索引下一篇文章:MongoDB指南---14、特殊的索引和集合:固定集合...

    Enlightenment 评论0 收藏0
  • MongoDB指南---13、索引类型、索引管理

    摘要:复合唯一索引也可以创建复合的唯一索引。中的稀疏索引与关系型数据库中的稀疏索引是完全不同的概念。但是这里不会指明索引是否是多键索引。上一篇文章指南使用和何时不应该使用索引下一篇文章指南特殊的索引和集合固定集合索引全文本索引 上一篇文章:MongoDB指南---12、使用explain()和hint()、何时不应该使用索引下一篇文章:MongoDB指南---14、特殊的索引和集合:固定集合...

    seanHai 评论0 收藏0
  • MongoDB指南---11、使用复合索引、$操作符如何使用索引索引对象数组、索引基数

    摘要:操作符如何使用索引有一些查询完全无法使用索引,也有一些查询能够比其他查询更高效地使用索引。有时能够使用索引,但是通常它并不知道要如何使用索引。索引对象和数组允许深入文档内部,对嵌套字段和数组建立索引。 上一篇文章:MongoDB指南---10、索引、复合索引 简介下一篇文章:MongoDB指南---12、使用explain()和hint()、何时不应该使用索引 1、使用复合索引 在多...

    saucxs 评论0 收藏0
  • MongoDB指南---11、使用复合索引、$操作符如何使用索引索引对象数组、索引基数

    摘要:操作符如何使用索引有一些查询完全无法使用索引,也有一些查询能够比其他查询更高效地使用索引。有时能够使用索引,但是通常它并不知道要如何使用索引。索引对象和数组允许深入文档内部,对嵌套字段和数组建立索引。 上一篇文章:MongoDB指南---10、索引、复合索引 简介下一篇文章:MongoDB指南---12、使用explain()和hint()、何时不应该使用索引 1、使用复合索引 在多...

    tomlingtm 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<