1

这是我的索引:

db.foobar.createIndex( { 'foo' : -1, 'bar' : 1, 'baz' : 1 }, { background : true, name : 'foobar_idx' } );

现在我希望排序foo和过滤的查询bar将使用索引。如果您指定一个限制,它确实如此:

rs0:PRIMARY> db.foobar.find( { 'bar' : 'xyz' }, { 'some.field' : 1 } ).sort( { 'foo' : -1 } ).limit(1000).explain()
{
        "queryPlanner" : {
                "plannerVersion" : 1,
                "namespace" : "foobardb",
                "winningPlan" : {
                        "stage" : "SUBSCAN",
                        "inputStage" : {
                                "stage" : "LIMIT_SKIP",
                                "inputStage" : {
                                        "stage" : "IXSCAN",
                                        "indexName" : "foobar_idx",
                                        "direction" : "forward"
                                }
                        }
                }
        },
        "ok" : 1
}

但是如果你不指定限制,或者限制非常高,它就不想使用索引:

rs0:PRIMARY> db.foobar.find( { 'bar' : 'xyz' }, { 'some.field' : 1 } ).sort( { 'foo' : -1 } ).explain()
{
        "queryPlanner" : {
                "plannerVersion" : 1,
                "namespace" : "foobardb",
                "winningPlan" : {
                        "stage" : "SUBSCAN",
                        "inputStage" : {
                                "stage" : "SORT",
                                "sortPattern" : {
                                        "foo" : -1
                                },
                                "inputStage" : {
                                        "stage" : "COLLSCAN"
                                }
                        }
           },
        "ok" : 1
}

即使我提供使用索引的提示,它也不会使用它。

为什么它不使用该死的索引?

4

2 回答 2

1

要理解这种行为,您必须考虑索引是如何构建的,以及它是如何被搜索的。

考虑一个包含这 10 个文档的集合:

{"foo" : 9, "bar" : "A", "baz" : "Y" }
{"foo" : 2, "bar" : "B", "baz" : "Y" }
{"foo" : 5, "bar" : "A", "baz" : "Z" }
{"foo" : 0, "bar" : "A", "baz" : "Y" }
{"foo" : 6, "bar" : "A", "baz" : "X" }
{"foo" : 4, "bar" : "B", "baz" : "Y" }
{"foo" : 8, "bar" : "A", "baz" : "Z" }
{"foo" : 1, "bar" : "A", "baz" : "Y" }
{"foo" : 7, "bar" : "B", "baz" : "Z" }
{"foo" : 3, "bar" : "B", "baz" : "X" }

如果我们在索引上定义一个索引{foo:1, bar:1, baz:1}将包含这些对:

0|A|Y => 3
1|A|Y => 7
2|B|Y => 1
3|B|X => 9
4|B|Y => 5
5|A|Z => 2
6|A|X => 4
7|B|Z => 8
8|A|Z => 6
9|A|Y => 0

平等查询

如果我们然后查询{foo:5, bar:"A"},查询执行器可以从第一个匹配值开始扫描,5|A|Z。在这种情况下,它是唯一匹配的值,所以它就在那里结束。

范围查询

如果我们接着查询{foo: {$lt:5}, bar:"A"},它将扫描索引以查找foo范围内的值[MinKey(),5),并且对于foo遇到的每个值,它都会扫描匹配的值bar。这意味着它不需要扫描索引的单个范围,而是需要扫描 5 个范围以找到 2 个匹配项。

查询+排序

如果我们查询 on{bar: "A"}并按 排序{foo:1},如果查询执行器试图使用这个索引,它需要检查索引中的每个条目,并为每个值foo进行扫描以查找匹配的值bar。对于此示例,这意味着 10 个范围。

查询计划

当第一次看到查询形状时,查询计划器会识别它可能运行查询的不同方式,并运行测试。每个计划都运行很短的时间,然后选择以最少的工作量产生最多结果的计划。

在 的情况下db.foobar.find({bar:"A"}).sort({foo:1}),我们的测试场景有 2 个可能的计划:

计划 A:索引扫描

  • 从磁盘加载索引(如果尚未在缓存中)
  • 扫描 10 个索引范围
  • 从磁盘加载 6 个文档(如果尚未在缓存中)

B计划:收集扫描

  • 从磁盘加载 10 个文档(如果尚未在缓存中)
  • 在内存中排序

根据缓存中已经存在的内容,这里的选择有点复杂。

使用限制

当您引入一个较小的限制时,例如db.foobar.find({bar:"A"}).sort({foo:1}).limit(2),当使用找到已按排序顺序的文档的索引时,他们的查询能够提前终止。在这种情况下,可能的计划如下所示:

计划 A:索引扫描

  • 从磁盘加载索引(如果尚未在缓存中)
  • 扫描 2 个索引范围
  • 从磁盘加载 2 个文档(如果尚未在缓存中)

B计划:收集扫描

  • 从磁盘加载 10 个文档(如果尚未在缓存中)
  • 在内存中排序
  • 限制为 2 个文件

很明显,索引扫描在这种情况下会表现得更好。

对于更大的限制,这并不那么明显。考虑一下db.foobar.find({bar:"A"}).sort({foo:1}).limit(5),对于这个查询,可能的计划是:

计划 A:索引扫描

  • 从磁盘加载索引(如果尚未在缓存中)
  • 扫描 9 个索引范围
  • 从磁盘加载 5 个文档(如果尚未在缓存中)

B计划:收集扫描

  • 从磁盘加载 10 个文档(如果尚未在缓存中)
  • 在内存中排序
  • 限制为 5 个文件

这几乎回到了与无限案例相同的计划。

更好的索引

在 MongoDB 中构建索引时,请考虑您计划如何查询数据,并根据相等排序范围对索引中的键进行排序。这意味着列出您将完全匹配的字段,然后是要排序的字段,然后是任何其他字段。

对于我们的示例,索引{bar:1, foo:1, baz:1}将包含以下对:

A|0|Y => 3
A|1|Y => 7
A|5|Z => 2
A|6|X => 4
A|8|Z => 6
A|9|Y => 0
B|2|Y => 1
B|3|X => 9
B|4|Y => 5
B|7|Z => 8

排序后的查询db.foobar.find({bar:"A"}).sort({foo:1})将有另一个可能的计划:

计划 C:索引扫描

  • {bar:1, foo:1, baz:1}扫描索引的单个范围
  • 从磁盘中获取 6 个文档(如果尚未在缓存中)

该计划应大大优于所有其他可能性,并且应用限制会减少该计划完成的工作,因此仍应选择它。

于 2020-06-10T09:40:48.220 回答
0

如果索引的选择性不足,则表扫描可能比索引扫描更有效。存储系统也会影响决策(旋转磁盘有利于表扫描,SSD 有利于索引扫描)。

于 2020-06-09T07:03:40.517 回答