使用不同字段进行排序的MongoDB索引以加速find()操作

6

我正在运行许多这样类型的查询:

db.mycollection.find({a:{$gt:10,$lt:100}, b:4}).sort({c:-1, a:-1})

我应该使用什么样的索引来加快速度?我认为我需要同时使用{a:1, b:1}{c:-1, a:-1},是吗?或者这些索引会在没有性能提升的情况下相互干扰?
编辑:我的实际问题是我在循环中运行许多查询,其中一些查询涉及小范围,而其他查询涉及大范围。如果我在{a:1, b:1}上创建索引,它会非常快速地选择小块,但当涉及到大范围时,我会看到一个错误“too much data for sort() with no index”。如果我在{c:-1, a:-1}上创建索引,则不会出现错误,但处理小块(而且有更多)的速度要慢得多。那么,如何保持对较小范围的快速选择,但在处理大量数据时不会出现错误呢?
如果有影响的话,我是通过Python的pymongo运行查询的。

你尝试过我们回答中提到的Sammaye或我建议的任何方法吗?是否有帮助,正如您在赏金描述中所写的那样?如果需要我们进一步开发我们的答案,请告诉我们。 - attish
如果您可以分享ABC背后的真实值,将有助于提供更具体的答案。例如,如果AISODate,而B是一个在所有文档中都找不到的标志,则我可以轻松优化您的查询。 - Oran
不知道为什么你标记了奥兰的答案,它完全是错误的。Attishs的答案是你所能期望的最好的,即使使用奥兰的第二个解决方案,你仍然会得到扫描和排序或未索引的查找。 - Sammaye
@Sammaye 嗯,这里的每个答案都有些缺陷。我选择了那个答案是因为它让我正确地重新设计了查询数据的方式。总体来说,我从最初打算使用的查询转移到了使用几个单独的查询,并在结果可能会很大的情况下使用 hint()在 sort()上使用索引,并在结果很小的情况下使用 hint()在 find()上查找。总之,我赞成所有做出良好努力的答案,但最终 Oran 的想法是对我有帮助的(是的,它并不完全正确,但其他答案也不完全正确)。 - sashkello
那个人并没有说你所做的一切。我可以请你用这样的话回答一个问题吗(复制上述文本),并接受它,即使它真的很糟糕,带有许多技术错误和对技术的明显误解,但可能会被认为是有用的。 - attish
5个回答

4
如果你阅读过文档,就会发现在这里使用两个索引是无用的,因为MongoDB每次查询只使用一个索引(除非它是$or),直到实现了https://jira.mongodb.org/browse/SERVER-3071
而且,当使用复合排序时,索引中的顺序必须与排序顺序匹配,才能正确使用索引,如下所示:

换句话说,这些索引会互相干扰,而不会提高性能吗?

如果实现了交集操作,那么它们不会相互干扰,{a:1,b:1}不匹配排序,{c:-1,a:-1}对于回答find()是次优的,而且a不是该复合键的前缀。
所以,立即迭代最优索引的方法如下:
{a:-1,b:1,c:-1}

但这并不是全部,由于$gt$lt实际上是范围,在索引方面会遇到与$in相同的问题。本文将为您提供答案:http://blog.mongolab.com/2012/06/cardinal-ins/没有必要重复其内容。


应该将a放在索引的最后。这个链接对我们非常有帮助,可以将查询时间从几千毫秒降至几毫秒。如果查询没有使用正确的索引,可能需要使用提示。 - titogeo
好的,如果我按照你的建议去做,它就无法识别sort()的索引。 - sashkello
@sashkello 当然,使用索引交集,索引会变得更容易。 - Sammaye
根据您提供的文章,最佳索引为{c:-1,a:-1,b:1},正如我所提到的。我已更新我的答案,感谢您提供的来源。 - attish
对于那些点赞@attish答案的人,实际上它更好,并提供可能是目前唯一有效的索引,虽然不够优化但仍然可行。 - Sammaye
显示剩余5条评论

4
免责声明:适用于 MongoDB v2.4 使用hint是一个很好的解决方案,因为它会强制查询使用您选择的索引,所以您可以通过不同的索引来优化查询,直到您满意为止。 缺点是您在每个请求中都设置自己的索引。 我更喜欢在整个集合上设置索引,并让Mongo为我选择正确(最快)的索引,特别是对于反复使用的查询。
您的查询有两个问题:
  • 永远不要在没有索引的参数上进行排序。如果您.find()中的文档数量非常大,大小取决于您使用的mongo版本,则会出现错误"too much data for sort() with no index"。这意味着您必须在AC上建立索引才能使查询正常工作。
  • 现在是更大的问题。您正在执行一个范围查询(对参数A使用$lt$gt),这在Mongo中无法工作。MongoDB一次只使用一个索引,而您正在同一参数上使用两个索引。有几种解决方案来处理代码中的这个问题:

    1. r = range( 11,100 )
      db.mycollection.find({a:{$in: r }, b:4}).sort({c:-1, a:-1})

    2. 在您的查询中仅使用$lt$gt
      db.mycollection.find({ a: { $lt:100 }, b:4}).sort({c:-1, a:-1})
      获取结果并在Python代码中过滤它们。 此解决方案将返回更多数据,因此如果您有数百万个结果小于A=11,请不要使用它!
      如果您选择此选项,请确保使用带有AB复合键

在查询中使用$or时要注意,因为$or与$in相比优化效率较低,它使用索引的方式不同。


1
当我在查询中使用 $gte$lt 与时间戳一起使用时,遇到了索引类似的问题。仅使用一个条件然后在MongoDB返回结果后进行过滤是个好主意。 - Lix
1
使用提示并不是一个很好的解决方案,因为它要求在应用程序中持久化编码更改。例如,如果您想利用MongoDB中的索引交集,您将不得不重写所有带提示的查询以使用不同的提示,这会破坏KISS和DRY范例,这些范例有助于保持程序员的理智。另外提示并不设置索引,它仅仅设置该查询的索引使用情况,索引仍然在集合级别定义。 - Sammaye
1
顺便提一下,$or可以使用索引,它会为每个子句使用一个索引。 - Sammaye
你有没有一些文章可以建议,在第二种情况下,如果两个约束都是同一个字段,为什么单个$gt可以利用索引而区间查询不能使用相同的索引。在像mongodb这样使用的b-tree索引中,不应该有任何差异。 - attish
1
它仍然是10gen,只是换了个名字,那有什么区别呢?有多年使用Mongo的老手开发人员仍然记得旧名称,因为更名是在2013年8月27日进行的。你仍然可以在他们的主页中看到对旧名称的引用。 - Oran
显示剩余14条评论

2
如果定义一个索引 {c:-1,a:-1,b:1},它将有助于一些考虑。
使用此选项将完全扫描索引,但仅基于索引值访问适当的文档,并且它们将按正确顺序访问,因此在获取结果后不需要排序阶段。如果索引非常大,我不知道它会如何表现,但是我假设当结果较小时,速度会较慢,在结果集很大的情况下,速度会更快。
关于前缀匹配。如果提示索引和较低级别可用于服务查询,则将使用这些级别。为了演示这种行为,我进行了一个简短的测试。
我准备了测试数据:
> db.createCollection('testIndex')
{ "ok" : 1 }
> db.testIndex.ensureIndex({a:1,b:1})
> db.testIndex.ensureIndex({c:-1,a:-1})
> db.testIndex.ensureIndex({c:-1,a:-1,b:1})
> for(var i=1;i++<500;){db.testIndex.insert({a:i,b:4,c:i+5});}
> for(var i=1;i++<500;){db.testIndex.insert({a:i,b:6,c:i+5});}

查询结果(带提示):
> db.testIndex.find({a:{$gt:10,$lt:100}, b:4}).hint('c_-1_a_-1_b_1').sort({c:-1, a:-1}).explain()
{
    "cursor" : "BtreeCursor c_-1_a_-1_b_1",
    "isMultiKey" : false,
    "n" : 89,
    "nscannedObjects" : 89,
    "nscanned" : 588,
    "nscannedObjectsAllPlans" : 89,
    "nscannedAllPlans" : 588,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 1,
    "indexBounds" : {
        "c" : [
            [
                {
                    "$maxElement" : 1
                },
                {
                    "$minElement" : 1
                }
            ]
        ],
        "a" : [
            [
                100,
                10
            ]
        ],
        "b" : [
            [
                4,
                4
            ]
        ]
    },
    "server" :""
}

输出的解释是,索引被扫描,所以nscanned是588(扫描的索引条目和文档数),nscannedObjects中的数字是扫描的文档数量。因此,基于索引,MongoDB只会读取与条件匹配的文档(部分覆盖或者说是索引覆盖)。如您所见,scanAndOrder为false,因此没有排序阶段。(这意味着如果索引在内存中,速度会很快)。
除了文章中其他人提供的链接:http://blog.mongolab.com/wp-content/uploads/2012/06/IndexVisitation-4.png,你必须先将排序键放入索引中,然后再放查询键。如果它们有一个子集匹配,你必须按照排序标准的相同顺序包含子集(而对于查询部分则无关紧要)。

我认为无论你选择哪种方式,都会导致问题。你的索引为scanAndOrder提供了一个很好的值,而另一个索引提供了更少的最优运行,但是那个find查询无法正确地建立索引。我还意识到,排序的基数范围需要扫描整个索引(正如你所说),这可能会大大降低速度,特别是在必须从文档本身中选择a或b的情况下。 - Sammaye
哦,不好意思,我误读了解释输出,它并没有从磁盘中挑选出不需要的文档。 - Sammaye
它只从磁盘中读取符合条件的适当文档,但不利用索引结构。无论如何,它都会扫描整个索引。如果结果集很小,{a:1, b:1} 将会更快。 - attish
是的,我在第一部分误读了解释,扫描整个索引似乎违背了索引的初衷,就我个人而言,这似乎是 MongoDB 本身的一个弱点,希望可以通过交集操作来解决。 - Sammaye

0

我认为更改查找字段的顺序会更好。

db.mycollection.find({b:4, a:{$gt:10,$lt:100}}).sort({c:-1, a:-1})

然后您添加一个索引

{b:1,a:-1,c:-1}

1
在查找中更改字段的顺序对任何事情都没有影响,你为什么认为它会有影响呢?重要的是索引的顺序。 - Sammaye
@Sammaye - 我认为先搜索确切的值再搜索范围会更快,我错了吗? - Katja
1
这是关于索引顺序而不是查找顺序的问题,MongoDB查询优化器实际上会完全改变您的查找顺序以匹配索引顺序。 - Sammaye

0

我尝试了两个不同的索引,

一个是按照顺序索引的db.mycollection.ensureIndex({a:1,b:1,c:-1})

执行计划如下:

{
    "cursor" : "BtreeCursor a_1_b_1_c_-1",
    "nscanned" : 9542,
    "nscannedObjects" : 1,
    "n" : 1,
    "scanAndOrder" : true,
    "millis" : 36,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {
        "a" : [
            [
                3,
                10000
            ]
        ],
        "b" : [
            [
                4,
                4
            ]
        ],
        "c" : [
            [
                {
                    "$maxElement" : 1
                },
                {
                    "$minElement" : 1
                }
            ]
        ]
    }
}

还有其他索引,使用db.mycollection.ensureIndex({b:1,c:-1,a:-1})

> db.mycollection.find({a:{$gt:3,$lt:10000},b:4}).sort({c:-1, a:-1}).explain()
{
    "cursor" : "BtreeCursor b_1_c_-1_a_-1",
    "nscanned" : 1,
    "nscannedObjects" : 1,
    "n" : 1,
    "millis" : 8,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "isMultiKey" : false,
    "indexOnly" : false,
    "indexBounds" : {
        "b" : [
            [
                4,
                4
            ]
        ],
        "c" : [
            [
                {
                    "$maxElement" : 1
                },
                {
                    "$minElement" : 1
                }
            ]
        ],
        "a" : [
            [
                10000,
                3
            ]
        ]
    }
}
> 

我认为,由于您正在查询一系列值的'a'和一个特定值的'b',所以第二个选项更合适。 nscanned对象从9542更改为1


你没有排序,我相信你推荐的第二个索引会导致扫描和排序。 - Sammaye
我进行了排序,编辑了我的答案。 - Srivatsa N
你的第二个解释为什么完全没有显示scanandorder呢? - Sammaye

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