精通 MongoDB 索引,实现最佳查询性能

通过掌握索引技术,在 MongoDB 中实现最佳查询性能。本综合指南涵盖了单字段索引、复合索引和多键索引,阐释了覆盖查询的强大功能,并指导您使用 `explain()` 进行性能分析。学习最佳实践,以加速读操作、减少数据库负载,并构建更具响应性的应用程序。对于任何专注于性能调优的 MongoDB 开发者而言,这是必读资料。

30 浏览量

掌握 MongoDB 索引以实现最佳查询性能

在数据库管理的领域中,性能至关重要。对于流行的 NoSQL 文档数据库 MongoDB 而言,优化查询性能通常是实现响应迅速和可扩展应用的关键。可供您使用的最强大的工具之一就是索引。MongoDB 中的索引是一种特殊的数据结构,它以易于遍历的形式存储了集合数据集中一小部分数据。这使得 MongoDB 能够在不扫描整个集合的情况下快速定位和检索文档,从而极大地加快了读取操作的速度。

本文将指导您完成在 MongoDB 中创建高效索引的基本技术。我们将涵盖索引的基础知识,探讨复合索引和覆盖查询等高级概念,并讨论可利用的各种索引类型,以显著提高应用程序的读取性能。通过掌握 MongoDB 索引,您可以释放数据库的全部潜力,确保流畅的用户体验。

理解 MongoDB 索引

从本质上讲,索引就像书中的目录。您不必阅读整本书来查找特定主题,而是查阅目录以快速跳转到相关页面。同样,MongoDB 索引可帮助数据库引擎高效地定位匹配查询条件的文档。如果没有索引,MongoDB 将不得不执行集合扫描,检查每个文档以查找满足查询的文档。这可能非常缓慢,特别是对于大型集合。

索引的工作原理

MongoDB 通常为其索引使用 B 树结构。B 树是一种自平衡的树状数据结构,它维护已排序的数据,并允许以对数时间进行搜索、顺序访问、插入和删除。当您查询具有索引字段的集合时,MongoDB 会遍历 B 树以查找匹配的文档。此过程比扫描整个集合要快得多。

何时使用索引

索引对经常用于以下目的的字段最有利:

  • 查询条件(find(), findOne()): 用于查询的 filter 文档中的字段。
  • 排序条件(sort()): 用于对查询结果进行排序的字段。
  • _id 字段: 默认情况下,MongoDB 会在 _id 字段上创建索引,确保 ID 的唯一性和快速查找。

但是,索引也有成本:

  • 存储空间: 索引会占用磁盘空间。
  • 写入性能: 每当插入、更新或删除文档时,都需要更新索引,这可能会减慢写入操作的速度。

因此,至关重要的是要有策略地创建索引,重点关注那些能为常用读取操作带来最大性能提升的字段。

创建和管理索引

MongoDB 提供了 createIndex() 方法来创建索引,并使用 getIndexes() 查看现有索引。dropIndex() 方法用于删除索引。

基本索引创建

要创建单字段索引,您需要指定字段名称和索引类型(通常 1 表示升序,-1 表示降序)。

db.collection.createIndex( { fieldName: 1 } );

示例: 以升序索引 username 字段:

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

查看索引

要查看集合上的索引:

db.collection.getIndexes();

示例: 查看 users 集合上的索引:

db.users.getIndexes();

这将返回一个索引定义数组,包括默认的 _id 索引。

删除索引

要删除索引:

db.collection.dropIndex( "indexName" );

您可以在 getIndexes() 的输出中找到 indexName。或者,您可以通过指定与 createIndex() 相同格式的索引字段来删除索引:

db.collection.dropIndex( { fieldName: 1 } );

示例: 删除 username 索引:

db.users.dropIndex( "username_1" ); // 使用索引名称
// 或者
db.users.dropIndex( { username: 1 } ); // 使用索引定义

复合索引

复合索引涉及多个字段。复合索引中字段的顺序至关重要。MongoDB 对在 filtersort 子句中涉及多个字段的查询使用复合索引。

何时使用复合索引

当您的查询频繁地按字段组合进行筛选或排序时,复合索引最有效。该索引可以满足与索引中定义的字段顺序相同或作为索引前缀的查询。

示例: 考虑一个包含 userIdorderDatestatus 等字段的 orders 集合。如果您经常按特定用户查询订单并按日期排序,那么对 { userId: 1, orderDate: 1 } 创建复合索引将非常有益。

db.orders.createIndex( { userId: 1, orderDate: 1 } );

此索引可以有效地支持以下查询:

  • db.orders.find( { userId: "user123" } ).sort( { orderDate: -1 } )
  • db.orders.find( { userId: "user123", orderDate: { $lt: ISODate() } } )

然而,如果仅按 orderDate 筛选而未指定 userId,或者字段顺序不同,则它对仅按 orderDate 筛选的查询效果可能不佳。

字段顺序很重要

复合索引中字段的顺序决定了它对不同查询模式的选择性。通常,将基数较高(具有更多不同值)或最常用于相等匹配的字段放在索引的前面。

对于排序结果的查询,索引中字段的顺序应与 sort() 操作中字段的顺序相匹配,以获得最佳性能。如果查询同时包含筛选和排序,并且索引与筛选字段匹配,则它也可以用于排序,而无需单独的集合扫描来进行排序。

覆盖查询

覆盖查询是指 MongoDB 仅使用索引即可满足整个查询的查询。这意味着索引包含查询和投影中使用的所有字段。覆盖查询避免了从集合本身获取文档,因此速度极快。

如何实现覆盖查询

要实现覆盖查询,请确保:

  1. 您有一个包含查询筛选中使用的所有字段的索引。
  2. 您的投影中仅包含这些索引字段(或其中的一个子集)。

示例: 考虑一个包含 nameagecity 字段的 employees 集合。如果您有一个索引 { city: 1, age: 1 },并且希望检索特定城市员工的姓名和年龄,您可以创建一个覆盖查询:

db.employees.find( { city: "New York" }, { name: 1, age: 1, _id: 0 } ).explain()

在此查询中,city 位于索引中,nameage 包含在投影中。如果索引还包含 nameage,则它将是一个覆盖查询。

让我们完善索引和查询以实现真正的覆盖查询:

// 创建一个包含查询和投影所需所有字段的索引
db.employees.createIndex( { city: 1, age: 1, name: 1 } );

// 现在,按城市筛选并投影姓名和年龄的查询可以被覆盖
db.employees.find( { city: "New York" }, { name: 1, age: 1, _id: 0 } )

当您对该查询运行 explain("executionStats") 时,您应该会看到 “totalDocsExamined” 等于 “totalKeysExamined”,并且 “executionType” 可能指示 “_id_only” 或 “covered_query”。这表示查询已完全由索引满足。

其他重要索引类型

MongoDB 为特定用例提供了各种索引类型:

多键索引

当您索引数组字段时,会自动创建多键索引。它们允许您查询数组内的元素。

示例: 如果您有一个包含 tags 数组字段 ["electronics", "gadgets"]products 集合:

db.products.createIndex( { tags: 1 } );

此索引将支持 db.products.find( { tags: "electronics" } ) 这样的查询。

文本索引

文本索引支持对文档中字符串内容的高效搜索。它们用于使用 $text 运算符的文本搜索查询。

db.articles.createIndex( { content: "text" } );

这允许进行如下搜索:db.articles.find( { $text: { $search: "database performance" } } )

地理空间索引

地理空间索引用于使用 $near$geoWithin$geoIntersects 运算符对地理空间数据进行高效查询。

db.locations.createIndex( { loc: "2dsphere" } ); // 用于 2dsphere 索引

唯一索引

唯一索引强制要求一个字段或一组字段的唯一性。如果插入或更新了重复值,MongoDB 将返回错误。

db.users.createIndex( { email: 1 }, { unique: true } );

使用 explain() 进行性能分析

了解 MongoDB 如何执行查询对于优化它们至关重要。explain() 方法提供了对查询执行计划的深入了解,包括索引是否被使用以及如何使用。

db.collection.find( {...} ).explain( "executionStats" );

explain() 输出中要查找的关键字段:

  • winningPlan.stage:指示执行计划的阶段(例如,COLLSCAN 表示集合扫描,IXSCAN 表示索引扫描)。
  • executionStats.totalKeysExamined:检查的索引键的数量。
  • executionStats.totalDocsExamined:检查的文档数量。

良好的执行计划应使 totalDocsExamined 等于或接近返回的文档数量,并且 totalKeysExamined 远小于集合中文档的总数。如果 totalDocsExamined 非常高,或者使用了 COLLSCAN,则表明缺少索引或索引未被有效使用。

MongoDB 索引的最佳实践

  • 仅索引您需要的: 避免对很少被查询或排序的字段创建索引。每个索引都会增加开销。
  • 明智地使用复合索引: 根据查询模式正确排序字段。首先考虑选择性最高的字段。
  • 目标是覆盖查询: 如果读取性能至关重要,请设计索引以覆盖常见的读取操作。
  • 监控索引使用情况: 定期使用 explain()db.collection.aggregate([{ $indexStats: {} }]) 审查索引使用情况,以识别未被使用或效率低下的索引。
  • 考虑索引的选择性: 基数较低(不同值较少)的字段上的索引可能不如基数较高的字段上的索引有效。
  • 保持索引较小: 除非绝对必要用于覆盖查询,否则避免在索引中包含大型字段或数组。
  • 测试您的索引: 在实际负载条件下,始终测试新索引对读取和写入性能的影响。

结论

有效的 MongoDB 索引是高性能 NoSQL 应用程序的基石。通过理解基本原理、掌握复合索引、利用覆盖查询以及使用 explain() 方法进行分析,您可以显著优化数据库的读取操作。请记住,需要在索引的优点与其成本之间取得平衡,并始终测试您的索引策略,以确保它们满足应用程序的具体需求。战略性索引不仅仅是加快查询速度;它是关于构建可扩展、响应迅速且高效的数据库系统。