MongoDB 查询超过500万条记录的性能

最近,我们的一个主要集合的记录超过了200万条,现在我们开始为该集合的主要性能问题而苦恼。

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

我已经添加了几个带有筛选字段和时间戳的复合索引 例如:

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

我还添加了两个索引,用于同时使用多个过滤器,以期获得更好的性能。但有些过滤器仍然需要很长的时间才能运行。

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

I was wondering if sharding is the way to go now.. but we will soon start to have about 1 million new records per day in that collection.. so I'm not sure if it will scale well..

编辑: 查询的例子:

> 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个值。

74635 次浏览

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

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

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

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

  1. 确保它不是因为内存不足和过多的分页
  2. 启用数据库分析器(使用 db.setProfilingLevel(1, timeout),其中 timeout是查询或命令所需的毫秒数的阈值,稍慢的内容都会被记录下来)
  3. 检查 db.system.profile中的慢速查询,并使用 explain()手动运行查询
  4. 尝试识别 explain()输出中较慢的操作,如 scanAndOrder或大 nscanned等。
  5. 查询选择性的原因以及是否可以使用索引 完全没有改进查询。如果没有,可以考虑不允许终端用户使用过滤器设置,或者给他一个警告对话框,告诉他操作可能会很慢。

一个关键问题是,您显然允许用户随意组合过滤器。如果没有索引交集,那么需要的索引数量将急剧增加。

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

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

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

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

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

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

编辑

您发布的解释是一个很好的例子,说明了扫描低选择性字段的问题。显然,有很多关于“ 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}); 不能再使用相同的索引了这样的查询,因此,如前所述,索引的数量将迅速增长。

恐怕现在还没有很好的解决方案。

如果使用 $in,则 mongodb 从不使用 INDEX。通过删除此 $in 更改查询。它应该使用 index,它将提供比之前更好的性能。

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