很奇怪 - 添加复合索引会使查询慢得多(MongoDB)

时间:2014-06-25 02:39:08

标签: 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

1 个答案:

答案 0 :(得分:0)

这里你的例子的问题基本上是数据确实远远太随机了#34;为了在这种情况下有效地使用索引。结果是预期的,因为没有多少&#34; order&#34;在索引如何遍历这一过程中,以及当您索引文档中的每个字段时,索引大小将比文档本身稍大一些。

为了更好地代表现实世界&#34;您可以查看更多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标记。当您添加一些投影以强制统计数据仅为#34;时,这也会变得更有趣:

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

现在虽然在这种情况下不需要返回文档并标记为"indexOnly": true,但工作集大小可能足够小以适应内存,因此您会看到由于额外的工作&#34;投射&#34;田野。现在索引的平均值在硬件上约为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标记处跳过。

因此,现实世界的数据分布通常不是那么有意义#34;随机&#34;作为你设计的测试。虽然分布几乎从来没有像50/50那样明确,但一般情况下实际上并没有那么多分散,并且往往存在您正在寻找的范围的自然聚类。

这也是&#34;真正随机&#34;的一个例子。在值之间具有高度分布的数据,则b树索引不是解决数据访问的最佳方式。

我希望你能更清楚地考虑一下这一点。


这是另一个更接近原始测试的样本,唯一的区别是改变了&#34;精度&#34;所以数据并非如此&#34;随机&#34;这是我正在提出的要点之一:

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 = [];
    }
}

所以有一个&#34;两位小数位精度&#34;在被强制执行的数据中,它更直接地再代表真实世界的数据案例。另请注意,插入操作不会在每次迭代时完成,因为MongoDB 2.6中shell的插入实现将返回&#34;写入问题&#34;每次更新都会回复。设置起来要快得多。

如果您考虑原始测试查询,那么没有索引的响应将花费590ms来完成我的硬件。当您添加相同的索引时,查询将在360ms

中完成

如果你只是在&#34; a&#34;和&#34; b&#34;没有索引:

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

响应来自490ms左右。仅为&#34; a&#34;添加索引和&#34; b&#34;

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

索引查询大约需要300ms,所以仍然要快得多。

这里所说的一切基本上都是:

  • B-tree索引支持自然分布,完全随机不是。
  • 索引您只需要在这些字段上查询的内容。这是一个尺寸成本,也有内存成本。

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

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

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

在&#34; a&#34;上添加复合索引通过&#34; e&#34;使用385ms附近的索引进行相同的查询。仍然更快,但比我们的完整查询慢,但有一个很好的理由为什么考虑索引包含所有字段和条件。但是如果你通过仅对所需字段进行投影来改变它:

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

这会稍微减少时间,现在使用索引来获取结果。删除索引并发出相同的查询需要大约650ms,并且需要额外的投影开销。这表明有效指数实际上确实对结果产生了很大的影响。