【问题标题】:Very strange -- Adding a compound index makes queries much slower (MongoDB)很奇怪——添加复合索引会使查询变慢(MongoDB)
【发布时间】:2014-08-15 11:11:53
【问题描述】:

我遇到了一个应该非常简单的问题,但我被这个问题难住了——也许我对 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

=&gt; 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

=&gt; 762ms

添加索引时,相同的查询占用的时间是原来的两倍!即使我多次尝试,这也是可重复的。使用 db.test.dropIndexes() 删除索引会使查询再次运行得更快,回到 ~350 毫秒。

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

所以我的问题是:为什么会这样?更重要的是,我如何让这个查询运行得更快?在我在同一台机器上进行的 SQL 基准测试中,使用 SQL 进行的类似查询在没有索引的情况下花费了大约 240 毫秒,而索引将其降至大约 180 毫秒。

我的 MongoDB 版本信息:

> mongo --version
MongoDB shell version: 2.6.3

【问题讨论】:

    标签: performance mongodb indexing


    【解决方案1】:

    您的示例的问题基本上是数据确实“过于随机”,以便在这种情况下有效利用索引。结果与预期一致,因为索引如何遍历它并没有太多“顺序”,并且考虑到当您为文档中的每个字段编制索引时,索引大小将比文档本身大一些。

    为了更好地表示“现实世界”的情况,您可以查看更多 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-tree 索引并不是解决数据访问问题的最佳方式。

    我希望这能让您更清楚地了解一些需要考虑的要点。


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

    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 中 shell 的插入实现将在每次更新时返回“写关注”响应。设置起来要快得多。

    如果您随后考虑您的原始测试查询,根据我的硬件,没有索引的响应将需要大约 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-tree 索引很好地支持自然分布,但不支持完全随机。
    • 仅在这些字段上为您需要查询的内容编制索引。有大小成本,也有内存成本。

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

    考虑使用以下查询的完整复合索引,没有索引的响应大约需要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 以及投影的额外开销。这表明一个有效的指标实际上确实会对结果产生很大影响。

    【讨论】:

    • 感谢您的回复。这很有趣,但我仍然很困惑为什么索引无法处理随机数据。在 SQL 中,进行类似的实验(使用随机字段填充)按预期工作,索引产生显着的性能提升。您是说这是 MongoDB 索引对 SQL 的基本限制吗?至少,我觉得索引查询应该以与非索引查询相同的速度运行,但不会更慢。在我的示例中,索引使查询的执行速度慢了 2 倍以上,这看起来很荒谬。
    • 我的问题的另一部分“我如何让这个查询运行得更快”仍然存在。真的没有办法在 MongoDB 中提高对这个数据集的查询性能吗?
    • @user3773381 我认为您在这里忽略了重点。您对此的理解不仅不应该是 MongoDB 和其他东西之间的“竞赛”。一个显而易见的事实是,这种随机数据通常不存在于现实世界中。此处添加了更多信息供您考虑。我建议你这样做。在实际情况下,索引使事情变得更快。我还指出您的测试没有考虑到“精度”。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-24
    • 2012-08-17
    • 2017-01-18
    • 2017-03-17
    相关资源
    最近更新 更多