编写高效 MongoDB 查询的五项最佳实践
通过更好的索引、投影、避免扫描、排序规划和定向更新来提升 MongoDB 查询性能。
编写高效 MongoDB 查询的五项最佳实践
MongoDB 查询在开发阶段可能感觉很快,但随着集合的增长,性能会急剧下降。高效的 MongoDB 查询依赖于将索引与实际访问模式相匹配、仅返回有用的字段,以及避免强制进行大规模扫描的操作。
这五项实践有助于保持读取性能的可预测性,并减少服务器上的不必要工作。
1. 战略性索引以支持查询
影响查询性能的最重要因素是索引的存在和正确使用。索引允许查询规划器快速定位匹配的文档,而无需扫描集合中的每个文档(即“COLLSCAN”)。
索引的工作原理
MongoDB 使用索引来满足查询谓词(查询的 filter 部分)。如果查询使用了索引中的字段,MongoDB 可以利用该索引快速缩小结果集。
最佳实践: 始终分析你的常见查询模式。如果你经常查询或排序字段 A、B 和 C,考虑创建一个复合索引 { A: 1, B: 1, C: 1 }。
避免无索引扫描
如果查询无法使用索引,MongoDB 默认会执行集合扫描(COLLSCAN),即读取集合中的每个文档。这在大型数据集上非常缓慢。
提示: 使用查询的 explain('executionStats') 方法来检查 winningPlan 以及 totalKeysExamined 与 totalDocsExamined 的对比。较大的差异通常表明索引使用不当或缺少索引。
// 示例:检查查询性能
db.users.find({ status: "active" }).explain('executionStats')
2. 利用投影限制返回字段
执行查询时,MongoDB 默认返回整个匹配文档。在许多应用中,你只需要几个字段(例如,显示名称列表)。获取不必要的大字段(如嵌入式数组或大文本块)会增加网络延迟、数据库服务器的内存使用以及客户端内存消耗。
投影允许你指定应返回哪些字段。
投影语法
在 find() 方法中使用第二个参数来指定包含(1)或排除(0)的字段。
_id默认包含,除非显式排除(_id: 0)。
// 低效:返回整个用户文档
db.users.find({ organizationId: "XYZ" })
// 高效:仅返回用户的姓名和电子邮件
db.users.find(
{ organizationId: "XYZ" },
{ name: 1, email: 1, _id: 0 } // 包含 name 和 email,排除 _id
)
警告: 投影在与索引字段结合使用时效果最佳。如果查询仍然需要全表扫描,投影字段仅节省网络带宽,但不会改善初始搜索时间。
3. 避免强制全集合扫描的操作
某些查询操作本质上难以或无法通过标准索引满足,即使存在索引,也常常导致代价高昂的全集合扫描。
避免正则表达式中的前导通配符
索引是分层组织的(类似于按字母顺序排列的书籍索引)。以通配符(.*)开头的正则表达式无法利用索引,因为搜索词的起始点未知。
- 通常支持索引:
db.products.find({ sku: /^ABC/ }) - 通常代价高昂:
db.products.find({ sku: /.*CDE$/ })
提示: 如果必须在字符串值内搜索,考虑使用 MongoDB 的文本索引进行全文搜索,或规范化数据结构以支持前缀搜索。
谨慎查询非索引字段
如前所述,查询未索引的字段会强制进行扫描。要特别警惕涉及 $where 子句或评估 JavaScript 函数的复杂查询,因为这些几乎总是导致扫描每个文档。
4. 优化排序操作(覆盖查询)
使用 .sort() 方法对结果进行排序需要 MongoDB 要么检索所有匹配文档并在内存中排序(如果集合较小),要么使用索引排序执行计划(如果索引支持排序顺序)。
如果 MongoDB 无法使用索引进行排序,则可能需要进行阻塞式内存排序,并且当排序超过服务器对阻塞排序操作的内存限制时,可能会失败。
最佳实践:使用覆盖查询进行排序
覆盖查询是指查询谓词、投影和排序操作中涉及的所有字段都包含在单个索引中。当查询被覆盖时,MongoDB 无需查看实际文档——它直接从索引结构中获取所需的一切。
// 假设索引:{ category: 1, price: -1 }
// 高效的覆盖查询:
db.inventory.find(
{ category: "Electronics" }, // 查询字段在索引中
{ price: 1, _id: 0 } // 投影字段在索引中
).sort({ price: -1 }) // 排序字段在索引中
5. 优先使用原子更新和写操作
虽然本文重点讨论读取性能,但高效的写操作通过减少锁定和争用,显著有助于整体数据库健康。更新应尽可能有针对性。
使用更新运算符而非替换整个文档
修改文档时,使用特定的更新运算符,如 $set、$inc 或 $push,而不是读取文档、在客户端修改后再将整个文档写回。
低效: 读取整个文档 -> 在应用中修改 -> 写回整个文档。
高效: 使用原子运算符仅更改必要的字段。
// 高效更新:原子递增计数器,不触及其他字段
db.metrics.updateOne(
{ metricName: "login_attempts" },
{ $inc: { count: 1 } }
)
通过使用原子运算符,你可以最小化写冲突的可能性,并减少通过网络传输的数据量。
关键要点
编写高效的 MongoDB 查询围绕应用程序逻辑与数据库引擎使用索引之间的协作。通过遵循这五项最佳实践,你可以确保读取操作快速、可扩展且资源友好:
- 战略性索引: 确保为常见查询过滤器和排序条件创建索引。
- 使用投影: 仅检索你绝对需要的字段。
- 避免扫描: 避免在正则表达式中使用前导通配符和
$where子句。 - 优化排序: 追求覆盖查询,使索引包含查询、投影和排序所需的所有字段。
- 优先原子写入: 使用
$set等运算符在更新期间最小化开销。
定期检查慢查询日志,并使用 explain() 验证你的查询是否使用了你创建的索引。性能调优是一个持续的过程,但这些实践为高性能的 MongoDB 部署奠定了坚实基础。