外观
MongoDB 查询改写技巧
查询改写的重要性
- 提高查询性能:通过改写查询可以利用索引,减少扫描的文档数量
- 减少内存使用:优化的查询可以减少内存消耗
- 提高吞吐量:高效的查询可以处理更多并发请求
- 避免全表扫描:改写查询可以避免COLLSCAN,减少磁盘I/O
- 优化聚合操作:改写聚合管道可以提高执行效率
查询改写基础
理解查询执行计划
javascript
// 分析查询计划
db.collection.find({ field: "value" }).explain("executionStats")
// 查看查询计划的关键指标
// - executionStats.nReturned:返回的文档数量
// - executionStats.totalKeysExamined:扫描的索引键数量
// - executionStats.totalDocsExamined:扫描的文档数量
// - executionStats.executionTimeMillis:查询执行时间
// - executionStats.executionStages.stage:执行阶段(IXSCAN表示索引扫描,COLLSCAN表示全表扫描)查询改写原则
- 利用索引:确保查询条件能够使用索引
- 减少扫描范围:缩小查询的时间范围或数据范围
- 简化查询条件:避免复杂的逻辑表达式
- 使用投影:只返回需要的字段
- 限制结果集大小:使用limit()限制返回的文档数量
- 优化排序:确保排序操作能够使用索引
常见查询改写技巧
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" } }
])查询改写最佳实践
- 分析查询计划:使用explain()分析查询执行计划,识别性能瓶颈
- 创建合适的索引:为常用查询创建合适的索引
- 优化数据模型:根据查询模式优化数据模型,考虑数据冗余
- 监控查询性能:定期监控慢查询,及时优化
- 使用hint():在必要时使用hint()强制使用特定索引
- 限制结果集大小:使用limit()和skip()限制返回的文档数量
- 使用投影:只返回需要的字段
- 避免复杂查询:将复杂查询拆分为多个简单查询
- 定期更新统计信息:确保查询优化器有准确的统计信息
- 测试不同改写方案:比较不同改写方案的性能,选择最优方案
查询改写案例
案例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 })问题:查询执行时间长,使用了全表扫描
解决方案:
- 创建复合索引:
javascript
db.orders.createIndex({
customer_id: 1,
status: 1,
createdAt: -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 })- 监控查询性能:
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") }
})问题:正则表达式查询无法使用索引
解决方案:
- 创建索引:
javascript
db.logs.createIndex({ level: 1, timestamp: 1 })- 改写查询,先使用索引字段过滤,再使用正则表达式:
javascript
db.logs.find({
level: "error",
timestamp: { $gte: ISODate("2023-06-01"), $lte: ISODate("2023-06-30") },
message: /timeout/
})- 考虑使用文本索引:
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: 优化慢查询的步骤:
- 使用explain()分析查询计划
- 识别性能瓶颈(全表扫描、内存使用过高、索引未使用等)
- 创建合适的索引
- 改写查询,利用索引
- 优化数据模型
- 监控优化后的性能
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: 平衡查询性能和写性能的方法:
- 只创建必要的索引,避免过多索引影响写性能
- 考虑使用部分索引或稀疏索引
- 定期维护索引,清理不使用的索引
- 优化写操作,使用批量写入
- 考虑使用读写分离架构
