MongoDB explain输出

TrumanWong
7/17/2024

MongoDB中,explain 可以为查询提供大量的信息。对于慢查询来说,它是最重要的诊断工具之一。通过查看一个查询的 explain 输出,可以了解查询都使用了哪些索引以及是如何使用的。对于任何查询,都可以在末尾添加一个 explain 调用(就像添加 sortlimit 一样,但是 explain 必须是最后一个调用)。

最常见的 explain 输出有两种类型:使用索引的查询和未使用索引的查询。特殊类型的索引可能会创建略有不同的查询计划,但是大多数字段应该是相似的。此外,分片返回的是多个 explain 的集合,因为查询会在多个服务器端上执行。

最基本的 explain 类型是不使用索引的查询。如果一个查询不使用索引,则是因为它使用了 COLLSCAN

首先创建一个1000000个文档的集合:

> for (i=0; i<1000000; i++) {
...     db.users.insertOne(
...         {
...             "i" : i,
...             "username" : "name"+i,
...             "age" : Math.floor(Math.random()*120),
...             "created" : new Date()
...         }
...     );
... }

当不加索引的时候,explain结果看起来会像下面这样:

> db.users.find({"age": 42}).explain('executionStats')
{
  explainVersion: '1',
  queryPlanner: {
    namespace: 'videos.users',
    indexFilterSet: false,
    parsedQuery: { age: { '$eq': 42 } },
    queryHash: '97A4421A',
    planCacheKey: '97A4421A',
    maxIndexedOrSolutionsReached: false,
    maxIndexedAndSolutionsReached: false,
    maxScansToExplodeReached: false,
    winningPlan: {
      stage: 'COLLSCAN',
      filter: { age: { '$eq': 42 } },
      direction: 'forward'
    },
    rejectedPlans: []
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 8328,
    executionTimeMillis: 388,
    totalKeysExamined: 0,
    totalDocsExamined: 1000000,
    executionStages: {
      stage: 'COLLSCAN',
      filter: { age: { '$eq': 42 } },
      nReturned: 8328,
      executionTimeMillisEstimate: 20,
      works: 1000001,
      advanced: 8328,
      needTime: 991672,
      needYield: 0,
      saveState: 1000,
      restoreState: 1000,
      isEOF: 1,
      direction: 'forward',
      docsExamined: 1000000
    }
  },
  command: { find: 'users', filter: { age: 42 }, '$db': 'videos' },
  serverInfo: {
    host: 'localhost.localdomain',
    port: 27117,
    version: '7.0.12',
    gitVersion: 'b6513ce0781db6818e24619e8a461eae90bc94fc'
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
    internalQueryFrameworkControl: 'trySbeRestricted'
  },
  ok: 1
}

接下来,我们创建2个复合索引:

> db.users.createIndex({'age': -1, 'username': 1}, {'name': 'idx_age_username'})
idx_age_username
> db.users.createIndex({'username': 1, 'age': -1}, {'name': 'idx_username_age'})
idx_username_age
复合索引是包含对多个字段的引用的索引。复合索引可以提高对索引中的字段或索引前缀中的字段的查询性能。为常用查询字段建立索引,可增加查询覆盖的机会,这意味着 MongoDB 可完全利用索引来满足查询需求,而无需检查文档。限制:在单个复合索引中最多可以指定 32 个字段。

在本例中:

接下来运行查询:

> db.users.find({"age": 42}).explain('executionStats')
{
  explainVersion: '1',
  queryPlanner: {
    namespace: 'videos.users',
    indexFilterSet: false,
    parsedQuery: { age: { '$eq': 42 } },
    queryHash: '97A4421A',
    planCacheKey: '5786CCC4',
    maxIndexedOrSolutionsReached: false,
    maxIndexedAndSolutionsReached: false,
    maxScansToExplodeReached: false,
    winningPlan: {
      stage: 'FETCH',
      inputStage: {
        stage: 'IXSCAN',
        keyPattern: { age: -1, username: 1 },
        indexName: 'idx_age_username',
        isMultiKey: false,
        multiKeyPaths: { age: [], username: [] },
        isUnique: false,
        isSparse: false,
        isPartial: false,
        indexVersion: 2,
        direction: 'forward',
        indexBounds: { age: [ '[42, 42]' ], username: [ '[MinKey, MaxKey]' ] }
      }
    },
    rejectedPlans: []
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 8328,
    executionTimeMillis: 18,
    totalKeysExamined: 8328,
    totalDocsExamined: 8328,
    executionStages: {
      stage: 'FETCH',
      nReturned: 8328,
      executionTimeMillisEstimate: 3,
      works: 8329,
      advanced: 8328,
      needTime: 0,
      needYield: 0,
      saveState: 8,
      restoreState: 8,
      isEOF: 1,
      docsExamined: 8328,
      alreadyHasObj: 0,
      inputStage: {
        stage: 'IXSCAN',
        nReturned: 8328,
        executionTimeMillisEstimate: 0,
        works: 8329,
        advanced: 8328,
        needTime: 0,
        needYield: 0,
        saveState: 8,
        restoreState: 8,
        isEOF: 1,
        keyPattern: { age: -1, username: 1 },
        indexName: 'idx_age_username',
        isMultiKey: false,
        multiKeyPaths: { age: [], username: [] },
        isUnique: false,
        isSparse: false,
        isPartial: false,
        indexVersion: 2,
        direction: 'forward',
        indexBounds: { age: [ '[42, 42]' ], username: [ '[MinKey, MaxKey]' ] },
        keysExamined: 8328,
        seeks: 1,
        dupsTested: 0,
        dupsDropped: 0
      }
    }
  },
  command: { find: 'users', filter: { age: 42 }, '$db': 'videos' },
  serverInfo: {
    host: 'localhost.localdomain',
    port: 27117,
    version: '7.0.12',
    gitVersion: 'b6513ce0781db6818e24619e8a461eae90bc94fc'
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
    internalQueryFrameworkControl: 'trySbeRestricted'
  },
  ok: 1
}

这个输出会首先告诉你使用了哪个索引:idx_age_username。接下来是实际返回了多少文档:nReturned。注意,这并不一定能反映出 MongoDB 在执行查询时做了多少工作(例如,它需要搜索多少索引和文档)。totalKeysExamined 描述了所扫描的索引项数量,totalDocsExamined 表示扫描了多少个文档。

executionTimeMillis 报告了查询的执行速度,即从服务器接收请求到发出响应的时间。可以看到,加索引后执行速度有了明显的提升。

接下来是对一些重要字段的详细介绍:

isMultiKey: false

本次查询是否使用了多键索引。

nReturned: 8328

本次查询返回的文档数量。

totalDocsExamined: 8328

MongoDB 按照索引指针在磁盘上查找实际文档的次数。如果查询中包含的查询条件不是索引的一部分,或者请求的字段没有包含在索引中,MongoDB 就必须查找每个索引项所指向的文档。

totalKeysExamined: 8328

如果使用了索引,那么这个数字就是查找过的索引条目数量。如果本次查询是一次全表扫描,那么这个数字就表示检查过的文档数量。

stage: 'IXSCAN'

MongoDB 是否可以使用索引完成本次查询。如果不可以,那么会使用COLLSCAN 表示必须执行集合扫描来完成查询。

在本例中,可以看出 MongoDB 使用索引找到了所有匹配的文档,因为totalKeysExaminedtotalDocsExamined 是一样的。不过,此查询需要返回匹配文档中的每个字段,而索引中只包含了 age 字段和 username 字段。

needYield: 0

为了让写请求顺利进行,本次查询所让步(暂停)的次数。如果有写操作在等待执行,那么查询将定期释放它们的锁以允许写操作执行。在本次查询中,由于并没有写操作在等待,因此查询永远不会进行让步。

executionTimeMillis: 18

数据库执行本次查询所花费的毫秒数。这个数字越小越好。

indexBounds: { age: [ '[42, 42]' ], username: [ '[MinKey, MaxKey]' ] }

这描述了索引是如何被使用的,并给出了索引的遍历范围。在本例中,由于查询中的第一个子句是精确匹配,因此索引只需要查找 42 这个值就可以了。第二个索引键是一个自由变量,因为查询没有对它进行任何限制。因此,数据库会在符合 age: 42的结果中查找用户名在负无穷("$minElement" : 1)和正无穷("$maxElement" :1)之间的数据。

如果发现 MongoDB 正在使用的索引与自己希望的不一致,则可以用 hint 强制其使用特定的索引。如果希望 MongoDB 在上面例子的查询中使用 {"username" : 1, "age" :1} 索引,则可以像下面这样做:

> db.users.find({"age" : 14, "username" : /.*/}).hint("idx_username_age")
注意:如果查询没有使用你希望其使用的索引,而你使用了 hint 强制进行更改,那么应该在部署之前对这个查询执行 explain。如果强制 MongoDB 在它不知道如何使用索引的查询上使用索引,则可能会导致查询效率比不使用索引时还要低。

查看更多字段说明请查看官方文档