MongoDB:复合索引决策

3
我最近需要优化我们 MongoDB 上的某些查询,并遇到了这个特定的问题:
假设我有一个查询,匹配 AB,然后在 C 上进行范围选择,并通过对 D 进行排序来输出结果,在 shell 中它们看起来像这样:
db.collection.find({ A: 'something', B: 'something-else', C: { $gt: 100 } })
             .sort({ D: -1 }).limit(10)

去年我阅读了一篇文章,讲述了为这种情况创建索引的基本规则:

  1. 精确匹配字段优先
  2. 排序字段其次
  3. 范围搜索($in、$gt等)字段最后

他们的解释看起来很合理,所以我按照此方式创建了一个索引:

db.collection.ensureIndex({ A:1, B:1, D:-1, C:-1 })

现在问题来了:mongodb认为BasicCursor比这个索引更好。如果我使用hint全索引,它可以工作(并且速度更快),但这样做需要对我们的代码库进行相当多的更改,因此我们尽可能避免这种情况。

我的问题是:

  1. 当我的查询包含所有4个字段时,为什么mongodb查询优化器会决定选择{ A:1, E:-1 }{ D:-1 }甚至是BasicCursor比{ A:1, B:1, D:-1, C:-1 }更好。

  2. { A:1, D:-1 }是否多余,mongo docs确实说使用部分索引不太有效率?

此外,我们还有以下查询:

db.collection.find({ A: { $in : ['str1','str2'] }, B: 'something', C: { $gt: 100 } })
             .sort({ D: -1 }).limit(10)

为了高效地查询它,我们需要像下面这样的额外索引吗?坦白说,我不确定MongoDB查询优化器会如何处理它们。
db.collection.ensureIndex({ B:1, D:-1, C:-1, A:1 })

这是我的查询带或不带提示的解释。 结果表明它默认为 { A:1, E:-1 } 而不是 { A:1, D:-1 },这似乎更奇怪,因为我们没有在字段 E 上进行查询。
我在{ A:1, E:-1 }上删除了索引,现在解释告诉我它默认为{ D:-1 },所以我也删除了它,现在MongoDB开始使用BasicCursor... 它似乎不喜欢我的完整索引或A:1, D:-1索引(尽管提示结果表现更好)。
感觉很奇怪。

最有可能的是,两个字段的复合索引在更短的时间内产生了更多的结果,因此它赢得了比4个字段的复合索引更快的速度,这是优化器中那些罕见的情况之一。至于这个两个字段的索引是否是多余的...嗯,我会选择是的,但我必须看到这两个索引的解释才能确定。 - Sammaye
@Sammaye 我已经更新了我的帖子并添加了解释信息。 - bitinn
@Sammeye,你说得很有可能是正确的。除了我的主索引之外,我几乎删除了所有索引,似乎mongodb决定最好的方法是BasicCursor。不过这也带来了一个问题,A、B、C或A、B、C、D哪个是下一个最佳选择呢? - bitinn
问题在于,链接的博客文章是18个月前写的,特别是他们没有考虑到2.2和2.4中针对索引使用进行的大量优化。我建议阅读这篇文章http://emptysqua.re/blog/optimizing-mongodb-compound-indexes/,并具体查看B:1、A:1、C:1、D:1在有或没有提示的情况下对您的作用。 - Asya Kamsky
@AsyaKamsky,这并没有提供一个解决方案,它仍然说a、b、d、c是最好的选择,并且你必须使用提示才能让它工作。 - Sammaye
显示剩余4条评论
2个回答

1
唯一发生这种“不寻常”情况的原因是您的数据分布恰好使BasicCursor完成查询(即找到所有匹配的文档)比索引查询更快。同样适用于“部分”索引。
一个特定的情况是,以您的数据结构为例,如果a在集合开头具有相对较少的不同值,并且b的基数极低(即非常少的不同值,如一个或一小撮),那么按顺序扫描集合或使用“不太有效”的索引将显示等效或更好的性能,而不是使用理论上“理想”的索引。
以下是一个示例,其中前1000个文档具有a = 1和b = 2-后面的文档分布非常不同。
> db.compound4.find({a:1, b:2, d:{$lt:100}}).sort({c:-1}).limit(10).explain(true)
{
    "cursor" : "BtreeCursor a_1",
    "isMultiKey" : false,
    "n" : 10,
    "nscannedObjects" : 18,
    "nscanned" : 18,
    "nscannedObjectsAllPlans" : 46,
    "nscannedAllPlans" : 56,
    "scanAndOrder" : true,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 0,
    "indexBounds" : {
        "a" : [
            [
                1,
                1
            ]
        ]
    },
    "allPlans" : [
        {
            "cursor" : "BtreeCursor a_1",
            "n" : 18,
            "nscannedObjects" : 18,
            "nscanned" : 18,
            "indexBounds" : {
                "a" : [
                    [
                        1,
                        1
                    ]
                ]
            }
        },
        {
            "cursor" : "BtreeCursor a_1_b_1_c_1_d_1 reverse",
            "n" : 10,
            "nscannedObjects" : 10,
            "nscanned" : 20,
            "indexBounds" : {
                "a" : [
                    [
                        1,
                        1
                    ]
                ],
                "b" : [
                    [
                        2,
                        2
                    ]
                ],
                "c" : [
                    [
                        {
                            "$maxElement" : 1
                        },
                        {
                            "$minElement" : 1
                        }
                    ]
                ],
                "d" : [
                    [
                        100,
                        -1.7976931348623157e+308
                    ]
                ]
            }
        },
        {
            "cursor" : "BasicCursor",
            "n" : 18,
            "nscannedObjects" : 18,
            "nscanned" : 18,
            "indexBounds" : {

            }
        }
    ]
}

由于复合索引较大,遍历时间比较长,而且由于“b”的选择性不太好(即非常差),使得查询计划落后。


我会接受这个答案,因为它指引了我正确的方向。请查看我的回答,了解完整索引未被选择的确切原因。 - bitinn
我的观点是特定的数据分布将决定此时最佳的索引 - 优化器会定期重新测试,因此如果数据分布发生变化,它将“注意到”并更改查询计划(除非存在限制和错误 :)) - Asya Kamsky

0

在使用explain(true)运行我的原始查询后,mongodb查询优化器背后的推理变得更加清晰。

这是因为字段C是对一个int字段进行范围搜索,并且预计具有很高的选择性,但在搜索开始时没有太多匹配结果(特别是当结果按D和降序排序时,这是一个日期字段)。

mongodb查询优化器背后的基本逻辑似乎是:在给定相同数量的n个结果的情况下,使用具有最小nscanned(如果nscanned = n,则最佳)的计划(索引)。在n较小的情况下,例如limit(10),优化器可能不会使用我们为其计划的最有效的索引。

在我的情况中,结果表明,索引{ D:-1 }BasicCursor都击败了完整索引。所以谜团解开了。

附注:出于好奇,这些是我测试数据集的explain(true)输出:

希望这能帮助到某些人 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接