MongoDB 查询分析可以确保我们所建立的索引是否有效,是查询语句性能分析的重要工具。MongoDB 查询分析常用函数有 explain() 和 hint()。
为了更好的理解 explain() 函数,这里采用脚本的方式创建 100万用户数据,脚本如下:
> var chas = ['A','B','C','D','E','F','G','H','I','K','L','M','N','O'] > for(var i=0; i < 1000000; i++){ ... db.users.insertOne({ name: chas[Math.ceil(Math.random()*chas.length)] + "name", age:Math.ceil(Math.random()*100) }) ... } { "acknowledged" : true, "insertedId" : ObjectId("6503af99f470730f2bb44244") }
上面脚本将在 mongo 客户端中执行,执行后的结果集内容如下:
从上图可知,刚好 100 万用户数据,用户数据只包含了 name 和 age。
在 MongoDB 中,可以使用 explain() 方法来进行查询分析。explain() 方法可以返回查询的执行计划和相关统计信息,帮助我们了解查询的性能和优化情况。
explain() 方法使用的基本语法如下:
db.collection.find(query).explain()
其中,db.collection.find(query) 是要进行分析的查询语句。
explain() 方法返回的结果包含了以下重要的字段:
{ "explainVersion" : "1", "queryPlanner" : { // 查询规划器的信息,包括查询使用的索引、查询计划和查询优化器的相关信息 }, "command" : { // 执行的命令 }, "serverInfo" : { // MongoDB 服务器的信息,包括服务器版本、操作系统等 }, "serverParameters" : { // 服务参数信息 }, "ok" : 1 }
如果运行如下命令:
db.users.find().explain()
完整的输出如下:
{ "explainVersion" : "1", // 查询计划 "queryPlanner" : { // 在哪里执行查询,在 test 数据库的 users 集合中执行查询 "namespace" : "test.users", "indexFilterSet" : false, // 查询参数解析 "parsedQuery" : { }, "queryHash" : "8B3D4AB8", "planCacheKey" : "D542626C", "maxIndexedOrSolutionsReached" : false, "maxIndexedAndSolutionsReached" : false, "maxScansToExplodeReached" : false, // 最终胜出的查询计划,如果有多个匹配的索引时,mongodb 会为每个匹配 "winningPlan" : { // 扫描整个集合 "stage" : "COLLSCAN", "direction" : "forward" }, // 被拒绝的查询计划,即没有获胜的查询计划 "rejectedPlans" : [ ] }, // 执行的命令,在 test 数据库的 users 集合中执行 find 命令,没有过滤条件 "command" : { "find" : "users", "filter" : { }, "$db" : "test" }, // 服务信息,如:主机地址、端口、mongodb 版本信息 "serverInfo" : { "host" : "hxstrive", "port" : 27017, "version" : "5.0.20", "gitVersion" : "2cd626d8148120319d7dca5824e760fe220cb0de" }, // 服务参数信息 "serverParameters" : { "internalQueryFacetBufferSizeBytes" : 104857600, "internalQueryFacetMaxOutputDocSizeBytes" : 104857600, "internalLookupStageIntermediateDocumentMaxSizeBytes" : 104857600, "internalDocumentSourceGroupMaxMemoryBytes" : 104857600, "internalQueryMaxBlockingSortMemoryUsageBytes" : 104857600, "internalQueryProhibitBlockingMergeOnMongoS" : 0, "internalQueryMaxAddToSetBytes" : 104857600, "internalDocumentSourceSetWindowFieldsMaxMemoryBytes" : 104857600 }, "ok" : 1 }
(1)在 users 集合的 name 和 age 字段上面创建索引:
> db.users.createIndex({ name:1, age:-1 }) { "numIndexesBefore" : 1, "numIndexesAfter" : 2, "createdCollectionAutomatically" : false, "ok" : 1 } > db.users.createIndex({ age:-1, name:1 }) { "numIndexesBefore" : 2, "numIndexesAfter" : 3, "createdCollectionAutomatically" : false, "ok" : 1 } (2)在查询语句中使用 explain 查看查询计划: > db.users.find({ age:45, name:/D.+/ }).explain() { "explainVersion" : "1", "queryPlanner" : { // 查询 test 数据库 users 集合 "namespace" : "test.users", "indexFilterSet" : false, // 查询条件,查询 age 和 name 字段 "parsedQuery" : { "$and" : [ { "age" : { "$eq" : 45 } }, { "name" : { "$regex" : "D.+" } } ] }, "queryHash" : "4FA0B9FF", "planCacheKey" : "65BB8DE0", "maxIndexedOrSolutionsReached" : false, "maxIndexedAndSolutionsReached" : false, "maxScansToExplodeReached" : false, // 获胜的查询计划 "winningPlan" : { "stage" : "FETCH", // 该阶段的输出将传递给父阶段 FETCH,作为 FETCH 阶段的输入 "inputStage" : { // 使用索引 "stage" : "IXSCAN", "filter" : { "name" : { "$regex" : "D.+" } }, "keyPattern" : { "age" : -1, "name" : 1 }, // 使用名为 age_-1_name_1 的索引 "indexName" : "age_-1_name_1", "isMultiKey" : false, "multiKeyPaths" : { "age" : [ ], "name" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "age" : [ "[45.0, 45.0]" ], "name" : [ "["", {})", "[/D.+/, /D.+/]" ] } } }, // 被拒绝的查询计划 "rejectedPlans" : [ { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "filter" : { "name" : { "$regex" : "D.+" } }, "keyPattern" : { "name" : 1, "age" : -1 }, "indexName" : "name_1_age_-1", "isMultiKey" : false, "multiKeyPaths" : { "name" : [ ], "age" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "name" : [ "["", {})", "[/D.+/, /D.+/]" ], "age" : [ "[45.0, 45.0]" ] } } } ] }, // 查询命令 "command" : { "find" : "users", "filter" : { "age" : 45, "name" : /D.+/ }, "$db" : "test" }, // 服务器信息 "serverInfo" : { "host" : "hxstrive", "port" : 27017, "version" : "5.0.20", "gitVersion" : "2cd626d8148120319d7dca5824e760fe220cb0de" }, // 服务器参数信息 "serverParameters" : { "internalQueryFacetBufferSizeBytes" : 104857600, "internalQueryFacetMaxOutputDocSizeBytes" : 104857600, "internalLookupStageIntermediateDocumentMaxSizeBytes" : 104857600, "internalDocumentSourceGroupMaxMemoryBytes" : 104857600, "internalQueryMaxBlockingSortMemoryUsageBytes" : 104857600, "internalQueryProhibitBlockingMergeOnMongoS" : 0, "internalQueryMaxAddToSetBytes" : 104857600, "internalDocumentSourceSetWindowFieldsMaxMemoryBytes" : 104857600 }, "ok" : 1 }
plain 结果将查询计划以阶段树的形式呈现,每个阶段将其结果(文档或索引键)传递给父节点,叶节点访问集合或索引。中间节点操纵由子节点产生的文档或索引键。根节点是MongoDB 从中派生结果集的最后阶段。
MongoDB 常见的阶段描述如下:
COLLSCAN 集合扫描
IXSCAN 索引扫描
FETCH 检出文档
SHARD_MERGE 合并分片中结果
SHARDING_FILTER 分片中过滤掉孤立文档
LIMIT 使用limit 限制返回数
PROJECTION 使用 skip 进行跳过
IDHACK 针对_id进行查询
COUNT 利用db.coll.explain().count()之类进行count运算
COUNTSCAN count不使用Index进行count时的stage返回
COUNT_SCAN count使用了Index进行count时的stage返回
SUBPLA 未使用到索引的$or查询的stage返回
TEXT 使用全文索引进行查询时候的stage返回
PROJECTION 限定返回字段时候stage的返回
...
使用 explain("executionStats") 命令获取查询的执行统计信息,例如:
> db.users.find({ age:45, name:/D.+/ }).explain("executionStats") { "explainVersion" : "1", // 查询计划 "queryPlanner" : { "namespace" : "test.users", "indexFilterSet" : false, // 查询条件 "parsedQuery" : { "$and" : [ { "age" : { "$eq" : 45 } }, { "name" : { "$regex" : "D.+" } } ] }, "maxIndexedOrSolutionsReached" : false, "maxIndexedAndSolutionsReached" : false, "maxScansToExplodeReached" : false, // 胜出的查询计划 "winningPlan" : { // 根据索引返回的 ObjectId 查询具体文档 "stage" : "FETCH", "inputStage" : { // 使用索引 "stage" : "IXSCAN", "filter" : { "name" : { "$regex" : "D.+" } }, "keyPattern" : { "age" : -1, "name" : 1 }, // 使用名为 age_-1_name_1 的索引 "indexName" : "age_-1_name_1", "isMultiKey" : false, "multiKeyPaths" : { "age" : [ ], "name" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "age" : [ "[45.0, 45.0]" ], "name" : [ "["", {})", "[/D.+/, /D.+/]" ] } } }, // 被拒绝的查询计划 "rejectedPlans" : [ { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "filter" : { "name" : { "$regex" : "D.+" } }, "keyPattern" : { "name" : 1, "age" : -1 }, "indexName" : "name_1_age_-1", "isMultiKey" : false, "multiKeyPaths" : { "name" : [ ], "age" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "name" : [ "["", {})", "[/D.+/, /D.+/]" ], "age" : [ "[45.0, 45.0]" ] } } } ] }, // 执行状态 "executionStats" : { "executionSuccess" : true, // 本次查询返回的文档数量。 "nReturned" : 710, // 数据库执行本次查询所花费的毫秒数。这个数字越小越好。 "executionTimeMillis" : 34, // 如果使用了索引,那么这个数字就是查找过的索引条目数量。 // 如果本次查询是一次全表扫描,那么这个数字就表示检查过的文档数量。 "totalKeysExamined" : 10168, // MongoDB 按照索引指针在磁盘上查找实际文档的次数。 // 如果查询中包含的查询条件不是索引的一部分,或者请求的字段没有包含在索引中,MongoDB 就必须查找每个索引项所指向的文档。 "totalDocsExamined" : 710, "executionStages" : { "stage" : "FETCH", "nReturned" : 710, "executionTimeMillisEstimate" : 9, "works" : 10168, "advanced" : 710, "needTime" : 9457, // 为了让写请求顺利进行,本次查询所让步(暂停)的次数。如果有写操作在等待执行,那么查询将定期释放它们的锁以允许写操作执行。 // 在本次查询中,由于并没有写操作在等待,因此查询永远不会进行让步。 "needYield" : 0, "saveState" : 11, "restoreState" : 11, "isEOF" : 1, "docsExamined" : 710, "alreadyHasObj" : 0, "inputStage" : { // MongoDB 是否可以使用索引完成本次查询。如果不可以,那么会使用 "COLLSCAN" 表示必须执行集合扫描来完成查询。 // IXSCAN 表示使用了索引 "stage" : "IXSCAN", "filter" : { "name" : { "$regex" : "D.+" } }, // 返回的键数 "nReturned" : 710, // 索引执行时间 "executionTimeMillisEstimate" : 3, "works" : 10168, "advanced" : 710, "needTime" : 9457, "needYield" : 0, "saveState" : 11, "restoreState" : 11, "isEOF" : 1, "keyPattern" : { "age" : -1, "name" : 1 }, // 索引名称 "indexName" : "age_-1_name_1", "isMultiKey" : false, "multiKeyPaths" : { "age" : [ ], "name" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "age" : [ "[45.0, 45.0]" ], "name" : [ "["", {})", "[/D.+/, /D.+/]" ] }, "keysExamined" : 10168, "seeks" : 1, "dupsTested" : 0, "dupsDropped" : 0 } } }, "command" : { "find" : "users", "filter" : { "age" : 45, "name" : /D.+/ }, "$db" : "test" }, "serverInfo" : { "host" : "hxstrive", "port" : 27017, "version" : "5.0.20", "gitVersion" : "2cd626d8148120319d7dca5824e760fe220cb0de" }, "serverParameters" : { "internalQueryFacetBufferSizeBytes" : 104857600, "internalQueryFacetMaxOutputDocSizeBytes" : 104857600, "internalLookupStageIntermediateDocumentMaxSizeBytes" : 104857600, "internalDocumentSourceGroupMaxMemoryBytes" : 104857600, "internalQueryMaxBlockingSortMemoryUsageBytes" : 104857600, "internalQueryProhibitBlockingMergeOnMongoS" : 0, "internalQueryMaxAddToSetBytes" : 104857600, "internalDocumentSourceSetWindowFieldsMaxMemoryBytes" : 104857600 }, "ok" : 1 }
在 MongoDB 中,可以使用 hint() 方法来指定查询使用的索引。hint() 方法用于强制查询使用指定的索引,以便优化查询性能。
hint() 方法的基本语法如下:
db.collection.find(query).hint(index)
其中,db.collection.find(query) 是要进行查询的语句,index 是要使用的索引的名称或索引键。
假设我们有一个名为 col 的集合,其中包含了用户信息,我们可以使用 hint() 方法来指定查询使用的索引。例如:
# 通过索引名 > db.col.find({ age:{ $gt:25 } }).hint("age_-1") { "_id" : ObjectId("64e71af810366fa87109a134"), "name" : "何八", "age" : 42, "email" : "heba@outlook.com", "sex" : "male" } { "_id" : ObjectId("64e71af810366fa87109a133"), "name" : "顾七", "age" : 30, "email" : "guqi@qq.com guq@163.com", "sex" : "male" } { "_id" : ObjectId("64e71af810366fa87109a131"), "name" : "王五", "age" : 27, "email" : "wangwu@sina.com.cn", "sex" : "male" } # 通过指定字段方式使用索引 > db.col.find({ age:{ $gt:25 } }).hint({ age:-1 }) { "_id" : ObjectId("64e71af810366fa87109a134"), "name" : "何八", "age" : 42, "email" : "heba@outlook.com", "sex" : "male" } { "_id" : ObjectId("64e71af810366fa87109a133"), "name" : "顾七", "age" : 30, "email" : "guqi@qq.com guq@163.com", "sex" : "male" } { "_id" : ObjectId("64e71af810366fa87109a131"), "name" : "王五", "age" : 27, "email" : "wangwu@sina.com.cn", "sex" : "male" }
上述示例中,我们使用 hint() 方法指定查询使用 age 字段的索引。这样可以确保查询使用该索引进行优化,提高查询性能。
如果指定了一个不存在的索引,则查询操作会报错:
> db.col.find({ age:{ $gt:25 } }).hint({ age:1 }) Error: error: { "ok" : 0, "errmsg" : "error processing query: ns=test.colTree: age $gt 25.0 Sort: {} Proj: {} planner returned error :: caused by :: hint provided does not correspond to an existing index", "code" : 2, "codeName" : "BadValue" }
这是因为 col 集合中没有 age 字段为升序的索引(age_1),只有 age 降序索引(age_-1),如下:
> db.col.getIndexes() [ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" }, { "v" : 2, "key" : { "name" : 1 }, "name" : "name_1" }, { "v" : 2, "key" : { "age" : -1 }, "name" : "age_-1" } ]
如果索引是一个多字段索引,hint() 该如何指定呢?
> db.col.getIndexes() [ ... { "v" : 2, "key" : { "age" : -1, "name" : 1 }, "name" : "age_-1_name_1" } ] # 方式一 > db.col.find({ age:{ $gt:25 } }).hint("age_-1_name_1") { "_id" : ObjectId("64e71af810366fa87109a134"), "name" : "何八", "age" : 42, "email" : "heba@outlook.com", "sex" : "male" } { "_id" : ObjectId("64e71af810366fa87109a133"), "name" : "顾七", "age" : 30, "email" : "guqi@qq.com guq@163.com", "sex" : "male" } { "_id" : ObjectId("64e71af810366fa87109a131"), "name" : "王五", "age" : 27, "email" : "wangwu@sina.com.cn", "sex" : "male" } # 方式二 > db.col.find({ age:{ $gt:25 } }).hint({ age:-1, name:1 }) { "_id" : ObjectId("64e71af810366fa87109a134"), "name" : "何八", "age" : 42, "email" : "heba@outlook.com", "sex" : "male" } { "_id" : ObjectId("64e71af810366fa87109a133"), "name" : "顾七", "age" : 30, "email" : "guqi@qq.com guq@163.com", "sex" : "male" } { "_id" : ObjectId("64e71af810366fa87109a131"), "name" : "王五", "age" : 27, "email" : "wangwu@sina.com.cn", "sex" : "male" }
注意:
(1)使用 hint() 方法指定索引并不一定会提高查询性能。在大多数情况下,MongoDB 会自动选择合适的索引来执行查询。只有在特定情况下,当我们了解查询模式并且确定使用指定索引可以优化查询性能时,才应该使用 hint() 方法来指定索引。
(2)hint() 方法只对当前查询有效,不会影响其他查询。并且,使用 hint() 方法时需要确保指定的索引存在于集合中。