MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MongoDB explain输出解读:优化查询性能

2023-12-082.6k 阅读

MongoDB explain 概述

在 MongoDB 数据库中,explain 是一个极为强大的工具,它用于帮助开发者深入了解查询执行计划。通过 explain,我们能够知晓 MongoDB 如何执行特定查询,包括选择的索引、扫描的文档数量、返回的文档数量等关键信息。这对于优化查询性能至关重要,因为只有清楚了解查询的执行过程,才能有针对性地进行优化。

在 MongoDB 中,我们可以对任何查询使用 explain 方法。例如,假设我们有一个名为 users 的集合,其中包含用户信息,每个文档可能有 nameageemail 等字段。如果我们想查询年龄大于 30 岁的用户,可以这样写查询语句:

db.users.find({ age: { $gt: 30 } });

要获取这个查询的执行计划,只需在查询语句后链式调用 explain 方法:

db.users.find({ age: { $gt: 30 } }).explain();

explain 的输出模式

explain 方法支持几种不同的输出模式,分别是 queryPlannerexecutionStatsallPlansExecution

queryPlanner 模式

queryPlanner 模式是默认模式。在这种模式下,explain 主要返回查询规划阶段的信息。它会展示查询优化器为执行查询所生成的各种执行计划,包括每个计划所使用的索引、扫描方向等。

例如,对于前面查询年龄大于 30 岁用户的例子,queryPlanner 模式下的部分输出可能如下:

{
    "queryPlanner": {
        "plannerVersion": 1,
        "namespace": "test.users",
        "indexFilterSet": false,
        "parsedQuery": {
            "age": {
                "$gt": 30
            }
        },
        "winningPlan": {
            "stage": "COLLSCAN",
            "filter": {
                "age": {
                    "$gt": 30
                }
            },
            "direction": "forward"
        },
        "rejectedPlans": []
    }
}

在这个输出中,winningPlan 表明最终选择的执行计划。这里 stageCOLLSCAN,意味着 MongoDB 将进行全集合扫描,因为没有合适的索引可以利用来优化这个查询。

executionStats 模式

executionStats 模式不仅包含查询规划阶段的信息,还会添加执行阶段的统计信息。这些统计信息包括实际扫描的文档数量、返回的文档数量、执行查询所花费的时间等。这对于评估查询的实际性能非常有帮助。

要使用 executionStats 模式,我们这样调用 explain

db.users.find({ age: { $gt: 30 } }).explain("executionStats");

输出结果中除了包含 queryPlanner 模式的信息外,还会有 executionStats 部分:

{
    "queryPlanner": {
        // 与 queryPlanner 模式类似的内容
    },
    "executionStats": {
        "executionSuccess": true,
        "nReturned": 10,
        "executionTimeMillis": 20,
        "totalKeysExamined": 0,
        "totalDocsExamined": 100,
        "executionStages": {
            "stage": "COLLSCAN",
            "filter": {
                "age": {
                    "$gt": 30
                }
            },
            "nReturned": 10,
            "executionTimeMillisEstimate": 15,
            "works": 101,
            "advanced": 10,
            "needTime": 90,
            "needYield": 0,
            "saveState": 1,
            "restoreState": 1,
            "isEOF": 1,
            "direction": "forward",
            "docsExamined": 100
        }
    }
}

这里 nReturned 表示返回的文档数量为 10,executionTimeMillis 表示执行查询花费了 20 毫秒,totalDocsExamined 表示总共检查了 100 个文档。

allPlansExecution 模式

allPlansExecution 模式会返回所有可能的执行计划及其执行统计信息。这对于深入分析查询优化器为什么选择某个特定计划非常有用,尤其是在存在多个可行计划的情况下。

调用方式如下:

db.users.find({ age: { $gt: 30 } }).explain("allPlansExecution");

其输出结果会包含多个计划及其执行统计信息,例如:

{
    "queryPlanner": {
        // 与前面模式类似的规划信息
    },
    "executionStats": {
        // 与 executionStats 模式类似的执行统计信息
    },
    "allPlansExecution": [
        {
            "stage": "COLLSCAN",
            "filter": {
                "age": {
                    "$gt": 30
                }
            },
            "nReturned": 10,
            "executionTimeMillisEstimate": 15,
            // 其他执行统计信息
        },
        {
            // 另一个可能的执行计划及其统计信息
        }
    ]
}

explain 输出字段详解

queryPlanner 部分字段

  1. plannerVersion:表示查询优化器的版本,目前通常为 1。
  2. namespace:指定查询所涉及的集合,格式为 数据库名.集合名
  3. indexFilterSet:如果为 true,表示查询使用了索引过滤。在某些复杂查询中,可能会根据索引来过滤部分文档,以减少后续的处理量。
  4. parsedQuery:展示解析后的查询条件,这与我们在查询语句中指定的条件相对应,方便确认查询的正确性。
  5. winningPlan:这是最重要的部分之一,它展示了查询优化器最终选择的执行计划。stage 字段表示执行计划的阶段,常见的阶段有 COLLSCAN(全集合扫描)、IXSCAN(索引扫描)、FETCH(根据索引获取文档)等。例如,如果 stageIXSCAN,还会有 keyPattern 字段展示使用的索引结构。
  6. rejectedPlans:包含查询优化器考虑但最终拒绝的执行计划。通过分析这些被拒绝的计划,可以了解为什么优化器做出了最终的选择。

executionStats 部分字段

  1. executionSuccess:一个布尔值,表明查询是否成功执行。如果为 false,则需要进一步查看错误信息来调试查询。
  2. nReturned:实际返回给客户端的文档数量。
  3. executionTimeMillis:查询执行所花费的总时间,单位为毫秒。这是衡量查询性能的关键指标之一。
  4. totalKeysExamined:查询过程中检查的索引键的总数。如果这个值很高,说明索引的使用可能存在问题,或者查询需要扫描大量的索引数据。
  5. totalDocsExamined:查询过程中实际检查的文档数量。在全集合扫描的情况下,这个值可能会很大,可能需要考虑优化索引以减少文档检查数量。
  6. executionStages:详细描述执行阶段的信息。除了 stage 字段与 queryPlanner 中的 winningPlanstage 相对应外,还有其他一些重要字段,如 works 表示执行阶段执行的操作次数,advanced 表示成功推进(返回文档)的次数,needTime 表示需要更多时间来完成操作的次数等。

根据 explain 输出优化查询性能

索引优化

通过 explain 输出,如果发现 winningPlanstageCOLLSCAN,而查询条件又经常被使用,那么可以考虑添加合适的索引。

例如,对于前面年龄大于 30 岁用户的查询,我们可以添加一个年龄字段的索引:

db.users.createIndex({ age: 1 });

添加索引后,再次执行 explain

db.users.find({ age: { $gt: 30 } }).explain("executionStats");

此时输出的 winningPlan 可能变为:

{
    "winningPlan": {
        "stage": "IXSCAN",
        "keyPattern": {
            "age": 1
        },
        "indexName": "age_1",
        "isMultiKey": false,
        "direction": "forward",
        "indexBounds": {
            "age": [
                "(30.0, max]"
            ]
        }
    }
}

可以看到,stage 变为 IXSCAN,表示使用了索引扫描,这通常会大大提高查询性能。同时,executionStats 中的 totalDocsExaminedexecutionTimeMillis 等指标也会得到改善。

复合索引优化

在实际应用中,查询条件可能会涉及多个字段。例如,我们可能需要查询年龄大于 30 岁且居住在特定城市的用户:

db.users.find({ age: { $gt: 30 }, city: "New York" });

如果单独对 agecity 字段创建索引,查询优化器可能无法充分利用这些索引。此时,可以考虑创建复合索引:

db.users.createIndex({ age: 1, city: 1 });

复合索引的顺序很重要,一般将选择性高(区分度大)的字段放在前面。创建复合索引后,再次执行 explain,观察执行计划的变化。如果查询优化器能够正确使用复合索引,winningPlan 中的 stage 会变为 IXSCAN,并且 keyPattern 会展示复合索引的结构。

避免全集合扫描

全集合扫描通常是性能瓶颈,通过 explain 确认存在全集合扫描后,除了添加索引外,还可以考虑限制查询返回的字段。例如,如果我们只需要用户的 nameage 字段,而不是整个文档:

db.users.find({ age: { $gt: 30 } }, { name: 1, age: 1, _id: 0 });

这样可以减少从磁盘读取的数据量,提高查询性能。同时,explain 输出中的 executionTimeMillis 等指标可能会有所改善。

分析执行时间和文档检查数量

仔细分析 executionStats 中的 executionTimeMillistotalDocsExamined 等指标。如果执行时间过长,而 totalDocsExamined 又很大,说明可能存在索引不合理或者查询条件过于宽泛的问题。可以尝试进一步细化查询条件,或者调整索引结构。

例如,如果一个查询执行时间很长,totalDocsExamined 为 10000,但只返回了 10 个文档,可能需要检查查询条件是否可以更精确,或者是否可以通过索引来更快地定位到这 10 个文档。

复杂查询的 explain 分析

多条件查询

假设我们有一个电商数据库,其中的 products 集合包含产品信息,每个文档有 price(价格)、category(类别)、rating(评分)等字段。我们要查询价格在 50 到 100 之间,类别为 electronics,且评分大于 4 的产品:

db.products.find({
    price: { $gte: 50, $lte: 100 },
    category: "electronics",
    rating: { $gt: 4 }
});

执行 explain("executionStats") 后,我们来分析输出。如果 winningPlanCOLLSCAN,说明没有合适的索引支持这个查询。我们可以考虑创建复合索引:

db.products.createIndex({ price: 1, category: 1, rating: 1 });

再次执行 explain,查看执行计划是否变为 IXSCAN,以及执行时间和文档检查数量等指标的变化。

聚合查询

聚合查询在 MongoDB 中也很常见。例如,我们要统计每个类别的产品数量,并只返回数量大于 10 的类别:

db.products.aggregate([
    { $group: { _id: "$category", count: { $sum: 1 } } },
    { $match: { count: { $gt: 10 } } }
]);

执行 explain("executionStats") 来分析聚合查询的执行计划。在聚合查询中,stage 可能会有 $group$match 等不同阶段的详细信息。如果某个阶段执行时间过长,可以针对该阶段进行优化。例如,如果 $group 阶段处理的数据量过大,可以考虑在 $group 之前添加一些过滤条件,减少输入数据量。

总结 explain 在性能优化中的作用

通过深入理解 MongoDB explain 的输出,我们能够清晰地看到查询的执行过程,发现潜在的性能问题,并针对性地进行优化。无论是简单查询还是复杂的聚合查询,explain 都是优化查询性能的重要工具。通过合理使用索引、避免全集合扫描、分析执行时间和文档检查数量等方式,我们可以显著提高 MongoDB 数据库应用的性能。在实际开发中,应养成对关键查询使用 explain 进行分析的习惯,持续优化数据库查询,确保系统的高效运行。同时,随着数据量的增长和业务需求的变化,定期重新评估查询和索引结构,利用 explain 及时发现并解决性能问题。

以上就是关于 MongoDB explain 输出解读以及如何利用它优化查询性能的详细介绍,希望能帮助开发者更好地优化基于 MongoDB 的应用程序。在实际场景中,需要不断实践和分析 explain 输出,结合业务特点进行针对性优化,以达到最佳的性能效果。同时,要注意索引的合理使用,避免过多索引带来的存储和维护成本增加等问题。通过持续关注查询性能和利用 explain 工具,能够构建更加健壮和高效的 MongoDB 数据库系统。