一、为什么需要关注查询计划

作为一个数据库管理员或者开发人员,你可能经常遇到这样的场景:某个查询昨天还跑得好好的,今天突然就变得特别慢。这时候你可能会抓耳挠腮,百思不得其解。其实啊,这就像是开车时突然遇到堵车,你得知道到底是前方有事故,还是单纯的车流量太大。

MongoDB的查询计划就像是这个路况分析报告,它能告诉你查询到底是怎么执行的。通过分析查询计划,我们可以找出查询变慢的根本原因,就像交警通过监控找出堵车的源头一样。

举个例子,假设我们有一个电商网站的订单集合,里面有上百万条记录。某天你发现查询某个用户的订单历史变得特别慢:

// MongoDB查询示例
db.orders.find({
    userId: "user123",
    status: "completed"
}).sort({createTime: -1}).limit(10)

这个查询看起来很简单,但为什么会变慢呢?这时候查询计划就能帮上大忙了。

二、如何获取和分析查询计划

获取查询计划其实很简单,MongoDB提供了explain()方法来展示查询的执行计划。这个方法就像是给你的查询装了个X光机,能让你看到查询执行的内部细节。

让我们用上面的订单查询为例,看看如何获取和分析查询计划:

// 获取查询计划
db.orders.explain("executionStats").find({
    userId: "user123",
    status: "completed"
}).sort({createTime: -1}).limit(10)

这个命令会返回一大堆信息,我们主要关注几个关键点:

  1. queryPlanner.winningPlan:展示了MongoDB选择的执行计划
  2. executionStats:包含了查询执行的实际统计信息
  3. executionStats.executionStages:详细展示了查询执行的各个阶段

举个例子,假设返回的结果中有这样的信息:

{
    "executionStats": {
        "nReturned": 10,
        "executionTimeMillis": 1200,
        "totalKeysExamined": 0,
        "totalDocsExamined": 850000,
        "executionStages": {
            "stage": "COLLSCAN",
            "filter": {
                "userId": {"$eq": "user123"},
                "status": {"$eq": "completed"}
            },
            "direction": "forward",
            "docsExamined": 850000
        }
    }
}

看到这个结果,问题就很明显了:MongoDB正在执行全集合扫描(COLLSCAN),检查了85万份文档才找到10条匹配的记录。这就像是你要在一本没有目录的厚书中找某个特定章节,只能一页一页翻,效率当然低了。

三、优化查询的常见策略

既然发现了问题,那怎么解决呢?这里我给大家介绍几种常见的优化策略。

1. 添加合适的索引

索引就像是书的目录,能帮助数据库快速定位到需要的数据。针对上面的查询,我们可以创建一个复合索引:

// 创建复合索引
db.orders.createIndex({
    userId: 1,
    status: 1,
    createTime: -1
})

这个索引包含了查询的所有条件字段和排序字段。创建后再看查询计划:

{
    "executionStats": {
        "nReturned": 10,
        "executionTimeMillis": 5,
        "totalKeysExamined": 10,
        "totalDocsExamined": 10,
        "executionStages": {
            "stage": "FETCH",
            "inputStage": {
                "stage": "IXSCAN",
                "keyPattern": {
                    "userId": 1,
                    "status": 1,
                    "createTime": -1
                },
                "indexName": "userId_1_status_1_createTime_-1"
            }
        }
    }
}

现在查询只需要检查10个索引条目和10个文档,执行时间从1200毫秒降到了5毫秒,效果立竿见影!

2. 覆盖查询

有时候查询可以完全通过索引完成,不需要读取实际文档,这就是覆盖查询。比如我们只需要用户的订单ID和创建时间:

// 覆盖查询示例
db.orders.find({
    userId: "user123",
    status: "completed"
}, {
    _id: 1,
    createTime: 1
}).sort({createTime: -1}).limit(10)

如果我们的索引包含了这些字段,查询计划会显示:

{
    "executionStats": {
        "stage": "PROJECTION_COVERED",
        "inputStage": {
            "stage": "IXSCAN"
        }
    }
}

这种查询效率极高,因为完全不需要读取文档数据。

3. 避免索引失效的情况

有时候即使有索引,查询还是会全表扫描。常见的原因包括:

  • 使用了$where或JavaScript表达式
  • 使用了正则表达式且不是左锚定
  • 使用了$not或$nin操作符
  • 数据类型不匹配

比如这样的查询就会使索引失效:

// 会使索引失效的查询
db.orders.find({
    userId: {$not: {$eq: "user123"}}
})

四、高级查询计划分析技巧

除了基本的索引优化,还有一些高级技巧可以帮助我们更好地分析查询性能。

1. 比较多个查询计划

有时候MongoDB可能会考虑多个查询计划,我们可以通过explain("allPlansExecution")来查看所有候选计划:

// 查看所有候选计划
db.orders.explain("allPlansExecution").find({
    userId: "user123",
    createTime: {$gt: new Date("2023-01-01")}
})

结果会显示MongoDB评估的所有可能计划及其执行统计信息,帮助我们理解为什么选择了某个特定计划。

2. 分析索引交集

MongoDB有时会使用多个索引来满足一个查询,这称为索引交集。比如:

db.orders.find({
    $or: [
        {userId: "user123"},
        {status: "completed"}
    ]
})

如果有单独的userId和status索引,MongoDB可能会使用这两个索引然后合并结果。

3. 监控查询性能变化

我们可以使用db.currentOp()和数据库分析器来监控慢查询:

// 启用分析器
db.setProfilingLevel(1, 100)  // 记录超过100ms的查询

// 查看慢查询
db.system.profile.find().sort({ts:-1}).limit(10)

五、实际案例分析

让我们看一个真实的案例。某电商平台发现他们的商品搜索接口在促销期间变得特别慢。查询是这样的:

db.products.find({
    category: "electronics",
    price: {$lte: 1000},
    stock: {$gt: 0},
    tags: "discount"
}).sort({rating: -1}).limit(50)

分析查询计划后发现MongoDB使用了{category:1, price:1}的索引,但仍然需要扫描大量文档。我们优化了索引:

db.products.createIndex({
    category: 1,
    tags: 1,
    stock: 1,
    price: 1,
    rating: -1
})

同时,我们意识到促销期间"discount"标签的商品特别多,于是添加了部分索引:

db.products.createIndex({
    category: 1,
    price: 1,
    rating: -1
}, {
    partialFilterExpression: {
        tags: "discount",
        stock: {$gt: 0}
    }
})

这样优化后,查询性能提升了20倍。

六、总结与最佳实践

通过上面的例子,我们可以总结出一些MongoDB查询优化的最佳实践:

  1. 总是先分析查询计划,不要凭直觉优化
  2. 创建合适的复合索引,考虑查询、排序和投影的需求
  3. 考虑使用覆盖查询减少IO
  4. 注意索引失效的情况
  5. 对于特定场景考虑使用部分索引
  6. 定期监控查询性能,特别是数据量变化后
  7. 理解MongoDB的查询优化器工作原理

记住,没有放之四海而皆准的优化方案,每个查询都需要根据具体情况分析。就像医生看病一样,要先诊断再治疗,查询计划分析就是我们的诊断工具。

最后提醒一点,索引不是越多越好。每个索引都会增加写入时的开销,并且占用存储空间。要在查询性能和写入性能之间找到平衡点。