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

86

我们最近的一个主要集合已经达到了200万条记录,现在我们开始在该集合上遇到严重的性能问题。

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

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

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

我还添加了一些索引,以便同时使用多个过滤器来实现更好的性能。但有些过滤器仍然需要很长时间才能执行。

我确保使用explain命令查询时,可以看到我创建的索引被使用,但性能仍然不够好。

我在考虑是否采用分片的方式,但是我们将很快开始每天在该集合中新增约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 个取值。


你正在使用 limit 参数吗? - Joe
Joe,当然,我正在使用限制,目前我将结果限制在25个文档以内。我甚至不想谈论跳过,因为我将在不久的将来用范围查询替换它们。 - Yarin Miran
2
Enver,当集合中有大约1-2百万条记录时,我开始感觉到一些性能问题(5-50秒的查询时间)。然后我添加了索引,并且对于小于1000ms的查询获得了合理的性能。现在查询需要20毫秒到60秒不等,但这完全取决于被过滤的字段的值分布以及索引实际上有多少“帮助”。 - Yarin Miran
哪些查询很慢?一个没有过滤条件的简单查询已经很慢了吗?还是只有通过一个字段进行过滤的查询才慢?或者是通过两个字段进行过滤的查询才慢? - Joe
你能贴出一条慢查询的解释吗?此外,你的工作集是否适合放入内存中?这是在集合温热还是冷启动时发生的? - Sammaye
显示剩余4条评论
3个回答

78
这就像在大海捞针。对于那些性能不佳的查询,我们需要一些explain()输出的结果。不幸的是,即使如此,这只会为特定的查询解决问题,因此以下是一种解决方法:
  1. 确保不是由于RAM不足和过多的分页导致的
  2. 启用DB分析器(使用 db.setProfilingLevel(1, timeout) 其中timeout 是查询或命令所需的毫秒数的阈值,任何慢于此的内容都将被记录)
  3. 检查db.system.profile中的慢查询,并手动运行这些查询,使用explain()
  4. 尝试在explain()输出中识别出慢操作,例如scanAndOrder或大型的nscanned
  5. 考虑查询的选择性以及是否可能通过索引完全改进查询。如果不行,请考虑禁止终端用户的过滤设置,或向其提供警告对话框,提示操作可能很慢。

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

而且,盲目地将索引应用于每个可能的查询是一种非常糟糕的策略。重要的是构造查询并确保索引字段具有足够的选择性

假设您有一个查询,用于查找所有状态为“active”的用户以及其他标准。但在500万个用户中,300万个处于活动状态,而200万个不是,因此在5百万条记录中只有两个不同的值。这样的索引通常不会有帮助。最好先搜索其他标准,然后扫描结果。平均而言,在返回100个文档时,您将不得不扫描167个文档,这不会对性能产生太大影响。但事情并不那么简单。如果主要标准是用户的joined_at日期,而用户随着时间的推移很可能停止使用,则在找到一百个匹配项之前,您可能需要扫描成上万个文档。

因此,优化非常依赖数据(不仅是其结构,还包括数据本身),其内部相关性和您的查询模式

当数据太大无法在RAM中存放时,情况会变得更糟,因为这时拥有索引非常重要,但扫描(甚至仅仅是返回)结果可能需要从磁盘随机获取大量数据,这需要很长时间。

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

如果所有其他办法都失败了,并且您确实需要这么多筛选器上的灵活性,那么值得考虑一个支持索引交集的单独搜索数据库,从那里获取mongo id,然后使用$in从mongo获取结果。但这本身就存在危险。

-- 编辑--

您发布的解释(explain)是关于扫描选择性低

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中,我恐怕没有非常好的解决方案。


感谢您的回复!我们面临的另一个问题是MongoDB上有多个客户端数据库,每个数据库都有巨大的集合。我们担心为所有这些集合建立索引将会严重影响性能,因为我们需要大量的 RAM 来支持不同用户之间的并行查询。您是否有任何好的搜索数据库建议? - Yarin Miran
我猜那取决于您需要的搜索功能。对于基础知识,任何支持索引交集的数据库都可以做到。如果您需要全文搜索、多维分类搜索甚至是切片和骰子,事情就变得棘手了,这里有一个完整的工具宇宙,从SolR、Elastic Search到OLAP立方体。在此过程中,您还可以投票支持MongoDB Jira中的索引交集:https://jira.mongodb.org/browse/SERVER-3071 - mnemosyn
我认为我们应该选择ElasticSearch来处理这个特定的表格。你对此有什么看法? - Yarin Miran
2
很棒的回答。我很想知道在这方面过去4.5年发生了什么变化。 - Daniel Hilgarth
我很想知道在这方面过去8年发生了什么变化。 - Stunner

3
Mongo每次查询只使用一个索引。 因此,如果您想要对两个字段进行过滤,Mongo将使用其中一个字段的索引,但仍需要扫描整个子集。
这意味着基本上你需要为每种类型的查询创建一个索引才能实现最佳性能。
根据您的数据情况,每个字段可能需要一个查询,并在应用程序中处理结果。这样,您只需要在每个字段上建立索引,但处理的数据量可能太大。

-2

19
FYI,$in使用索引,而$nin不使用索引。根据我们的经验,$in的问题在于Mongo针对$in中的每个值执行一次查询。尽管对每个查询使用了索引,但速度非常慢。 - Yarin Miran

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接