【问题标题】:MongoDB /AWS documentdb index utilization with and without limitMongoDB /AWS documentdb 索引利用率有无限制
【发布时间】:2020-06-10 17:40:48
【问题描述】:

这是我的索引:

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
}

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

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

【问题讨论】:

    标签: mongodb indexing aws-documentdb


    【解决方案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"},它将扫描索引以查找[MinKey(),5) 范围内的foo 值,并且对于遇到的每个foo 值,它都会扫描bar 的匹配值.这意味着它不需要扫描索引的单个范围,而是需要扫描 5 个范围以找到 2 个匹配项。

    查询+排序

    如果我们在{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 个文档(如果尚未在缓存中)

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

    【讨论】:

    • 谢谢,我不清楚您的索引示例中的数字指向什么:B|2|Y => 1 1 代表什么?此外,我的文档每个都有数兆字节,并且有数百万个,所以如果需要排序和过滤的所有内容都在索引中,我看不出如何进行 collscan 是一个更好的计划。
    • 1代表内部文档标识符,在这种情况下我只是按照它们出现的顺序对原始的10个文档进行编号。
    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-01
      • 1970-01-01
      相关资源
      最近更新 更多