这些SentryOne  共享提示和技巧,不仅可以解决SQL Server性能问题,还可以在它们到达生产环境之前阻止它们。

MongoDB-explain-method-optimization-tool-300x199.jpg这是由两部分组成的系列的第二部分。在,我们介绍了MongoDB支持的主要索引类型以及如何创建和使用它们。在第二篇文章中,我们将看到一些如何使用explain()方法来调查查询的示例。您需要优化MongoDB查询吗?您将看到如何使用explain()来查明查询如何使用索引。或者,也许,它不使用索引!

什么是解释()?

explain()是一种可以应用于简单查询或游标以调查查询执行计划的方法。执行计划是MongoDB解析查询的方式。查看explain()返回的所有信息,我们可以看到以下内容:

  • 扫描了多少文件

  • 返回了多少文件

  • 使用了哪个索引

  • 查询执行了多长时间

  • 评估了哪些替代执行计划

......以及其他有用的信息。

使用explain()的目的是找出如何改进查询,例如,通过创建缺失的索引或通过重写它来更正确地使用现有索引。如果您熟悉MySQL中的EXPLAIN命令,那么MongoDB的explain()方法的目标是完全相同的。

可解释的对象

您可以按照以下任何方法以下列方式将explain()方法应用于查询或游标:

MongoDB> db.mycollection.find()。explain()

但是,调查mongo shell中的查询的首选方法是首先创建可解释的对象。

我们可以像这样创建一个可解释的对象:

MongoDB> var myexp = db.mycollection.explain()

一旦创建了可解释对象,就可以对其运行任何类型的操作来调查查询或游标执行计划。例如:

MongoDB> myexp.find()
MongoDB> myexp.update()
MongoDB> myexp.remove()
MongoDB> myexp.aggregate()

餐厅测试数据库

要查看一些示例,我们需要一个包含一些数据的集合。

出于我们的目的,我们可以使用纽约餐馆数据库。您可以从以下URL下载::  

解压缩归档并将JSON文件导入MongoDB:

$ unzip retaurants.zip
$ mongoimport  --host  127家 .0.0.1 -d测试-c餐馆--file retaurants.json

这个系列有3772个文件:纽约市的所有餐厅。这是一份文件样本。

MongoDB>使用测试
切换到db测试
MongoDB> db.restaurants.find()。pretty()。limit(1)
{
“_id”:ObjectId(“5b71b3281979e24aa18c0121”),
“地址”:{
“建筑”:“1007”,
“coord”:[
-73 .856077,
40 .848447
]
“街”:“莫里斯公园大道”,
“zipcode”:“10462”
},
“自治市镇”:“布朗克斯”,
“美食”:“面包店”,
“成绩”:[
{
“date”:ISODate(“2014-03-03T00:00:00Z”),
“等级”:“A”,
“得分”:2
},
{
“date”:ISODate(“2013-09-11T00:00:00Z”),
“等级”:“A”,
“得分”:6
},
{
“date”:ISODate(“2013-01-24T00:00:00Z”),
“等级”:“A”,
“得分”:10
},
{
“日期”:ISODate(“2011-11-23T00:00:00Z”),
“等级”:“A”,
“得分”:9
},
{
“日期”:ISODate(“2011-03-10T00:00:00Z”),
“等级”:“B”,
“得分”:14
}
]
“名称”:“莫里斯公园烘焙店”,
“restaurant_id”:“30075445”
}

解释()冗长

explain()方法有三种冗长模式。

  • queryPlanner  - 这是默认模式。在此级别,explain提供有关获胜计划的信息,包括使用的索引或是否需要收集扫描(COLLSCAN)

  • executionStats  - 此模式包括queryPlanner提供的所有信息以及统计信息。统计信息包括详细信息,例如检查和返回的文档数,执行时间(以毫秒为单位)等。

  • allPlansExecution - 此模式包括executionStats提供的所有信息以及有关丢弃的执行计划的信息

我们将在以下示例中看到explain()输出。

例1

现在是时候使用餐厅系列来运行我们的第一个例子:找出曼哈顿区的所有餐厅。

让我们首先使用executionStats模式创建可解释的对象。

MongoDB> var exp = db.restaurants.explain(“executionStats”)

然后让我们调查查询。

MongoDB> exp.find({自治市镇:“曼哈顿” })
{
“queryPlanner”:{
“plannerVersion”:1,
“namespace”:“test.restaurants”,
“indexFilterSet”:false,
“parsedQuery”:{
“自治市镇”:{
“ $ eq ”:“曼哈顿”
}
},
“winnerPlan”:{
“舞台”:“COLLSCAN”,
“过滤器”:{
“自治市镇”:{
“ $ eq ”:“曼哈顿”
}
},
“方向”:“前进”
},
“被拒绝的人”:[]
},
“executionStats”:{
“executionSuccess”:是的,
“nReturned”:1883年,
“executionTimeMillis”:1,
“totalKeysExamined”:0,
“totalDocsExamined”:3772,
“executionStages”:{
“舞台”:“COLLSCAN”,
“过滤器”:{
“自治市镇”:{
“ $ eq ”:“曼哈顿”
}
},
“nReturned”:1883年,
“executionTimeMillisEstimate”:0,
“作品”:3774,
“先进的”:1883年,
“需要时间”:1890年,
“needYield”:0,
“saveState”:29,
“restoreState”:29,
“isEOF”:1,
“无效”:0,
“方向”:“前进”,
“docsExamined”:3772
}
},
“serverInfo”:{
“主持人”:“管理员-MBP”,
“港口”:27017,
“版本”:“3.6.4”,
“gitVersion”:“d0181a711f7e7f39e60b5aeb1dc7097bf6ae5856”
},
“好的”:1
}

在这里,我们可以看到explain()的输出。首先,我们可以清楚地区分“queryPlanner”和“executionStats”模式。我们不会描述每个值,因为有些值非常直观。让我们看看其中一些:

queryPlanner.winningPlan.stage =“COLLSCAN”

这提供了有关获胜计划的非常重要的信息:这意味着MongoDB需要进行收集扫描。查询未优化,因为必须读取所有文档。

queryPlanner.winningPlan.rejectedPlans = []

它是空的。没有被拒绝的计划。当需要使用COLLSCAN执行查询时,唯一的执行计划是获胜计划。除了_id上的索引之外,我们在集合中没有任何索引,因此没有其他执行计划。

executionStats.nReturned = 1883

返回的文件数量是1883,即位于曼哈顿的餐馆数量。

executionStats.totalDocsExamined = 3772

检查的文档数正是集合中的文档数。这是预期的,因为查询使用COLLSCAN

executionStats.executionTimeMillis = 1

查询的执行时间。它只有1毫秒。这似乎很好,但请记住,这是扫描3772个文档所需的时间,这是一个非常小的测试集合。想想这个拥有数百万份文档的集合的时间是什么!

我们如何改进查询?

在这种情况下,它很简单。让我们尝试在自治市镇创建一个单一的字段索引这是我们在find()中唯一的条件。然后让我们再次尝试解释相同的查询。

MongoDB> db.restaurants.createIndex({borough:1 })
{
“createdCollectionAutomatically”:false,
“numIndexesBefore”:1,
“numIndexesAfter”:2,
“好的”:1
}
MongoDB> exp.find({自治市镇:“曼哈顿” })
{
“queryPlanner”:{
“plannerVersion”:1,
“namespace”:“test.restaurants”,
“indexFilterSet”:false,
“parsedQuery”:{
“自治市镇”:{
“ $ eq ”:“曼哈顿”
}
},
“winnerPlan”:{
“阶段”:“FETCH”,
“inputStage”:{
“舞台”:“IXSCAN”,
“keyPattern”:{
“自治市镇”:1
},
“indexName”:“borough_1”,
“isMultiKey”:false,
“multiKeyPaths”:{
“自治市”:[]
},
“isUnique”:false,
“isSparse”:false,
“isPartial”:false,
“indexVersion”:2,
“方向”:“前进”,
“indexBounds”:{
“自治市镇”:[
“[\”曼哈顿\“,”曼哈顿\“]”
]
}
}
},
“被拒绝的人”:[]
},
“executionStats”:{
“executionSuccess”:是的,
“nReturned”:1883年,
“executionTimeMillis”:1,
“totalKeysExamined”:1883年,
“totalDocsExamined”:1883年,
“executionStages”:{
“阶段”:“FETCH”,
“nReturned”:1883年,
“executionTimeMillisEstimate”:0,
“作品”:1884年,
“先进的”:1883年,
“needTime”:0,
“needYield”:0,
“saveState”:14,
“restoreState”:14,
“isEOF”:1,
“无效”:0,
“docsExamined”:1883年,
“alreadyHasObj”:0,
“inputStage”:{
“舞台”:“IXSCAN”,
“nReturned”:1883年,
“executionTimeMillisEstimate”:0,
“作品”:1884年,
“先进的”:1883年,
“needTime”:0,
“needYield”:0,
“saveState”:14,
“restoreState”:14,
“isEOF”:1,
“无效”:0,
“keyPattern”:{
“自治市镇”:1
},
“indexName”:“borough_1”,
“isMultiKey”:false,
“multiKeyPaths”:{
“自治市”:[]
},
“isUnique”:false,
“isSparse”:false,
“isPartial”:false,
“indexVersion”:2,
“方向”:“前进”,
“indexBounds”:{
“自治市镇”:[
“[\”曼哈顿\“,”曼哈顿\“]”
]
},
“keysExamined”:1883年,
“寻求”:1,
“dupsTested”:0,
“dupsDropped”:0,
“seenInvalidated”:0
}
}
},
“serverInfo”:{
“主持人”:“管理员-MBP”,
“港口”:27017,
“版本”:“3.6.4”,
“gitVersion”:“d0181a711f7e7f39e60b5aeb1dc7097bf6ae5856”
},
“好的”:1
}

解释输出

现在,输出完全不同。我们来看看一些最相关的值:

queryPlanner.winningPlan.inputStage.stage =“IXSCAN”

这非常重要。IXSCAN意味着现在MongoDB不需要进行集合扫描,但可以使用索引来查找文档。

queryPlanner.winningPlan.inputStage.indexName =“borough_1”

使用的索引的名称。这是索引的默认名称:字段的名称加上_1表示升序,_- 1表示降序。

queryPlanner.winningPlan.inputStage.direction =“forward”

MongoDB以正向遍历索引。

executionStats.nRertuned = 1883

返回的文档数。显然,这和以前一样。

executionStats.totalKeysExamined = 1883

索引中检查的键数。

executionStats.totalDocsExamined = 1883

现在,检查的文档数对应于索引中检查的元素数。

我们优化了查询。

例2

现在我们想查看一个查询,找出所有获得评分大于50的意大利菜的餐厅。

MongoDB> var exp = db.restaurants.explain()
MongoDB> exp.find({ $ and:[{ “cuisine”:{ $ eq:“Italian” }},{ “grades.score”:{ $ gt:50 }}]})
{
“queryPlanner”:{
“plannerVersion”:1,
“namespace”:“test.restaurants”,
“indexFilterSet”:false,
“parsedQuery”:{
“ $和”:[
{
“美食”:{
“ $ eq ”:“意大利语”
}
},
{
“grades.score”:{
“ $ gt ”:50
}
}
]
},
“winnerPlan”:{
“舞台”:“COLLSCAN”,
“过滤器”:{
“ $和”:[
{
“美食”:{
“ $ eq ”:“意大利语”
}
},
{
“grades.score”:{
“ $ gt ”:50
}
}
]
},
“方向”:“前进”
},
“被拒绝的人”:[]
},
“serverInfo”:{
“主持人”:“管理员-MBP”,
“港口”:27017,
“版本”:“3.6.4”,
“gitVersion”:“d0181a711f7e7f39e60b5aeb1dc7097bf6ae5856”
},
“好的”:1
}

我们又有了一个COLLSCAN。让我们尝试改进查询,在菜肴领域创建一个索引。

MongoDB> var exp = db.restaurants.explain(“executionStats”)
MongoDB> db.restaurants.createIndex({cuisine:1})
{
“createdCollectionAutomatically”:false,
“numIndexesBefore”:2,
“numIndexesAfter”:3,
“好的”:1
}
MongoDB> exp.find({ $ and:[{ “cuisine”:{ $ eq:“Italian” }},{ “grades.score”:{ $ gt:50 }}]})
{
“queryPlanner”:{
“plannerVersion”:1,
“namespace”:“test.restaurants”,
“indexFilterSet”:false,
“parsedQuery”:{
“ $和”:[
{
“美食”:{
“ $ eq ”:“意大利语”
}
},
{
“grades.score”:{
“ $ gt ”:50
}
}
]
},
“winnerPlan”:{
“阶段”:“FETCH”,
“过滤器”:{
“grades.score”:{
“ $ gt ”:50
}
},
“inputStage”:{
“舞台”:“IXSCAN”,
“keyPattern”:{
“美食”:1
},
“indexName”:“cuisine_1”,
“isMultiKey”:false,
“multiKeyPaths”:{
“美食”:[]
},
“isUnique”:false,
“isSparse”:false,
“isPartial”:false,
“indexVersion”:2,
“方向”:“前进”,
“indexBounds”:{
“美食”:[
“[\”意大利语“,”意大利语“”“
]
}
}
},
“被拒绝的人”:[]
},
“executionStats”:{
“executionSuccess”:是的,
“nReturned”:6,
“executionTimeMillis”:4,
“totalKeysExamined”:325,
“totalDocsExamined”:325,
“executionStages”:{
“阶段”:“FETCH”,
“过滤器”:{
“grades.score”:{
“ $ gt ”:50
}
},
“nReturned”:6,
“executionTimeMillisEstimate”:0,
“作品”:326,
“先进”:6,
“needTime”:319,
“needYield”:0,
“saveState”:2,
“restoreState”:2,
“isEOF”:1,
“无效”:0,
“docsExamined”:325,
“alreadyHasObj”:0,
“inputStage”:{
“舞台”:“IXSCAN”,
“nReturned”:325,
“executionTimeMillisEstimate”:0,
“作品”:326,
“先进的”:325,
“needTime”:0,
“needYield”:0,
“saveState”:2,
“restoreState”:2,
“isEOF”:1,
“无效”:0,
“keyPattern”:{
“美食”:1
},
“indexName”:“cuisine_1”,
“isMultiKey”:false,
“multiKeyPaths”:{
“美食”:[]
},
“isUnique”:false,
“isSparse”:false,
“isPartial”:false,
“indexVersion”:2,
“方向”:“前进”,
“indexBounds”:{
“美食”:[
“[\”意大利语“,”意大利语“”“
]
},
“keysExamined”:325,
“寻求”:1,
“dupsTested”:0,
“dupsDropped”:0,
“seenInvalidated”:0
}
}
},
“serverInfo”:{
“主持人”:“管理员-MBP”,
“港口”:27017,
“版本”:“3.6.4”,
“gitVersion”:“d0181a711f7e7f39e60b5aeb1dc7097bf6ae5856”
},
“好的”:1
}

查询已得到改进。使用创建的索引cuisine_1,但仍然,我们检查了325个文档,只返回了6个文档。让我们看看我们是否可以通过创建一个使用条件中的字段的复合索引来做得更好cuisinegrades.score

现在,获胜计划使用新的复合指数cuisine_1_grades.score_1,我们只检查了6个文件。请注意,现在我们有一个被拒绝的计划,即使用之前创建的单字段索引cuisine_1的计划。

我们优化了查询。

例3

让我们找出所有在布鲁克林没有准备任何“美国”美食并达到“A”等级的餐厅。我们希望看到按菜肴排序的结果。

此时,您应该熟悉explain()输出,因此在下一个框中,为简单起见,我们将其截断,只留下相关部分。

MongoDB> var exp.restaurants.explain(“executionStats”)
MongoDB> exp.find({ “cuisine”:{ $ ne:“American” },
...... “grade.grade”:“A”,
...... “自治市镇”:“布鲁克林” })。排序({ “cuisine”:-1})
{
“queryPlanner”:{
...
...
“winnerPlan”:{
“阶段”:“FETCH”,
“过滤器”:{
“ $和”:[
{
“自治市镇”:{
“ $ eq ”:“布鲁克林”
}
},
{
“grades.grade”:{
“ $ eq ”:“A”
}
}
]
},
“inputStage”:{
“舞台”:“IXSCAN”,
“keyPattern”:{
“美食”:1
},
“indexName”:“cuisine_1”,
“isMultiKey”:false,
“multiKeyPaths”:{
“美食”:[]
},
“isUnique”:false,
“isSparse”:false,
“isPartial”:false,
“indexVersion”:2,
“方向”:“落后”,
“indexBounds”:{
“美食”:[
“[MaxKey,\”American \“)”,
“(”美国人“,”MinKey“)
]
}
}
},
...
...
“executionStats”:{
“executionSuccess”:是的,
“nReturned”:493,
“executionTimeMillis”:9,
“totalKeysExamined”:2518,
“totalDocsExamined”:2517,
...
...
“serverInfo”:{
“主持人”:“管理员-MBP”,
“港口”:27017,
“版本”:“3.6.4”,
“gitVersion”:“d0181a711f7e7f39e60b5aeb1dc7097bf6ae5856”
},
“好的”:1
}

看看获胜计划,我们看到我们在index_1上有IXSCAN。这里需要注意的重要一点是选择菜肴领域的指数 - 这是因为我们使用了sort({cuisine:-1})。没有SORT阶段,因为文档已经使用索引提取,因此它们已经排序。请注意方向:“向后” - 这是因为我们在查询中指定了降序。如果我们尝试执行稍微不同的查询,将排序更改为name:1而不是cuisine:-1,我们将看到完全不同的获胜计划。 

MongoDB> exp.find({ “cuisine”:{ $ ne:“American” },
...... “grade.grade”:“A”,
...... “自治市镇”:“布鲁克林” })。排序({ “名称”:1})
...
“winnerPlan”:{
“舞台”:“SORT”,
“sortPattern”:{
“名字”:-1
},
“inputStage”:{
“stage”:“SORT_KEY_GENERATOR”,
“inputStage”:{
“阶段”:“FETCH”,
“过滤器”:{
“ $和”:[
{
“grades.grade”:{
“ $ eq ”:“A”
}
},
{
“ $ nor ”:[
{
“美食”:{
“ $ eq ”:“美国人”
}
}
]
}
]
},
“inputStage”:{
“舞台”:“IXSCAN”,
“keyPattern”:{
“自治市镇”:1
},
“indexName”:“borough_1”,
“isMultiKey”:false,
“multiKeyPaths”:{
“自治市”:[]
},
“isUnique”:false,
“isSparse”:false,
“isPartial”:false,
“indexVersion”:2,
“方向”:“前进”,
“indexBounds”:{
“自治市镇”:[
“[\”布鲁克林\“,”布鲁克林\“]”
]
}
}
}
}
},
...
...
“executionStats”:{
“executionSuccess”:是的,
“nReturned”:493,
“executionTimeMillis”:13,
“totalKeysExamined”:684,
“totalDocsExamined”:684,
...
...

在这种情况下,我们检查的文档较少,但由于无法使用cuisine_1索引,因此需要SORT阶段,并且用于获取文档的索引是borough_1。虽然MongoDB检查的文档较少,但由于用于对文档进行排序的额外阶段,执行时间更长。

我们现在回到原始查询。我们还可以注意到,与返回的文件(493)相比,检查的文件数量仍然太高(2517)。那不是最佳选择。让我们看看我们是否可以通过添加另一个复合索引来进一步改进查询(cuisine,borough,grades.grade)。

MongoDB> db.restaurants.createIndex({cuisine:1,borough:1,“grades.grade”:1})
{
“createdCollectionAutomatically”:false,
“numIndexesBefore”:4,
“numIndexesAfter”:5,
“好的”:1
}
MongoDB> exp.find({ “cuisine”:{ $ ne:“American” },
...... “grade.grade”:“A”,
...... “自治市镇”:“布鲁克林” })。排序({ “cuisine”:-1})
...
...... “winnerPlan”:{
“阶段”:“FETCH”,
“过滤器”:{
“ $ nor ”:[
{
“美食”:{
“ $ eq ”:“美国人”
}
}
]
},
“inputStage”:{
“舞台”:“IXSCAN”,
“keyPattern”:{
“美食”:1,
“自治市镇”:1,
“grade.grade”:1
},
“indexName”:“cuisine_1_borough_1_grades.grade_1”,
“isMultiKey”:是的,
“multiKeyPaths”:{
“美食”:[],
“自治市”:[],
“grade.grade”:[
“等级”
]
},
“isUnique”:false,
“isSparse”:false,
“isPartial”:false,
“indexVersion”:2,
“方向”:“落后”,
“indexBounds”:{
“美食”:[
“[MaxKey,\”American \“)”,
“(”美国人“,”MinKey“)
]
“自治市镇”:[
“[\”布鲁克林\“,”布鲁克林\“]”
]
“grade.grade”:[
“[\”A \“,\”A \“]”
]
}
}
},
...
...
“executionStats”:{
“executionSuccess”:是的,
“nReturned”:493,
“executionTimeMillis”:6,
“totalKeysExamined”:591,
“totalDocsExamined”:493,
...
...

现在,MongoDB使用新索引,不需要额外的排序阶段。检查的文件数与退回的文件数相同。此外,执行时间更好。

我们优化了查询。

例4

这是最后一个例子。让我们找出一些餐厅,其中的成绩数组包含等级 “A”,特定日期等级为9 。

MongoDB> exp.find({ “grades.date”:ISODate(“2014-08-11T00:00:00Z”),
...... “grade.grade”:“A”,
...... “grade.score”:9 })
{
“queryPlanner”:{
...
...
“winnerPlan”:{
“舞台”:“COLLSCAN”,
“过滤器”:{
“ $和”:[
{
“grades.date”:{
“ $ eq ”:ISODate(“2014-08-11T00:00:00Z”)
}
},
{
“grades.grade”:{
“ $ eq ”:“A”
}
},
{
“grades.score”:{
“ $ eq ”:9
}
}
]
},
“方向”:“前进”
},
...

一个COLLSCAN了。在查询中,我们注意到所有条件都引用了数组对象中的嵌入字段。所以让我们尝试创建一个多键索引。我们只是在日期字段上创建索引,看看会发生什么。

MongoDB> db.restaurants.createIndex({ “grades.date”:1})
{
“createdCollectionAutomatically”:false,
“numIndexesBefore”:5,
“numIndexesAfter”:6,
“好的”:1
}
MongoDB> exp.find({ “grades.date”:ISODate(“2014-08-11T00:00:00Z”),
...... “grade.grade”:“A”,
...... “grade.score”:9 })
{
“queryPlanner”:{
...
...
“winnerPlan”:{
“阶段”:“FETCH”,
“过滤器”:{
“ $和”:[
{
“grades.grade”:{
“ $ eq ”:“A”
}
},
{
“grades.score”:{
“ $ eq ”:9
}
}
]
},
“inputStage”:{
“舞台”:“IXSCAN”,
“keyPattern”:{
“grades.date”:1
},
“indexName”:“grades.date_1”,
“isMultiKey”:是的,
“multiKeyPaths”:{
“grades.date”:[
“等级”
]
},
“isUnique”:false,
“isSparse”:false,
“isPartial”:false,
“indexVersion”:2,
“方向”:“前进”,
“indexBounds”:{
“grades.date”:[
“[新日期(1407715200000),新日期(1407715200000)]”
]
}
}
},
“被拒绝的人”:[]
},
“executionStats”:{
“executionSuccess”:是的,
“nReturned”:15,
“executionTimeMillis”:0,
“totalKeysExamined”:22,
“totalDocsExamined”:22,
...
...

MongoDB使用索引,获胜计划已经足够好了。您可以尝试作为练习来创建包含数组的其他嵌入字段的复合索引,例如{“grades.date”:1,“grades.grade”:1,“grades.score”:1}并查看会发生什么。您可能会看到我们仅在日期创建的索引足够好。扩大复合索引将仅生成被拒绝的计划。这是因为日期字段是最具选择性的。

提示:处理复合索引时,请记住字段的顺序很重要。第一个字段应该是最具选择性的字段,最后一个字段应该是选择性较低的字段。或者,如果您不需要在索引中放入大量字段,那么最具选择性的字段可能就是您需要改进查询的所有字段。

结论

这就是所有人。在这个由两部分组成的系列文章中,我们看到了MongoDB中可用的索引以及如何使用explain()来调查查询并了解如何提高其性能。我们不希望您现在知道所有内容,但我们希望这将是您使用explain()练习MongoDB查询优化的良好起点。如果您有动力了解更多信息,您可以在手册和互联网上找到有关索引和解释()的更多信息。

感谢阅读,如果您有任何想法或问题,请随时在评论中写下来!