performance - 非常奇怪-添加复合索引会使查询变慢(MongoDB)

标签 performance mongodb indexing

我有一个应该很简单的问题,但是我对此感到困惑-也许我对MongoDB中的复合索引有误解。

为了重现此问题,我创建了一个简单的集合,其中包含500000个条目和六个字段,每个字段都有一个随机数。在mongo终端中,我生成了这样的集合:

for(i = 0; i < 500000; i++){
    db.test.save({a: Math.random(), b: Math.random(), c: Math.random(), d: Math.random(), e: Math.random() })
}


然后,我对这个集合进行一次简单的查询,如下所示:

t1 = new Date().getTime()
db.test.count({a : {$gt: 0.5}, b : {$gt: 0.5}, c : {$gt: 0.5}, d : {$gt: 0.5}, e : {$gt: 0.5}  }) 
t2 = new Date().getTime() 
t2-t1


=> 335ms

查询在335毫秒内完成。因此,现在我添加一个复合索引来尝试使查询更快:

db.test.ensureIndex({a: 1, b:1 ,c:1 ,d:1, e:1})


现在查询应该更快,但是运行完全相同的查询需要更长的时间:

t1 = new Date().getTime()
db.test.count({a : {$gt: 0.5}, b : {$gt: 0.5}, c : {$gt: 0.5}, d : {$gt: 0.5}, e : {$gt: 0.5}  }) 
t2 = new Date().getTime() 
t2-t1


=> 762ms

添加索引时,相同的查询将花费两倍的时间!即使我尝试多次也可以重复。使用db.test.dropIndexes()删除索引可使查询再次运行得更快,回到〜350ms。

使用explain()检查查询显示在添加索引之前使用了BasicCursor。添加索引后,将使用BtreeCursor并具有预期的indexBounds

所以我的问题是:为什么会这样?更重要的是,如何使该查询运行得更快?在我在同一台计算机上执行的SQL基准测试中,使用SQL进行的类似查询花费了〜240ms的时间而没有索引,而索引下降到了〜180ms。

我的MongoDB版本信息:

> mongo --version
MongoDB shell version: 2.6.3

最佳答案

您的示例的问题基本上是,在这种情况下,为了有效利用索引,数据实际上“太随机”了。结果是预料之中的,因为在索引中如何遍历索引没有多少“顺序”,并且考虑到在索引文档中的每个字段时,索引大小将比文档本身大一些。

为了更好地表示“现实世界”的情况,您可以查看相关数据的50/50比例来进行搜索。这里有一个更优化的生成器形式:

var samples = [{ "a": "a", "b": "a" },{ "a": "b", "b": "b" }];
for ( var x = 0; x < 5; x++ ) {
    samples.forEach(function(s) {
       var batch = [];
       for(i = 0; i < 10000; i++){
           batch.push( s );
       }
       db.test.insert(batch);
    });
}


这样就可以以足够公平的表示方式插入数据,以至于任何一种搜索本质上都必须确定地扫描集合中的每个文档以在没有索引的情况下全部检索它们。

因此,如果您现在查询带有表单的查询以获取50%的数据:

db.test.find({ "a": 1, "b": 1 }).explain()


在我所坐的硬件上,即使预热也要持续100毫秒以上才能完成。但是,当您向两个字段添加索引时:

db.test.ensureIndex({ "a": 1, "b": 1 })


然后,同一查询始终在100ms下且主要在90ms标记附近完成。当您添加一些投影以将统计信息强制为“仅索引”时,这也会变得更加有趣:

db.test.find({ "a": 1, "b": 1 },{ "_id", "a": 1, "b": 1 }).explain()


现在,尽管在这种情况下不需要返回文档并将其标记为"indexOnly": true,但是工作集的大小可能足够小以适合内存,因此由于“额外的工作”投影,您会发现性能略有下降。 “ 田野。现在,带有索引的平均值在硬件上大约为110ms。但是当您删除索引时:

db.test.dropIndexes()


不使用索引的查询性能下降为170ms。这更清楚地显示了针对指数收益的预测开销。

将索引拉回到原来的形式:

db.test.ensureIndex({ "a": 1, "b": 1, "c": 1, "d": 1, "e": 1 })


保持相同的投影查询,您可以绕过135ms使用索引,当然也可以使用相同的170ms而不使用索引。现在,如果您再返回到原始查询表单:

db.test.find({ "a": 1, "b": 1, "c": 1, "d":1, "e": 1}).explain()


带有索引的结果仍然在135ms标记附近,而未索引的查询正在跳过185ms标记。

因此,有意义的是,现实世界中的数据分发通常不像您设计的测试那样“随机”。尽管也确实分布几乎从来没有像50/50那样清晰,但是一般情况下实际上并没有那么多的分散,并且您所寻找的范围往往是自然的群集。

这也可以作为一个示例,即“真正随机”的数据在值之间具有较高的分布水平,那么b树索引并不是解决数据访问的最佳方法。

我希望这对您来说可以考虑一些要点。



这是另一个更接近原始测试的示例,唯一的区别是更改了“精度”,因此数据不是那么“随机”,这是我要提出的主要观点之一:

var batch = []
for( i = 0; i < 500000; i++){
    batch.push({
        "a": Math.round(Math.random()*100)/100,
        "b": Math.round(Math.random()*100)/100,
        "c": Math.round(Math.random()*100)/100,
        "d": Math.round(Math.random()*100)/100,
        "e": Math.round(Math.random()*100)/100
    });
    if ( batch.length % 10000 == 0 ) {
        db.test.insert( batch );
        batch = [];
    }
}


因此,在强制执行的数据中存在“两位小数精度”,这再次更直接地代表了现实世界中的数据情况。另请注意,插入操作并非每次迭代都完成,因为MongoDB 2.6中外壳程序的插入实现将在每次更新时返回“写关注”响应。设置起来如此之快。

如果您随后考虑使用原始测试查询,则根据我的硬件,没有索引的响应将花费590ms左右。当您添加相同的索引时,查询将在360ms中完成。

如果仅对“ a”和“ b”执行此操作而没有索引:

db.test.find({ "a": {"$gt": 0.5}, "b": {"$gt": 0.5} }).explain()


响应大约在490ms处出现。将索引仅添加到“ a”和“ b”

db.test.ensureIndex({ "a": 1, "b": 1 })


索引查询大约占用300ms,因此仍要快得多。

这里的一切基本上都说:


B树索引很好地支持自然分布,而不是完全随机。
仅在那些字段上索引您需要查询的内容。有大小成本,也有存储器成本。


从第二点开始,还有另外一件事需要说明,因为通常需要此处的大多数示例来从集合中查找文档并在索引中找到它。这里显而易见的代价是索引和集合都需要分页到内存中才能返回结果。这当然需要时间。

考虑使用以下查询的完整复合索引,没有索引的响应大约为485ms

db.test.find({ "a": {"$gt": 0.5}, "b": {"$gt": 0.5} }).explain()


在“ a”到“ e”上添加复合索引,使相同的查询在适当的位置围绕385ms运行。虽然比我们的完整查询要快,但速度慢,但是考虑到索引包含所有字段和条件。但是,如果您仅对必填字段进行了投影更改:

db.test.find(
    { "a": {"$gt": 0.5}, "b": {"$gt": 0.5} },
    { "_id": 0, "a": 1, "b": 1 }
).explain()


这会稍微减少时间,现在仅使用索引即可获得结果。删除索引并发出相同的查询会花费650ms,并增加了投影的开销。这表明有效的索引实际上确实会对结果产生很大的影响。

关于performance - 非常奇怪-添加复合索引会使查询变慢(MongoDB),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24399354/

相关文章:

C:连接字符串的最佳和最快方法是什么

java - Hibernate:遍历数百万行并且不泄漏内存

R:将包含大量数据的图表导出为 .png 的更快方法?

c++ - 如何优化从半精度 float16 到单精度 float32 的转换?

Mongodb:$exists 和 $ne 不能一起工作

node.js - 验证可能不存在的字段上的 $regex

node.js - 如何根据 MongoDB Mongoose 中填充的引用的类型对文档进行排序?

ruby - 如何在 Ruby 中找到字符串中字符的索引?

MySQL - 需要在 URL 表中搜索包含指定单词的 URL

MySQL没有使用复合索引的所有关键部分