83

我们最近为我们的一个主要系列创造了超过 200 万的记录,现在我们开始为该系列的主要性能问题而受苦。

集合中的文档有大约 8 个字段,您可以使用 UI 过滤这些字段,并且结果应该按处理记录的时间戳字段排序。

我添加了几个带有过滤字段和时间戳的复合索引,例如:

db.events.ensureIndex({somefield: 1, timestamp:-1})

我还添加了几个索引,以便一次使用多个过滤器,以期获得更好的性能。但是一些过滤器仍然需要很长时间才能执行。

我已经确保使用解释查询确实使用了我创建的索引,但性能仍然不够好。

我想知道分片是否是现在要走的路..但我们很快就会开始在该集合中每天有大约 100 万条新记录..所以我不确定它是否会很好地扩展..

编辑:查询示例:

> db.audit.find({'userAgent.deviceType': 'MOBILE', 'user.userName': {$in: ['nickey@acme.com']}}).sort({timestamp: -1}).limit(25).explain()
{
        "cursor" : "BtreeCursor user.userName_1_timestamp_-1",
        "isMultiKey" : false,
        "n" : 0,
        "nscannedObjects" : 30060,
        "nscanned" : 30060,
        "nscannedObjectsAllPlans" : 120241,
        "nscannedAllPlans" : 120241,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 1,
        "nChunkSkips" : 0,
        "millis" : 26495,
        "indexBounds" : {
                "user.userName" : [
                        [
                                "nickey@acme.com",
                                "nickey@acme.com"
                        ]
                ],
                "timestamp" : [
                        [
                                {
                                        "$maxElement" : 1
                                },
                                {
                                        "$minElement" : 1
                                }
                        ]
                ]
        },
        "server" : "yarin:27017"
}

请注意 deviceType 在我的收藏中只有 2 个值。

4

3 回答 3

74

这是大海捞针。explain()对于那些表现不佳的查询,我们需要一些输出。不幸的是,即使这样也只能解决该特定查询的问题,所以这里有一个关于如何解决这个问题的策略:

  1. 确保不是因为 RAM 不足和分页过多
  2. 启用数据库探查器(使用db.setProfilingLevel(1, timeout)wheretimeout是查询或命令所用毫秒数的阈值,任何较慢的都将被记录)
  3. 检查慢查询db.system.profile并使用手动运行查询explain()
  4. 尝试识别explain()输出中的慢操作,例如scanAndOrderor largenscanned等。
  5. 关于查询的选择性以及是否可以使用索引来改进查询的原因。如果不是,请考虑禁止最终用户的过滤器设置,或者给他一个警告对话框,告诉他操作可能很慢。

一个关键问题是您显然允许您的用户随意组合过滤器。如果没有索引交叉,这将大大增加所需索引的数量。

此外,在每个可能的查询中盲目地抛出索引是一种非常糟糕的策略。构造查询并确保索引字段具有足够的选择性很重要。

假设您要查询具有status“活动”和其他一些条件的所有用户。但是在这 500 万用户中,有 300 万是活跃的,200 万是不活跃的,所以超过 500 万的条目只有两个不同的值。这样的索引通常没有帮助。最好先搜索其他条件,然后扫描结果。平均而言,当返回 100 个文档时,您必须扫描 167 个文档,这不会对性能造成太大影响。但这并不是那么简单。如果主要标准是用户的joined_at日期,并且用户随着时间的推移而停止使用的可能性很高,那么您可能最终不得不扫描数千份文档才能找到一百个匹配项。

所以优化很大程度上取决于数据(不仅是它的结构,还有数据本身)、它的内部相关性和你的查询模式

当数据对于 RAM 来说太大时,情况会变得更糟,因为那样的话,有一个索引是很棒的,但是扫描(甚至只是简单地返回)结果可能需要从磁盘随机获取大量数据,这需要很长时间。

控制这种情况的最佳方法是限制不同查询类型的数量,禁止对低选择性信息的查询,并尝试防止对旧数据的随机访问。

如果所有其他方法都失败了,并且如果您确实需要过滤器具有如此大的灵活性,那么考虑一个支持索引交集的单独搜索数据库可能是值得的,从那里获取 mongo id,然后使用$in. 但这也有其自身的危险。

- 编辑 -

您发布的说明是扫描低选择性字段问题的一个很好的例子。显然,“nickey@acme.com”有很多文件。现在,查找这些文档并按时间戳降序排序非常快,因为它由高选择性索引支持。不幸的是,由于只有两种设备类型,mongo 需要扫描 30060 个文档才能找到第一个匹配“移动”的文档。

我假设这是某种网络跟踪,并且用户的使用模式使查询变慢(他会每天切换移动和网络,查询会很快)。

可以使用包含设备类型的复合索引来加快此特定查询的速度,例如使用

a) ensureIndex({'username': 1, 'userAgent.deviceType' : 1, 'timestamp' :-1})

或者

b) ensureIndex({'userAgent.deviceType' : 1, 'username' : 1, 'timestamp' :-1})

不幸的是,这意味着像这样的查询find({"username" : "foo"}).sort({"timestamp" : -1}); 不能再使用相同的索引,因此,如前所述,索引的数量将增长得非常快。

恐怕目前使用 mongodb 没有很好的解决方案。

于 2013-10-24T08:53:17.370 回答
3

Mongo 每个查询只使用 1 个索引。因此,如果要过滤 2 个字段,mongo 将使用其中一个字段的索引,但仍需要扫描整个子集。

这意味着基本上您需要为每种类型的查询建立一个索引,以实现最佳性能。

根据您的数据,每个字段有一个查询并在您的应用程序中处理结果可能不是一个坏主意。这样,您只需要每个字段上的索引,但处理的数据可能太多。

于 2013-10-24T08:45:30.290 回答
-2

如果你使用 $in,mongodb 从不使用 INDEX。通过删除此 $in 来更改您的查询。它应该使用索引,它会提供比你之前得到的更好的性能。

http://docs.mongodb.org/manual/core/query-optimization/

于 2014-10-16T10:14:18.833 回答