投影使查询变慢。

9

我在MongoDb中有超过60万条记录。 我的用户架构看起来像这样:

{
    "_id" : ObjectId,
    "password" : String,
    "email" : String,
    "location" : Object,
    "followers" : Array,
    "following" : Array,
    "dateCreated" : Number,
    "loginCount" : Number,
    "settings" : Object,
    "roles" : Array,
    "enabled" : Boolean,
    "name" : Object
}

以下是需要翻译的内容:

以下查询:

db.users.find(
     {},
     { 
         name:1, 
         settings:1,
         email:1,
         location:1
     }
).skip(656784).limit(10).explain()

将结果转化为这样:
{
    "cursor" : "BasicCursor",
    "isMultiKey" : false,
    "n" : 10,
    "nscannedObjects" : 656794,
    "nscanned" : 656794,
    "nscannedObjectsAllPlans" : 656794,
    "nscannedAllPlans" : 656794,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 5131,
    "nChunkSkips" : 0,
    "millis" : 1106,
    "server" : "shreyance:27017",
    "filterSet" : false
}

在去除投影操作后,同样的查询如下:

db.users.find().skip(656784).limit(10).explain()
{
    "cursor" : "BasicCursor",
    "isMultiKey" : false,
    "n" : 10,
    "nscannedObjects" : 656794,
    "nscanned" : 656794,
    "nscannedObjectsAllPlans" : 656794,
    "nscannedAllPlans" : 656794,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 5131,
    "nChunkSkips" : 0,
    "millis" : 209,
    "server" : "shreyance:27017",
    "filterSet" : false
}

据我所知,投影(Projection)总是能提高查询性能的。因此,我无法理解为什么MongoDB会表现出这样的行为。有人能解释一下吗?何时使用投影,何时不使用?MongoDB中投影的实际实现方式是什么?

1
你是否反复得到这些结果?第二个查询可能会更快,因为数据从第一个查询中被缓存(加载到内存)了。 - Messa
抱歉,算了。嗯,很可能有一些条款阻止它在逻辑上跳过 _id 索引直到应该返回的点。 - Sammaye
1
你做出了一个假设,认为第二个查询由于投影而更快。实际上,它可能之所以更快,是因为它是第二个运行的,并且数据已经在内存中了。 - Asya Kamsky
你正在使用哪个版本的MongoDB? - Stennie
@AsyaKamsky 他确实说道:"是的,我反复得到这些结果。" - Sammaye
显示剩余7条评论
2个回答

5
你是正确的,投影使得MongoDB 2.6.3中的这个跳过查询变慢了。这与2.6查询规划器的优化问题有关,该问题被追踪为SERVER-13946
截至2.6.3版本,2.6查询规划器在投影分析后添加了SKIP(和LIMIT)阶段,因此在此查询期间,投影被不必要地应用于被丢弃的结果。我在MongoDB 2.4.10中测试了一个类似的查询,nScannedObjects等于我的limit返回的结果数,而不是skip + limit
有几个因素影响了你的查询性能:
1) 您没有指定任何查询条件({}),因此此查询按natural order进行集合扫描,而不是使用索引。
2) 查询无法覆盖,因为没有投影。
3) 您的skip值非常大,为656,784。

查询计划肯定有改进的空间,但我不认为在正常使用中会出现如此巨大的跳过值。例如,如果这是一个每页显示50个结果的分页应用程序查询,则skip()值相当于第13,135页。


1
的确,这个跳过操作不可行,但它是一个很好的发现。希望很快可以修复,因为这会导致即使是小的跳过操作都要做比必要更多的额外工作。 - Sammaye

3
除非您的投影结果能够生成“仅索引”查询,这意味着在结果中“投影”的字段都是索引中唯一存在的字段,否则您总是会为查询引擎产生更多的工作量。
您需要考虑以下过程:
1.如何进行匹配?在文档还是索引上?找到适当的主要索引或其他索引。
2.给定索引,扫描并查找内容。
3.现在我需要返回什么?索引中是否包含了所有数据?如果没有,请回到集合中提取文档。
这是基本过程。因此,除非其中一个阶段以任何方式“优化”,否则当然事情就会“变慢”。
您需要把这看作是设计“服务器引擎”并理解需要进行的步骤。考虑到您的条件都未满足任何能够在指定的步骤上产生“最佳”的东西,您需要学会接受这一点。
您的“最佳”情况是,选择的索引中只有投影字段。但实际上,即使是这种情况,也有加载索引的开销。
因此,请明智地选择,并了解您所编写查询所需的限制和内存要求。这就是“优化”的全部内容。

2
我认为这并没有解释为什么MongoDB看起来像是针对可以使用_id索引计数的656784个文档进行投影。 - Sammaye
1
@NeilLunn 我理解你的观点。但我也同意Sammaye的看法,“为什么Mongo要对每个文档应用投影”,它应该只对返回的文档应用投影。 - Shreyance Jain
@ShreyanceJain 在这个场景中,“投影”实际上是什么需要更详细的解释,因为它不是“仅索引”,所以从索引中选择字段实际上需要理解聚合框架管道中的 $project 运算符和查询引擎处理。本质上,否则您正在要求“穿过”所有文档并且“重新整形”。这需要进一步解释吗? - Neil Lunn
抱歉,我实际上是指遍历计数,MongoDB 能够遍历 B 树到跳过的点。这就是你实际上错过的东西。 - Sammaye

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