在MongoDB的世界里,索引就像是我们平时阅读书籍时使用的目录一样重要。如果你要在一本厚厚的技术书中寻找「数据库优化」这个话题,你会怎么做?直接从第一页开始一页一页地翻找,还是先查看目录,直接跳转到相关页码?显然,后者要高效得多。
MongoDB的索引正是扮演着这样的「目录」角色。当我们在一个包含数百万条用户记录的集合中查找特定用户时,如果没有索引,MongoDB就必须检查每一个文档,这个过程被称为「集合扫描」。这就像是在一个巨大的图书馆里,为了找到一本特定的书,我们不得不检查每一个书架上的每一本书。 索引的核心价值在于它为数据创建了一个有序的参考结构。通过这个结构,MongoDB能够快速定位到包含我们所需数据的文档,而无需遍历整个集合。这种机制可以将查询性能提升几个数量级,特别是在处理大型数据集时。

为了深入理解MongoDB索引的运行原理,下面以实际场景为例进行说明。设想我们有一个包含一万条用户数据的集合,我们首先生成如下测试数据:
|# 创建包含一万用户的测试集合 > for (i=0; i<10000; i++) { db.users.insertOne({ "i": i, "username": "user"+i, "age": Math.floor(Math.random()*120), "created": new Date() }); }
这个操作会在我们的数据库中创建一个名为users的集合,每个用户文档都包含一个序号、用户名、年龄和创建时间。现在,让我们尝试查找用户名为「user101」的用户,看看在没有索引的情况下会发生什么。
当我们执行查询时,MongoDB必须检查集合中的每一个文档。我们可以通过explain命令来观察这个过程:
|> db.users.find({"username": "user101"}).explain("executionStats") #输出结果如下: { "explainVersion" : "1", "queryPlanner" : { "namespace" : "db_4423ncne9_4429qbkbb.users", "indexFilterSet" : false, "parsedQuery" : { "username" : { "$eq" : "user101" } }, "maxIndexedOrSolutionsReached" : false, "maxIndexedAndSolutionsReached"
执行结果会显示一些关键信息。其中最重要的是totalDocsExamined字段,它告诉我们MongoDB为了找到这个用户检查了多少个文档。在我们的例子中,这个数字是10,000,意味着MongoDB必须检查集合中的每一个文档。
集合扫描的性能特点是随着数据量的增长呈线性下降。当你的数据从一千条增长到一百万条时,查询时间也会相应地增长一千倍。

现在,让我们看看创建索引后会发生什么奇迹。我们在username字段上创建一个索引:
|> db.users.createIndex({"username": 1})
这个命令告诉MongoDB为username字段创建一个升序索引。数字「1」表示升序排列,如果我们使用「-1」,则表示降序排列。索引创建完成后,MongoDB会构建一个包含所有用户名的有序结构,每个索引条目都指向相应的文档。
让我们再次执行相同的查询,观察性能的变化:
|> db.users.find({"username": "user101"}).explain("executionStats") #输出结果如下: { "explainVersion" : "1", "queryPlanner" : { "namespace" : "db_4423ncezm_4429qd6h7.users", "indexFilterSet" : false, "parsedQuery" : { "username" : { "$eq" : "user101" } }, "maxIndexedOrSolutionsReached" : false, "maxIndexedAndSolutionsReached"
这次的结果令人惊喜。executionTimeMillis从原来的几毫秒降到了1毫秒,而totalDocsExamined也从10000降到了1。这意味着MongoDB直接定位到了目标文档,无需检查其他任何文档。
索引的这种效果类似于我们在字典中查找单词。字典按字母顺序排列,所以我们可以快速跳转到以特定字母开头的部分,而不需要从第一页开始翻找。
虽然索引能够显著提升查询性能,但它们并非没有代价。每当我们插入、更新或删除包含被索引字段的文档时,MongoDB不仅需要修改文档本身,还必须更新相应的索引结构。这就像是维护一本书的目录一样,每当我们添加新章节或修改现有内容时,都需要相应地更新目录。
这种权衡通常是值得的,因为在大多数应用中,读取操作的频率远高于写入操作。一个精心设计的索引策略能够为应用带来数量级的性能提升,而写入性能的轻微下降通常是可以接受的。
选择为哪些字段创建索引需要仔细考虑应用的查询模式。我们应该分析哪些查询最频繁、哪些查询对性能要求最高,然后为这些查询涉及的字段创建合适的索引。对于那些很少使用或者只有管理员偶尔执行的查询,通常不值得为其专门创建索引。
在现实的应用场景中,我们经常需要基于多个字段进行查询。比如,我们可能需要查找特定年龄范围内的用户,并按用户名排序。这种情况下,单一字段的索引就显得力不从心了,这时候我们就需要使用复合索引。
复合索引是建立在两个或更多字段上的索引,它能够同时优化多个条件的查询。让我们通过一个具体的例子来理解这个概念。
假设我们经常需要执行这样的查询:
|> db.users.find().sort({"age": 1, "username": 1})
这个查询要求按年龄和用户名进行排序。如果我们只有一个基于username的单一索引,那么它对于这种排序需求帮助有限,因为排序首先基于年龄,而不是用户名。
为了优化这种查询,我们可以创建一个复合索引:
|> db.users.createIndex({"age": 1, "username": 1})
这个索引会创建一个包含年龄和用户名组合的有序结构。在这个结构中,文档首先按年龄排序,在相同年龄内,再按用户名排序。我们可以将这种结构想象为一个二维表格,其中每一行都包含了年龄和用户名的组合,并且整体保持有序。
复合索引支持三种主要的查询模式,每种模式的效率各不相同。让我们通过实例来理解这些模式:
当我们查询特定年龄的用户并按用户名排序时:
|> db.users.find({"age": 25}).sort({"username": -1})
在这种情况下,MongoDB能够直接跳转到年龄为25的索引段,然后按照索引中已经排好序的用户名顺序返回结果。这是最高效的查询模式,因为索引的结构完全匹配我们的查询需求。
当我们查询一个年龄范围内的用户时:
|> db.users.find({"age": {"$gte": 20, "$lte": 30}})
MongoDB会使用索引中的年龄字段来确定范围边界,然后遍历这个范围内的所有索引条目。由于索引按年龄排序,这种查询仍然很高效。
当我们同时使用范围查询和排序时:
|> db.users.find({"age": {"$gte": 20, "$lte": 30}}).sort({"username": 1})
这种模式比较复杂。MongoDB会先使用年龄范围来筛选文档,但由于查询涉及多个年龄值,返回的结果在用户名维度上可能不是有序的。因此,MongoDB可能需要在内存中对结果进行排序,这会降低查询效率。
当MongoDB需要在内存中排序超过32MB的数据时,查询会失败。为了避免这种情况,要么创建支持排序的索引,要么使用limit来减少结果集大小。
在进行复合索引设计时,应严格遵循若干关键原则。这些原则类似于数据库体系结构中的基础规范,能够最大程度提升查询性能与系统稳定性。
让我们通过一个实际的学生管理系统例子来说明这些原则:
|# 假设我们有这样的查询需求 > db.students.find({ "student_id": {"$gt": 500000}, "class_id": 54 }).sort({"final_grade": 1})
根据我们的设计原则,最优的索引结构应该是:
|> db.students.createIndex({"class_id": 1, "final_grade": 1, "student_id": 1})
这个索引设计首先使用class_id进行精确匹配(等值过滤器),然后利用final_grade支持排序,最后处理student_id的范围查询。
当我们的数据库中存在多个索引时,MongoDB面临一个重要问题:对于一个特定的查询,应该选择哪个索引?这就是查询优化器的职责。MongoDB的查询优化器采用了一种非常有趣的「竞赛」机制来解决这个问题。
想象一下一场赛跑比赛,参赛者是所有可能适用于当前查询的索引。当一个查询到达时,MongoDB会分析查询的「形状」(包括查询字段、排序要求等),然后识别出所有可能有用的候选索引。

假设当前集合上存在五个不同的索引,其中有三个被识别为与本次查询相关的候选索引。针对这三个候选索引,MongoDB会分别生成对应的查询执行计划,并在三个独立的执行线程中并行评估这些计划表现。整个过程类似一场指数级优化的“索引竞赛”,以评估哪一套索引结构能够以最高效的方式满足查询需求。
获胜的条件是:第一个返回所有查询结果,或者第一个按正确顺序返回试验数量结果的计划。这里的「正确顺序」很重要,因为如果查询需要排序,能够直接从索引返回有序结果的计划会有明显优势。
更重要的是,这种竞赛不是每次查询都要重新进行。MongoDB会将获胜的查询计划存储在缓存中,供未来相同「形状」的查询使用。这就像是记录了每种比赛的冠军,下次遇到相同类型的比赛时,直接让上次的冠军参赛。
查询计划缓存会在以下情况下被清除或更新:集合发生显著变化、索引被重建、添加或删除索引,或者显式清除计划缓存。服务器重启后,缓存也会丢失。
在设计复合索引时,字段的排序方向是一个经常被忽视但却很重要的因素。当我们需要基于多个字段进行排序时,索引字段的方向必须与查询的排序需求匹配。
考虑这样一个场景:我们需要按年龄从小到大,用户名从Z到A的顺序排列用户。如果我们有一个{"age": 1, "username": 1}的索引(两个字段都是升序),它对于这种混合方向的排序就不够理想。
为了优化这种查询,我们需要创建一个方向匹配的索引:
|> db.users.createIndex({"age": 1, "username": -1})
这个索引的结构是:年龄按升序排列,在每个年龄组内,用户名按降序排列。这样就能完美支持我们的排序需求。
有趣的是,索引的「逆序」版本具有相同的效果。也就是说,{"age": 1, "username": -1}索引支持的查询,{"age": -1, "username": 1}索引也能支持,只是遍历方向相反。
对于单字段排序,索引方向并不重要,因为MongoDB可以双向遍历索引。只有在多字段排序时,方向匹配才变得关键。
覆盖查询是索引优化中的一个高级概念,它能够显著提升查询性能。当查询所需的所有字段都包含在索引中时,MongoDB就无需访问实际的文档,直接从索引中获取所有需要的数据。 这种优化的效果是惊人的。想象一下,我们只需要用户的年龄和用户名信息:
|> db.users.find({"age": 25}, {"_id": 0, "age": 1, "username": 1})
如果我们有一个{"age": 1, "username": 1}的索引,这个查询就是一个覆盖查询。MongoDB可以完全从索引中获取结果,而无需访问任何文档。这不仅减少了磁盘I/O,还大大降低了内存使用。
要创建覆盖查询,需要注意以下几点:查询的投影必须排除_id字段(除非它也在索引中),查询条件和返回字段都必须包含在索引中。
复合索引有一个非常有用的特性:它们可以充当多个索引的角色。如果我们有一个{"a": 1, "b": 1, "c": 1}的复合索引,实际上我们免费获得了{"a": 1}和{"a": 1, "b": 1}这两个索引的效果。
这个特性被称为「索引前缀」原理。MongoDB可以使用复合索引的任何前缀部分来优化查询。但要注意,只有前缀才有效,{"b": 1}或{"a": 1, "c": 1}这样的组合无法利用我们的复合索引。
这种特性在索引规划时非常有价值。我们可以通过精心设计复合索引来支持多种查询模式,而不需要创建大量的单独索引。这既节省了存储空间,也减少了写操作的开销。

并非所有查询操作符都能够高效利用索引。深入理解各类操作符对索引的利用机制,对于实现查询性能优化至关重要。以下我们对常见操作符的索引适配模式进行专业分析。
否定型操作符通常对索引利用效率极低。例如,使用$ne(不等于)操作符时,MongoDB需要遍历索引中所有非目标值的条目,这实际上接近于全索引扫描,无法发挥索引的高效过滤作用。
考虑这个查询:
|> db.products.find({"category": {"$ne": "electronics"}})
即使category字段有索引,这个查询也会检查索引中除了「electronics」之外的所有条目。如果我们的商品数据中大部分都不是电子产品,那么这个查询就需要处理绝大部分的索引数据。
$not操作符的情况稍好一些,它有时候能够利用索引,特别是在处理简单范围查询的反向时。比如{"price": {"$lt": 100}}的反向{"price": {"$not": {"$lt": 100}}}可以转换为{"price": {"$gte": 100}}。但对于复杂的查询,$not通常会退回到集合扫描。
$nin(不在列表中)操作符总是使用集合扫描,无论是否有合适的索引。
如果必须使用否定操作符,尝试添加其他能够利用索引的条件来缩小搜索范围,然后再应用否定条件。
范围查询是复合索引设计的关键考虑因素。在复合索引中,字段的顺序直接影响范围查询的效率。精确匹配的字段应该放在前面,范围字段应该放在后面。 让我们通过例子来理解这个原理:
|# 高效的索引设计 > db.orders.createIndex({"status": 1, "created_date": 1}) > db.orders.find({ "status": "completed", "created_date": {"$gte": "2024-01-01", "$lte": "2024-12-31"} })
这个查询首先通过精确的状态匹配缩小范围,然后在结果中应用日期范围过滤。索引能够高效地支持这种查询模式。 相反,如果我们将字段顺序颠倒:
|# 效率较低的索引设计 > db.orders.createIndex({"created_date": 1, "status": 1})
这种情况下,查询必须扫描指定日期范围内的所有记录,然后逐一检查状态字段,效率显著降低。
$or操作符是MongoDB中唯一能够同时使用多个索引的操作符。每个$or子句都可以使用自己的索引,然后MongoDB会合并结果。
|> db.users.find({"$or": [ {"age": 25}, {"city": "北京"} ]})
如果我们有age和city的独立索引,MongoDB会并行执行两个索引扫描,然后合并结果并去重。这种机制比单一索引更灵活,但通常比使用单个复合索引效率低。
因此,当可能的时候,尽量使用$in操作符而不是$or。$in可以在单个索引扫描中处理多个值,而$or需要多次扫描和结果合并。
|# 更高效的方式 > db.users.find({"age": {"$in": [25, 30, 35]}}) # 而不是 > db.users.find({"$or": [ {"age": 25}, {"age": 30}, {"age": 35} ]})
MongoDB凭借其灵活的文档模型,支持在嵌套对象和数组字段上构建索引。这一特性为处理复杂数据结构时的高效查询提供了专业的优化手段。

对于文档中的嵌套对象字段,可以通过点(.)操作符指定字段路径,精确创建对应字段的索引:
|# 假设我们有用户位置信息的文档结构 { "username": "张三", "location": { "city": "上海", "district": "浦东新区", "coordinates": [121.5, 31.2] } } # 为嵌套字段创建索引 > db.users.createIndex({"location.city": 1})
这种索引可以高效地支持基于城市的查询。需要注意的是,为整个嵌套对象创建索引(比如{"location": 1})与为其子字段创建索引完全不同。整个对象的索引只能用于查询完整的嵌套对象,而不能优化子字段查询。
数组索引在MongoDB中有着与普通字段索引不同的特殊机制。假设某字段的值为数组,当我们对这个数组字段创建索引时,MongoDB不会只为整个数组本身建立一个索引条目,而是会为数组中的每一个元素分别生成一个独立的索引项。
举例来说,如果文档中的 tags 字段是 ["数据库", "NoSQL", "性能优化"],那么为 tags 创建索引后,实际索引表中会各自记录针对 数据库、NoSQL 和 性能优化 这三项的索引条目。这种方式使得任何针对数组中任一元素的查询都可以通过索引高效定位到相关文档。
|# 博客文章的标签数组 { "title": "MongoDB索引优化指南", "tags": ["数据库", "NoSQL", "性能优化", "MongoDB"] } # 为标签数组创建索引 > db.posts.createIndex({"tags": 1})
这个索引会为每个标签值创建独立的索引条目,使我们能够高效地查询包含特定标签的文章。但这也意味着数组索引比普通索引消耗更多的存储空间,并且写操作的开销更大。
MongoDB不允许在单个复合索引中包含多个数组字段。这是为了避免索引条目数量的指数级增长。
一旦某个字段包含数组值,相应的索引就会被标记为「多键索引」。即使后来删除了所有的数组文档,这个标记也不会自动清除。多键索引在某些操作中需要额外的去重步骤,因此性能略低于普通索引。 要将多键索引恢复为普通索引,唯一的方法是删除并重新创建该索引。
索引的性能在很大程度上依赖于其所覆盖字段的数据分布特性,即「基数」(Cardinality)。所谓高基数字段,指的是取值唯一性高、重复度低的字段,例如用户ID、电子邮件地址等;相比之下,性别、布尔标记等通常为低基数字段。 一般来说,高基数字段更适合作为索引键,因为它们能够显著缩小查询结果集,提升检索效率。而低基数字段因其值的分布过于集中,仅能带来有限的过滤效果。
在复合索引的设计中,建议优先将高基数字段置于索引的前部,将低基数字段放在后部,以最大化索引的筛选能力和利用率。
例如,在用户管理系统中,若需检索特定城市中特定年龄的女性用户,应优先考虑如下索引顺序:
|# 好的索引设计:高基数字段在前 > db.users.createIndex({"email": 1, "age": 1, "gender": 1}) # 不太理想的设计:低基数字段在前 > db.users.createIndex({"gender": 1, "age": 1, "email": 1})
第一种设计能够通过邮箱字段快速定位到特定用户,然后检查年龄和性别。而第二种设计需要扫描大量具有相同性别的用户记录。

explain命令是MongoDB性能分析与调优过程中不可或缺的工具。其作用在于以结构化和详细的方式揭示查询的执行计划、资源消耗及优化空间,相当于数据库操作的“执行计划剖析器”。对explain输出内容的系统性解读,是开展查询性能优化、定位瓶颈的基础能力。
通过explain修饰符执行查询时,MongoDB会返回丰富的执行统计数据。尽管整体输出信息量较大,但下述核心指标对性能分析尤为重要:
|> db.users.find({"age": 25}).explain("executionStats")
在返回的结果中,最重要的字段包括:
一个高效的查询应该具备以下特征:
|{ "executionTimeMillis": 5, // 执行时间短 "totalKeysExamined": 100, // 检查的索引键数量 "totalDocsExamined": 100, // 检查的文档数量 "nReturned": 100, // 返回的结果数量 "stage": "IXSCAN" // 使用索引扫描 }
这个例子显示了一个「完美」的查询:检查的索引键、文档数量和返回的结果数量完全匹配,并且使用了索引扫描。这意味着MongoDB精确地定位到了所需的数据,没有浪费任何查询资源。 当查询性能不佳时,explain输出会提供明确的线索:
"stage": "COLLSCAN",说明查询没有使用索引。这通常是性能问题的主要原因。totalDocsExamined远大于nReturned,说明查询检查了大量不相关的文档。这可能意味着索引设计不当或者查询条件选择性太低。"stage": "SORT",说明MongoDB在内存中进行排序操作,这会显著影响性能。在处理复杂查询时,explain的输出通常呈现多层次的执行计划树结构:
|> db.orders.find({ "status": "completed", "amount": {"$gte": 1000} }).sort({"created_date": -1}).explain("executionStats")
这种查询可能产生如下的执行计划:
|FETCH └── IXSCAN (status_1_amount_1_created_date_-1)
或者:
|SORT └── FETCH └── IXSCAN (status_1_amount_1)
第一种计划更优,因为它能够利用索引的排序特性避免内存排序。第二种计划需要额外的排序步骤,性能相对较差。
有时候,我们可能发现MongoDB选择了次优的索引。在这种情况下,我们可以使用hint来强制指定索引:
|> db.users.find({"age": 25}).hint({"age": 1})
但是,hint应该被谨慎使用。强制使用不合适的索引可能导致性能更差。在生产环境中使用hint之前,务必通过explain验证性能改善。
hint只是临时的解决方案。如果MongoDB的查询优化器持续选择错误的索引,应该考虑重新设计索引结构或者优化查询语句。
尽管索引在大多数场景下能够显著提升查询效率,但在某些特定情况下使用索引反而可能带来负面影响。准确理解和评估索引的适用性,是数据库架构与性能优化中的重要专业能力。
当查询操作需要返回集合中绝大部分文档时,采用索引往往并不具备性能优势。原因在于每次索引查找涉及两次磁盘IO(先定位索引,再检索实际文档),而全表扫描则是一趟顺序读取,顺序读取的磁盘访问成本通常更低。因此,对于分析型或批量处理型查询(例如某月订单报表,命中90%以上文档),应优先考虑集合扫描。
针对文档数量较少的小型集合,索引维护和查询所带来的开销可能大于收益。例如,只有几百条数据的集合,直接全表扫描通常能够获得更佳的响应时间。在这种情况下,额外维护索引结构不仅无助于性能,反而可能造成资源浪费。
在以写操作为主的应用中,索引会显著增加写入负担。每次插入、更新或删除都需同步更新所有相关索引,带来额外的CPU及IO开销。因此,对于高并发写入、批量入库等场景,应仔细权衡查询加速与写入性能的关系,合理控制索引数量,以避免影响整体系统吞吐量。
这个决策的经验法则是:如果查询返回超过30%的集合数据,索引可能不会提供显著的性能优势。但这个阈值会根据具体的硬件环境、文档大小和索引复杂度而变化,实际范围可能在2%到60%之间。

MongoDB提供了几种特殊类型的索引来满足不同的业务需求。
唯一索引是保障数据唯一性和完整性的重要机制。它要求某个字段或字段组合在集合中每个文档的取值都是唯一的,绝不允许出现重复。
例如,在用户集合中为username字段创建唯一索引,可以确保所有用户的用户名都不重复,避免同名冲突。这样,不仅可以防止应用层由于并发等原因插入重复数据,还能在插入或更新操作时,直接由数据库层面抛出重复错误,提升数据安全性。
此外,唯一索引同样具备普通索引的查询加速作用,能优化以该字段为条件的检索性能。因此,唯一索引既保证了业务数据规则的一致性,又提高了相关查询的效率,常用于诸如用户名、邮箱、手机号等要求全局唯一的业务字段。
|# 为用户名字段创建唯一索引 > db.users.createIndex({"username": 1}, {"unique": true})
唯一索引的工作机制类似于关系型数据库中的唯一约束。当我们尝试插入重复的值时,MongoDB会抛出错误:
|> db.users.insertOne({"username": "张三", "email": "zhangsan@example.com"}) > db.users.insertOne({"username": "张三", "email": "zhangsan2@example.com"}) # 第二次插入会失败,因为用户名重复
需要注意的是,唯一索引将null值也视为一个唯一值。这意味着如果多个文档的某个字段都缺失(即为null),唯一索引会阻止这种情况。
复合唯一索引是一种能够同时作用于多个字段的唯一性约束。与单字段唯一索引不同,复合唯一索引要求指定的多个字段组合起来的取值在整个集合中必须是唯一的。也就是说,只有当所有被索引字段的值完全相同时,MongoDB 才会认为发生了重复并阻止插入。
举例来说,如果我们在 username 和 company 两个字段上创建复合唯一索引,那么只有在同一个公司下用户名完全相同时才会被视为冲突;而不同公司的相同用户名则不会违反唯一性约束。这对于需要支持多租户(如同一套系统服务于多家公司)场景尤其实用,可以在保证数据隔离的同时实现合理的数据唯一性校验。
复合唯一索引的实际应用包括:防止同一单位中用户名重复、在某时间范围内限制操作重复发生、按业务逻辑组合字段实现复合唯一约束等。需要注意的是,复合唯一索引的字段顺序很重要,不同顺序会产生不同的索引结构和查询优化效果。
例如,下述创建语句将同时约束 username 和 company 字段的组合唯一:
|> db.users.createIndex({"username": 1, "company": 1}, {"unique": true})
这个索引允许同一个用户名在不同公司中存在,但在同一公司内用户名必须唯一。这种设计在多租户应用中特别有用。
部分索引是MongoDB提供的一种非常灵活的索引机制。与普通索引不同,部分索引允许我们只为集合中满足特定条件的部分文档创建索引条目。这样,只有在匹配partialFilterExpression指定条件的文档才会被纳入该索引,而不满足条件的文档则不会占用索引空间。
使用部分索引有几个主要优点:
比如,通常我们可能只要求已验证的邮箱是唯一的,而未验证阶段的邮箱可以重复,这时就可通过部分索引来实现。 此外,部分索引对于需要对集合的某一“子集”进行频繁查询优化、或根据状态字段筛选数据的场景都非常实用,使数据库在保证查询效率的同时,又能最大限度地利用资源。
|# 只为已验证邮箱的用户创建唯一索引 > db.users.createIndex( {"email": 1}, { "unique": true, "partialFilterExpression": {"email_verified": true} } )
这个索引只会为那些email_verified字段为true的文档创建索引条目。这样既确保了已验证邮箱的唯一性,又允许多个用户暂时拥有相同的未验证邮箱地址。
部分索引特别适用于以下场景:可选字段的唯一性约束、基于状态的查询优化、大型集合中的子集查询等等。
使用部分索引时要注意,同一个查询可能因为是否使用索引而返回不同的结果。这是因为部分索引不包含所有文档的信息。
有效的索引管理是维护数据库性能的关键环节。MongoDB提供了丰富的工具来帮助我们监控和管理索引。
我们可以使用getIndexes方法来查看集合中的所有索引:
|> db.users.getIndexes() [ { "v": 2, "key": {"_id": 1}, "name": "_id_" }, { "v": 2, "key": {"username": 1}, "name": "username_1", "unique": true }, { "v": 2,
每个索引都有一个唯一的名称。默认情况下,MongoDB会根据字段名和排序方向生成索引名称,比如username_1表示username字段的升序索引。
当索引包含多个字段时,自动生成的名称可能会很长。我们可以指定自定义的索引名称:
|> db.orders.createIndex( {"customer_id": 1, "order_date": 1, "status": 1}, {"name": "customer_orders_idx"} )
自定义名称不仅更简洁,还能更好地表达索引的用途。
当索引不再需要或者需要修改时,我们同样也可以删除它:
|> db.users.dropIndex("username_1")
需要注意的是,删除索引会影响依赖该索引的查询性能。在生产环境中删除索引之前,应该仔细评估对应用性能的影响。 对于大型集合,索引的创建和删除都是资源密集型操作。MongoDB 4.2引入了混合索引构建机制,在索引构建过程中只在开始和结束时持有排他锁,大大减少了对并发操作的影响。
在生产环境中,索引的创建与管理需经过充分的评估与规划。索引的构建过程会占用大量的CPU和内存资源,可能对数据库的正常读写操作产生显著影响,尤其是在数据量较大的集合上。 针对已有大量数据的集合,建议选择业务低峰时段进行索引创建,以尽量减少对系统性能的影响。若条件允许,建议在数据量较小时优先建立索引,再进行后续数据批量导入,这样能够更高效地完成索引构建。需要特别关注的是,在高并发和大规模数据环境下,索引操作的性能影响和资源消耗不可忽视。
此外,对索引实际使用情况的持续监控也是索引优化的重要环节。MongoDB 提供了 $indexStats 聚合管道阶段,用于详细追踪每个索引的使用频率和访问模式:
|> db.users.aggregate([{"$indexStats": {}}])
这个命令会显示每个索引的访问次数,帮助我们识别哪些索引被频繁使用,哪些索引可能是多余的。
定期审查和优化索引是数据库维护的重要组成部分。删除不必要的索引不仅能节省存储空间,还能提升写入操作的性能。同时,根据应用查询模式的变化,及时创建新的索引或调整现有索引,确保数据库始终保持最佳性能状态。
首先,让我们创建一个包含产品信息的集合:
|# 创建产品集合并插入测试数据 > for (i=0; i<5000; i++) { db.products.insertOne({ "name": "product_"+i, "category": ["电子产品", "家居用品", "服装"][Math.floor(Math.random()*3)], "price": Math.floor(Math.random()*1000)+10, "stock": Math.floor(Math.random
现在,请为 price 字段创建一个升序索引,然后查询价格在100-200元之间的产品,并使用explain命令查看查询性能。
|# 创建索引 > db.products.createIndex({"price": 1}) # 查询价格在100-200之间的产品 > db.products.find({"price": {"$gte": 100, "$lte": 200}}) # 查看查询性能 > db.products.find({"price": {"$gte": 100, "$lte": 200}}).explain("executionStats")
使用上面的产品集合,为 category 和 price 字段创建一个复合索引,然后查询电子产品中价格低于500元的产品。
|# 创建复合索引 > db.products.createIndex({"category": 1, "price": 1}) # 查询电子产品中价格低于500元的产品 > db.products.find({ "category": "电子产品", "price": {"$lt": 500} })
创建一个用户集合,包含不同城市的用户信息:
|# 创建用户集合并插入测试数据 > for (i=0; i<3000; i++) { db.users.insertOne({ "username": "user_"+i, "city": ["北京", "上海", "广州", "深圳"][Math.floor(Math.random()*4)], "age": Math.floor(Math.random()*50)+18, "active": Math.random()
为 active 字段(布尔类型)创建索引,然后查询活跃用户,使用explain查看索引的使用情况。
|# 为active字段创建索引 > db.users.createIndex({"active": 1}) # 查询活跃用户 > db.users.find({"active": true}) # 查看查询性能 > db.users.find({"active": true}).explain("executionStats")
创建一个学生成绩集合,要求学号唯一:
|# 创建学生成绩集合 > db.scores.insertMany([ {"student_id": "2024001", "name": "张三", "course": "数学", "score": 85}, {"student_id": "2024002", "name": "李四", "course": "数学", "score": 92}, {"student_id": "2024003",
为 student_id 字段创建唯一索引,然后尝试插入重复的学号。
|# 创建唯一索引 > db.scores.createIndex({"student_id": 1}, {"unique": true}) # 尝试插入重复学号(这会失败) > db.scores.insertOne({"student_id": "2024001", "name": "赵六", "course": "英语", "score": 88})
使用上面的用户集合,为 city 字段创建部分索引,只索引活跃用户:
|# 确保用户集合存在活跃用户数据 > db.users.find({"active": true}).limit(1)
创建部分索引并查询北京的活跃用户。
|# 创建部分索引,只为活跃用户建立索引 > db.users.createIndex( {"city": 1}, {"partialFilterExpression": {"active": true}} ) # 查询北京的活跃用户 > db.users.find({"city": "北京", "active": true})