Skip to content

MongoDB 查询改写技巧

查询改写的重要性

  1. 提高查询性能:通过改写查询可以利用索引,减少扫描的文档数量
  2. 减少内存使用:优化的查询可以减少内存消耗
  3. 提高吞吐量:高效的查询可以处理更多并发请求
  4. 避免全表扫描:改写查询可以避免COLLSCAN,减少磁盘I/O
  5. 优化聚合操作:改写聚合管道可以提高执行效率

查询改写基础

理解查询执行计划

javascript
// 分析查询计划
db.collection.find({ field: "value" }).explain("executionStats")

// 查看查询计划的关键指标
// - executionStats.nReturned:返回的文档数量
// - executionStats.totalKeysExamined:扫描的索引键数量
// - executionStats.totalDocsExamined:扫描的文档数量
// - executionStats.executionTimeMillis:查询执行时间
// - executionStats.executionStages.stage:执行阶段(IXSCAN表示索引扫描,COLLSCAN表示全表扫描)

查询改写原则

  1. 利用索引:确保查询条件能够使用索引
  2. 减少扫描范围:缩小查询的时间范围或数据范围
  3. 简化查询条件:避免复杂的逻辑表达式
  4. 使用投影:只返回需要的字段
  5. 限制结果集大小:使用limit()限制返回的文档数量
  6. 优化排序:确保排序操作能够使用索引

常见查询改写技巧

1. 使用索引字段进行精确匹配

原始查询

javascript
db.users.find({ name: /^John/ })

改写后

javascript
// 创建索引
db.users.createIndex({ name: 1 })

// 使用索引字段进行精确匹配
db.users.find({ name: { $regex: /^John/ } })
// 或使用前缀匹配
db.users.find({ name: { $gte: "John", $lt: "Johnz" } })

2. 优化范围查询

原始查询

javascript
db.orders.find({ amount: { $gt: 100, $lt: 1000 } })

改写后

javascript
// 创建索引
db.orders.createIndex({ amount: 1 })

// 优化范围查询
db.orders.find({ amount: { $gt: 100, $lt: 1000 } })
  .hint({ amount: 1 }) // 强制使用索引
  .explain()

3. 优化排序操作

原始查询

javascript
db.users.find({ age: { $gt: 25 } }).sort({ name: 1 })

改写后

javascript
// 创建复合索引,包含查询字段和排序字段
db.users.createIndex({ age: 1, name: 1 })

// 使用复合索引进行查询和排序
db.users.find({ age: { $gt: 25 } }).sort({ name: 1 })

4. 使用投影减少数据传输

原始查询

javascript
db.users.find({ age: { $gt: 25 } })

改写后

javascript
// 使用投影,只返回需要的字段
db.users.find({ age: { $gt: 25 } }, { name: 1, email: 1, _id: 0 })

5. 优化in查询

原始查询

javascript
db.users.find({ status: { $in: ["active", "pending"] } })

改写后

javascript
// 创建索引
db.users.createIndex({ status: 1 })

// 使用in查询,确保索引被使用
db.users.find({ status: { $in: ["active", "pending"] } })

// 对于大量值的in查询,考虑使用$or
// 但注意$or的每个条件都需要有索引
db.users.find({ $or: [{ status: "active" }, { status: "pending" }] })

6. 优化exists查询

原始查询

javascript
db.users.find({ email: { $exists: true } })

改写后

javascript
// 创建稀疏索引
db.users.createIndex({ email: 1 }, { sparse: true })

// 使用exists查询,利用稀疏索引
db.users.find({ email: { $exists: true } })

// 或使用部分索引
db.users.createIndex(
  { email: 1 },
  { partialFilterExpression: { email: { $exists: true } } }
)

7. 优化$ne查询

原始查询

javascript
db.users.find({ status: { $ne: "inactive" } })

改写后

javascript
// $ne查询通常无法使用索引,可以考虑使用$nin或其他方式
// 方法1:使用$nin
db.users.find({ status: { $nin: ["inactive"] } })

// 方法2:使用$or
db.users.find({ $or: [{ status: "active" }, { status: "pending" }] })

// 方法3:添加状态字段索引
db.users.createIndex({ status: 1 })

8. 优化聚合查询

原始聚合

javascript
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: { _id: "$customer_id", total: { $sum: "$amount" } } },
  { $sort: { total: -1 } }
])

改写后

javascript
// 创建索引
db.orders.createIndex({ status: 1, customer_id: 1, amount: 1 })

// 优化聚合管道
// 1. 先使用$match过滤数据
// 2. 再使用$group进行分组
// 3. 最后使用$sort进行排序
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: { _id: "$customer_id", total: { $sum: "$amount" } } },
  { $sort: { total: -1 } }
])

// 或使用hint强制使用索引
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: { _id: "$customer_id", total: { $sum: "$amount" } } },
  { $sort: { total: -1 } }
], { hint: { status: 1, customer_id: 1, amount: 1 } })

9. 优化join查询

原始查询

javascript
// 使用$lookup进行join
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $lookup: {
      from: "customers",
      localField: "customer_id",
      foreignField: "_id",
      as: "customer"
    }
  },
  { $unwind: "$customer" },
  { $match: { "customer.country": "China" } }
])

改写后

javascript
// 优化join查询
// 1. 先过滤orders数据
// 2. 再进行join操作
// 3. 最后过滤customer数据
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $lookup: {
      from: "customers",
      localField: "customer_id",
      foreignField: "_id",
      as: "customer"
    }
  },
  { $unwind: "$customer" },
  { $match: { "customer.country": "China" } }
])

// 或考虑数据冗余,在orders集合中存储customer.country字段
db.orders.updateMany({}, [
  { $lookup: {
      from: "customers",
      localField: "customer_id",
      foreignField: "_id",
      as: "customer"
    }
  },
  { $unwind: "$customer" },
  { $set: { "customer_country": "$customer.country" } },
  { $unset: "customer" }
])

// 然后直接查询
db.orders.find({ status: "completed", customer_country: "China" })

10. 优化$text全文搜索

原始查询

javascript
db.articles.find({ $text: { $search: "mongodb query optimization" } })

改写后

javascript
// 创建文本索引
db.articles.createIndex({ content: "text", title: "text" })

// 优化全文搜索
// 1. 使用精确匹配
db.articles.find({ $text: { $search: "\"mongodb query\"" } })

// 2. 使用排除词
db.articles.find({ $text: { $search: "mongodb -query" } })

// 3. 使用权重
db.articles.createIndex(
  { content: "text", title: "text" },
  { weights: { title: 10, content: 5 } }
)

聚合管道改写技巧

1. 优化$match位置

原则:将$match操作符放在聚合管道的前面,减少后续阶段处理的文档数量

原始管道

javascript
db.orders.aggregate([
  { $group: { _id: "$status", count: { $sum: 1 } } },
  { $match: { count: { $gt: 100 } } }
])

改写后

javascript
db.orders.aggregate([
  { $match: { status: { $in: ["active", "completed"] } } },
  { $group: { _id: "$status", count: { $sum: 1 } } },
  { $match: { count: { $gt: 100 } } }
])

2. 优化$group操作

原则:减少$group操作处理的文档数量,使用合适的分组字段

原始管道

javascript
db.orders.aggregate([
  { $group: { _id: "$customer_id", total: { $sum: "$amount" } } },
  { $match: { total: { $gt: 1000 } } }
])

改写后

javascript
db.orders.aggregate([
  { $match: { amount: { $gt: 0 } } },
  { $group: { _id: "$customer_id", total: { $sum: "$amount" } } },
  { $match: { total: { $gt: 1000 } } }
])

3. 优化$sort操作

原则:确保$sort操作能够使用索引,避免在内存中排序

原始管道

javascript
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $sort: { createdAt: -1 } },
  { $limit: 100 }
])

改写后

javascript
// 创建索引
db.orders.createIndex({ status: 1, createdAt: -1 })

// 优化聚合管道
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $sort: { createdAt: -1 } },
  { $limit: 100 }
])

4. 优化$lookup操作

原则:减少$lookup操作处理的文档数量,使用索引字段进行关联

原始管道

javascript
db.orders.aggregate([
  { $lookup: {
      from: "products",
      localField: "product_id",
      foreignField: "_id",
      as: "product"
    }
  },
  { $unwind: "$product" },
  { $match: { "product.category": "electronics" } }
])

改写后

javascript
// 为products集合的_id字段创建索引(默认已有)
// 优化聚合管道
db.orders.aggregate([
  { $lookup: {
      from: "products",
      localField: "product_id",
      foreignField: "_id",
      as: "product"
    }
  },
  { $unwind: "$product" },
  { $match: { "product.category": "electronics" } }
])

// 或考虑使用$lookup的let和pipeline选项
db.orders.aggregate([
  { $lookup: {
      from: "products",
      let: { productId: "$product_id" },
      pipeline: [
        { $match: { $expr: { $eq: ["$_id", "$$productId"] }, category: "electronics" } }
      ],
      as: "product"
    }
  },
  { $unwind: "$product" }
])

5. 优化$unwind操作

原则:在$unwind之前使用$match过滤数据,减少unwind处理的文档数量

原始管道

javascript
db.users.aggregate([
  { $unwind: "$orders" },
  { $match: { "orders.status": "completed" } }
])

改写后

javascript
// 在unwind之前使用$match过滤数据
db.users.aggregate([
  { $match: { "orders.status": "completed" } },
  { $unwind: "$orders" },
  { $match: { "orders.status": "completed" } }
])

查询改写最佳实践

  1. 分析查询计划:使用explain()分析查询执行计划,识别性能瓶颈
  2. 创建合适的索引:为常用查询创建合适的索引
  3. 优化数据模型:根据查询模式优化数据模型,考虑数据冗余
  4. 监控查询性能:定期监控慢查询,及时优化
  5. 使用hint():在必要时使用hint()强制使用特定索引
  6. 限制结果集大小:使用limit()和skip()限制返回的文档数量
  7. 使用投影:只返回需要的字段
  8. 避免复杂查询:将复杂查询拆分为多个简单查询
  9. 定期更新统计信息:确保查询优化器有准确的统计信息
  10. 测试不同改写方案:比较不同改写方案的性能,选择最优方案

查询改写案例

案例1:优化电商订单查询

原始查询

javascript
db.orders.find({
  customer_id: ObjectId("60d5ec49f1a5c83a3c4e3b1a"),
  status: "completed",
  createdAt: { $gte: ISODate("2023-01-01"), $lte: ISODate("2023-12-31") }
}).sort({ createdAt: -1 })

问题:查询执行时间长,使用了全表扫描

解决方案

  1. 创建复合索引:
javascript
db.orders.createIndex({
  customer_id: 1,
  status: 1,
  createdAt: -1
})
  1. 改写查询,确保使用索引:
javascript
db.orders.find({
  customer_id: ObjectId("60d5ec49f1a5c83a3c4e3b1a"),
  status: "completed",
  createdAt: { $gte: ISODate("2023-01-01"), $lte: ISODate("2023-12-31") }
}).sort({ createdAt: -1 })
  1. 监控查询性能:
javascript
db.orders.find(...).explain("executionStats")

案例2:优化日志查询

原始查询

javascript
db.logs.find({
  level: "error",
  message: /timeout/,
  timestamp: { $gte: ISODate("2023-06-01"), $lte: ISODate("2023-06-30") }
})

问题:正则表达式查询无法使用索引

解决方案

  1. 创建索引:
javascript
db.logs.createIndex({ level: 1, timestamp: 1 })
  1. 改写查询,先使用索引字段过滤,再使用正则表达式:
javascript
db.logs.find({
  level: "error",
  timestamp: { $gte: ISODate("2023-06-01"), $lte: ISODate("2023-06-30") },
  message: /timeout/
})
  1. 考虑使用文本索引:
javascript
db.logs.createIndex({ message: "text" })
db.logs.find({ $text: { $search: "timeout" }, level: "error", timestamp: { $gte: ISODate("2023-06-01"), $lte: ISODate("2023-06-30") } })

常见问题(FAQ)

Q1: 如何判断查询是否需要改写?

A1: 当查询出现以下情况时,可能需要改写:

  • 查询执行时间长(超过100ms)
  • 查询计划显示全表扫描(COLLSCAN)
  • 扫描的文档数量远大于返回的文档数量
  • 查询使用了大量内存
  • 查询导致系统负载过高

Q2: 如何查看查询是否使用了索引?

A2: 使用explain()方法查看查询计划,如果executionStats.executionStages.stage为IXSCAN,表示使用了索引;如果为COLLSCAN,表示使用了全表扫描。

Q3: $or查询如何优化?

A3: $or查询的优化方法:

  • 为每个$or条件的字段创建索引
  • 确保$or条件能够使用索引
  • 考虑使用$in替代$or(如果条件是同一个字段)
  • 将$or查询拆分为多个单独的查询

Q4: 如何优化聚合管道?

A4: 聚合管道的优化方法:

  • 将$match操作符放在前面
  • 使用索引优化$match和$sort操作
  • 减少$group操作处理的文档数量
  • 优化$lookup操作,减少关联的数据量
  • 使用allowDiskUse: true处理大数据集

Q5: 如何优化慢查询?

A5: 优化慢查询的步骤:

  1. 使用explain()分析查询计划
  2. 识别性能瓶颈(全表扫描、内存使用过高、索引未使用等)
  3. 创建合适的索引
  4. 改写查询,利用索引
  5. 优化数据模型
  6. 监控优化后的性能

Q6: 为什么有时候索引没有被使用?

A6: 索引未被使用的原因:

  • 查询条件没有包含索引前缀
  • 查询条件使用了$not、$ne等无法使用索引的操作符
  • 数据量太小,全表扫描比索引扫描更快
  • 查询优化器认为全表扫描更高效
  • 索引统计信息过时

Q7: 如何强制查询使用索引?

A7: 使用hint()方法强制查询使用特定索引:

javascript
db.collection.find({ field: "value" }).hint({ field: 1 })

Q8: 如何优化$in查询?

A8: $in查询的优化方法:

  • 为in查询的字段创建索引
  • 限制in数组的大小(建议不超过1000个值)
  • 对于大量值的in查询,考虑使用$or查询
  • 确保in查询的字段选择性高

Q9: 如何优化排序操作?

A9: 排序操作的优化方法:

  • 为排序字段创建索引
  • 确保排序方向与索引方向一致
  • 避免在排序前进行大量数据处理
  • 考虑使用覆盖索引包含排序字段

Q10: 如何监控查询性能?

A10: 监控查询性能的方法:

  • 使用MongoDB的慢查询日志
  • 启用数据库分析器(profiler)
  • 使用explain()方法分析查询计划
  • 使用MongoDB Atlas或Ops Manager监控查询性能
  • 使用Prometheus + Grafana监控查询指标

Q11: 查询改写会影响查询结果吗?

A11: 正确的查询改写不会影响查询结果,只会改变查询的执行方式。在改写查询时,需要确保改写后的查询返回的结果与原始查询一致。

Q12: 如何平衡查询性能和写性能?

A12: 平衡查询性能和写性能的方法:

  • 只创建必要的索引,避免过多索引影响写性能
  • 考虑使用部分索引或稀疏索引
  • 定期维护索引,清理不使用的索引
  • 优化写操作,使用批量写入
  • 考虑使用读写分离架构