索引简介

什么是索引

索引是 MongoDB 中优化查询性能的核心手段,通过创建索引可以大幅幅减少查询时的文档扫描量(从全集合扫描变为索引扫描)

本质上,索引是一种数据结构(类似书籍的目录),存储集合中某个 / 某些字段的值及其对应文档的位置信息,用于快速定位满足查询条件的文档。

索引并非没有代价,索引会占用额外存储空间,且写入操作(插入 / 更新 / 删除)会触发索引更新,可能降低写入性能。因此需平衡查询效率和写入成本,避免过度索引。

默认情况下,索引的创建是为每个集合自动为 _id 字段创建唯一索引(_id_),_id 索引可防止客户端插入两个具有相同 _id 字段值的文档。无法删除此索引。

  • 分片群集中,如果您不使用 _id 字段作为分分片键,那么您的应用程序必须确保 _id 字段中的值具有唯一性。为此,您可以使用一个具有自动生成的 ObjectId 的字段。

MongoDB 索引使用 B-Tree 数据结构(MySQL 是 B+Tree)

索引类型概述

MongoDB 支持多种索引类型,适用于不同查询场景:

索引类型 说明 适用场景
单字段索引 基于单个字段创建的索引({字段: 1} 升序,{字段: -1} 降序) 单字段条件查询(如 where age > 20
复合索引 基于多个字段创建的索引(如 {name: 1, age: -1} 多字段组合查询(如 where name="A" and age>20
唯一索引 确保索引字段的值唯一(通过 {unique: true} 选项) 防止重复数据(如用户邮箱、手机号)
多键索引 自动为数组字段的每个元素创建索引 数组字段查询(如 where tags = "java"
地理空间索引 用于存储和查询地理坐标数据(如 2d2dsphere 位置查询(如 “查找附近的商店”)
文本索引 用于全文检索,支持对字符串字段进行分词和模糊匹配 关键词搜索(如 where $text: {$search: "mongodb"}
TTL 索引 自动删除过期文档(通过 expireAfterSeconds 选项指定过期时间) 临时数据(如会话、日志)自动清理
哈希索引 对字段值进行哈希运算后创建索引,支持等值查询但不支持范围查询 分片集群中基于哈希值均匀分布数据
聚集索引 集群化索引指定集群化集合存储数据的顺序。使用集群化索引创建的集合称为集群化集合。

单字段索引

MongoDB 支持在文档的单个字段上创建用户定义的升序索引或降序索引,称为单字段索引(Single Field Index)

对于单个字段索引和排序操作,索引键的排序顺序(即升序或降序)并不重要,因为 MongoDB 可以在任何方向上遍历索引

image-20251025181706958

可以对整个嵌入式文档创建索引。但是,只有指定整个嵌入式文档的查询才会使用索引。对文档中的特定字段的查询不使用该索引。

什么意思,对整个嵌入式文档创建的索引,只有当查询条件是 “完全匹配这个嵌入式文档的所有字段” 时才会生效;如果只查询嵌入式文档中的某个具体字段,这个索引不会被使用

举个例子理解:

假设集合 users 中有文档:

1
2
3
4
{
name: "Alice",
info: { age: 25, city: "Beijing" } // 嵌入式文档
}

如果为 info 字段创建整个嵌入式文档的索引

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

会使用该索引的查询(完全匹配嵌入式文档):

1
2
// 查询 info 完全等于 { age: 25, city: "Beijing" } 的文档
db.users.find({ info: { age: 25, city: "Beijing" } });

此时 MongoDB 会使用这个索引,因为查询条件是 “对整个嵌入式文档的完全匹配”。

不会使用该索引的查询(只查嵌入式文档的某个字段):

1
2
3
4
5
// 只查询 info.age = 25 的文档
db.users.find({ "info.age": 25 });

// 只查询 info.city = "Beijing" 的文档
db.users.find({ "info.city": "Beijing" });

此时这个索引不会被使用,因为查询条件不是 “对整个嵌入式文档的完全匹配”,而是只针对其中的某个字段。如果需要优化这类查询,应该为 info.ageinfo.city 单独创建单字段索引(如 db.users.createIndex({ "info.age": 1 }))。

对嵌入式文档创建 “整体索引” 的场景非常有限,仅适用于需要 “完全匹配整个嵌套结构” 的查询。

复合索引

什么是复合索引

MongoDB 支持多个字段的自定义索引,即复合索引(Compound Index)

复合索引从集合中每个文档的多个字段值收集数据并对其进行排序。可以使用复合索引查询索引的第一个字段或任何前缀字段。

复合索引中字段的顺序非常重要。由复合索引创建的 B-tree 按照索引指定的字段顺序存储排序后的数据。

而且 MongoDB 可以在任一方向遍历复合索引。但是复合索引不支持排序顺序与索引不匹配或与索引相反的查询。因此,{ score: -1, username: 1 } 索引支持先按 score 值升序进行排序,然后再按 username 值升序进行排序

例如,下图显示了一个复合索引,其中文档首先按 userid 排序并以升序(按字母顺序)排列。然后,每个 useridscores 按降序排序:

image-20251025181838814

要创建复合索引,请使用 db.collection.createIndex() 方法:

1
2
3
4
5
6
db.<collection>.createIndex( {
<field1>: <sortOrder>,
<field2>: <sortOrder>,
...
<fieldN>: <sortOrder>
} )

在单个复合索引中最多可以指定 32 个字段。

最左前缀原则

索引前缀是索引字段的起始子集。复合索引支持对索引前缀中包含的所有字段进行查询。

复合索引的生效依赖于 “最左前缀”,即查询条件必须包含索引中从最左侧开始的连续字段,才能有效利用该索引。如果跳过左侧字段直接查询右侧字段,索引将无法生效。

以复合索引 { "item": 1, "location": 1, "stock": 1 } 为例:

哪些查询能使用该索引?

  • 包含最左前缀的查询:

    索引的 “前缀” 是指从最左侧字段开始的连续组合(如{item:1}{item:1, location:1})。

    • 查询 item(单独使用最左字段):能使用索引。
    • 查询 item + location(最左两个字段):能使用索引。
    • 查询 item + location + stock(全部字段):能使用索引。
    • 查询 item + stock(跳过中间的 location):能使用索引,但效率较低(因为需要先匹配所有 item 对应的文档,再过滤 stock,类似 “先大范围匹配再筛选”)。
  • 哪些查询不能使用该索引?

    • 不包含最左前缀的查询:

      如果查询条件中没有最左侧的item字段,无论组合其他字段,都无法使用该复合索引。

      • 单独查询 locationstock:不能使用索引(无 item 前缀)。
      • 组合查询 location + stock:不能使用索引(无 item 前缀)。

为什么会这样

复合索引的结构类似 “层级目录”:先按 item 排序,相同 item 再按 location 排序,相同 location 再按 stock 排序。

  • 若查询包含 item,相当于定位到目录的第一级,后续字段可在该范围内继续查找。
  • 若查询没有 item,相当于不知道第一级目录,无法直接定位到后续字段的范围,只能全集合扫描(COLLSCAN)。

所以说,设计复合索引时,需将查询频率最高、区分度最高的字段放在最左侧,确保多数查询能命中索引前缀。如果需要频繁查询 locationstock 而不带 item,则需要为这些字段单独创建索引(如 {location:1}{location:1, stock:1})。

稀疏复合索引的行为规则

这种情况看似少见实际上经常出现,也就是,当复合索引中混合了普通索引(升序 / 降序)与特殊类型索引(地理空间索引、文本索引)时,索引会如何决定对哪些文档进行索引(即哪些文档会被纳入该复合索引中)。

什么是稀疏索引(Sparse Index)

稀疏索引的特点是:仅对包含索引字段的文档建立索引,不包含该字段的文档会被排除在索引之外(普通非稀疏索引会为所有文档建立索引,即使字段不存在,也会存储 null 或空值)。

而当复合索引中包含多种类型的稀疏索引时,索引的 “稀疏性” 规则会由这些类型共同决定。

这么说太反人类了,也就是,为某个字段创建稀疏索引时,数据库只会为明确包含该字段的文档生成索引条目;对于不包含该字段的文档,不会为其创建索引条目(即这些文档不会被纳入该索引中)。

假设集合 users 中有以下 3 条文档:

1
2
3
4
5
6
7
8
// 文档1:包含 age 字段
{ name: "Alice", age: 25 }

// 文档2:包含 age 字段
{ name: "Bob", age: 30 }

// 文档3:不包含 age 字段
{ name: "Charlie" }

如果为 age 字段创建稀疏索引

1
db.users.createIndex({ age: 1 }, { sparse: true })

此时,该索引只会包含前两条文档(因为它们有 age 字段),第三条文档(无 age)不会被加入索引。

如果创建的是非稀疏索引(默认不稀疏):

1
db.users.createIndex({ age: 1 }) // 非稀疏,默认行为

数据库会为所有文档创建索引条目,即使文档不包含 age 字段,也会用 null 作为该字段的值存入索引。上述第三条文档会以 age: null 的形式出现在索引中。

稀疏索引能够减少索引体积:对于大量不包含该字段的文档,稀疏索引可以避免存储无效的索引条目(如 null),节省磁盘空间。

但是稀疏索引可能导致 count() 等操作结果不准确(因为未包含无字段的文档),需结合业务场景使用。

解读复合索引的行为

仅包含升序 / 降序索引(Ascending/Descending indexes)

  • 行为:仅对至少包含一个键值的文档进行索引。

  • 解释:

    复合索引由普通升序(1)或降序(-1)字段组成(如{a:1, b:-1})。

    只要文档包含索引中的至少一个字段(无论其他字段是否存在),就会被纳入该复合索引。

    例:索引{a:1, b:-1}会索引以下文档:

    • {a:1}(含 a)、{b:2}(含 b)、{a:1, b:2}(含两者)。

包含升序 / 降序索引 + 地理空间索引(Geospatial indexes)

  • 行为:仅当文档包含一个地理空间字段的值时,才为文档编制索引;不在升序或降序索引中单独索引文档。

  • 解释:

    地理空间索引(如2d、2dsphere)是特殊的稀疏索引,优先级高于普通索引。

    只有当文档明确包含地理空间字段的值(如location: {type: "Point", coordinates: [x,y]})时,才会被纳入该复合索引,无论普通字段(升序 / 降序)是否存在。

    例:索引{a:1, location: "2dsphere"}中:

    • {location: ...}{a:1, location: ...} 会被索引;
    • {a:1}(无地理空间字段)不会被索引。

包含升序 / 降序索引 + 文本索引(Text indexes)

  • 行为:仅当文档与一个文本字段匹配时,才为文档编制索引;不在升序或降序索引中单独索引文档。

  • 解释:

    文本索引(用于全文搜索)也是特殊稀疏索引,优先级高于普通索引。

    只有当文档包含文本索引字段且字段值符合文本索引规则(如非空字符串)时,才会被纳入该复合索引,普通字段是否存在不影响。

    例:索引{a:1, content: "text"}中:

    • {content: "hello"}{a:1, content: "hello"} 会被索引;
    • {a:1}(无符合条件的文本字段)不会被索引。

复合索引中若包含特殊类型索引(地理空间、文本),则特殊索引的 “稀疏规则” 会主导整个复合索引的文档匹配逻辑

  • 普通升序 / 降序索引单独存在时,只要文档含任一索引字段就会被索引;
  • 一旦混入地理空间或文本索引,只有满足这些特殊索引字段条件的文档,才会被纳入复合索引,普通字段的存在与否不再单独决定索引范围。

多键索引

什么是多键索引

多键索引是 MongoDB 为数组类型字段专门设计的索引类型。它会遍历数组中的每个元素,为每个元素单独创建索引条目,从而让数组字段的查询(如 “包含某个元素”“数组元素满足条件”)更高效。

无需显式指定多键类型。对包含数组值的字段创建索引时,MongoDB 会自动将该索引设为多键索引。

举个例子:

假设集合 students 中有文档:

1
2
3
4
{ 
name: "Alice",
scores: [85, 92, 78] // 数组字段
}

scores 字段创建多键索引后,MongoDB 会为数组中的每个元素(859278)分别生成索引条目,相当于把一条文档拆成多条 “虚拟记录” 来索引。

无论是存储字符串、数字的 “简单数组”(如 [1, 2, 3]),还是存储嵌套文档的 “复杂数组”(如 [{a:1}, {b:2}]),多键索引都能生效。

而且如果数组中包含重复的同一值(如 [1, 1, 2]),多键索引只会为该值创建一个索引条目,避免冗余。

多键索引可以和其他类型的索引组合(如复合索引)。例如,创建 { name: 1, scores: 1 } 的复合索引,既可以优化 name 字段的查询,也能优化 name + scores 组合的数组查询。

要创建多键索引,请使用以下原型:

1
db.<collection>.createIndex( { <arrayField>: <sortOrder> } )

下图显示了 addr.zip 字段的多键索引:

image-20251025183646148

如果您的应用程序频繁查询包含数组值的字段,则多键索引可提高这些查询的性能。典型场景包括:

  • 数组元素的等值查询:如 db.students.find({ scores: 92 })(查询 scores 包含 92 的学生)。
  • 数组元素的范围查询:如 db.students.find({ scores: { $gt: 90 } })(查询 scores 中存在大于 90 分的学生)。
  • 数组嵌套文档的查询:若数组存储的是嵌套文档(如 [{ subject: "Math", score: 85 }, ...]),多键索引也能优化这类查询(如 db.students.find({ "scores.subject": "Math" }))。

例如,students 集合中的文档包含 test_scores 字段:学生在整个学期中收到的测验成绩的数组。您会定期更新排名靠前学生的列表:至少有五项 test_scores 大于 90 的学生。

您可对 test_scores 字段创建索引,从而为此查询提高性能。由于 test_scores 包含数组值,因此 MongoDB 会将该索引存储为多键索引。

多键索引通过自动遍历数组元素创建索引条目,让数组查询从 “全集合扫描” 变为 “索引扫描”,大幅提升效率。使用时只需为数组字段创建普通索引,MongoDB 会自动处理多键逻辑,无需额外配置。

唯一多键索引

在 “唯一多键索引” 中,唯一性约束是跨不同文档的数组元素。只要不同文档的数组元素之间不重复,即使单个文档的数组内有重复元素,也不会违反唯一约束。

举个例子:

假设创建了 { tags: 1 } 的唯一多键索引,集合中有以下文档:

  • 文档 1:{ tags: ["mongodb", "database"] }

  • 文档 2:{ tags: ["database", "nosql"] }

    这是合法的,因为不同文档的数组元素没有完全重复的组合。

    但如果插入文档 3:{ tags: ["mongodb", "database"] },就会因数组元素与文档 1 重复而失败。

复合多键索引

复合多键索引的核心规则是:每个文档在复合索引中最多只能有一个数组类型的字段

不允许的情况

如果索引的多个字段都是数组,无法创建复合多键索引。

比如集合中有文档:

1
{ _id: 1, scores_spring: [8, 6], scores_fall: [5, 9] }

此时想创建 { scores_spring: 1, scores_fall: 1 } 的复合索引会失败,因为 scores_springscores_fall 都是数组。

允许的情况

只有一个字段是数组,其他字段是普通类型(非数组),可以创建复合多键索引。

比如集合中有文档:

1
2
{ _id: 1, scores_spring: [8, 6], scores_fall: 9 }
{ _id: 2, scores_spring: 6, scores_fall: [5, 7] }

此时可以创建 { scores_spring: 1, scores_fall: 1 } 的复合索引 —— 因为每个文档只有一个字段是数组(文档 1 的 scores_spring 是数组,文档 2 的 scores_fall 是数组)。

存在插入限制

创建复合多键索引后,不能插入两个字段都是数组的文档。

比如上述索引创建后,若插入 { scores_spring: [7, 8], scores_fall: [9, 10] },会因违反 “最多一个数组字段” 的规则而失败。

多键索引的排序限制

当对多键索引的字段进行排序时,MongoDB 可能需要额外的内存排序阶段,除非满足以下两个条件:

  1. 排序字段的索引边界是全范围:即索引扫描的范围是 [MinKey, MaxKey](可以理解为 “没有条件过滤,扫描整个索引”)。
  2. 多键索引字段的边界不与排序路径前缀重叠:这是比较技术的表述,简单来说就是排序的逻辑不能和多键索引的结构冲突。

如果不满足这两个条件,排序会在内存中进行,当数据量大时可能影响性能。

多键索引与分片键

  • 直接限制:多键索引不能直接作为分片键(分片键用于将数据分布到不同分片,多键索引的数组结构会导致分片逻辑混乱)。

  • 例外情况:如果分片键是复合索引的前缀,且后续的非分片键字段是数组(即复合索引包含多键字段),那么这个复合索引可以是 “复合多键索引”。

    例如,分片键是{ region: 1 }(非数组),复合索引是{ region: 1, tags: 1 }tags是数组),这种情况是允许的。

多键索引边界

什么是索引边界

索引边界是 MongoDB 执行查询时,利用索引扫描的值范围。当对索引字段指定多个查询条件时,MongoDB 会尝试合并这些条件的边界,生成更小的索引扫描范围—— 范围越小,查询速度越快、资源消耗越少。

MongoDB通过相交复合边界来组合边界。

边界也可以复合,组合成复合边界

多键索引的边界交集

边界交集指的是多个索引边界的重叠区域。例如:

  • 边界 A:[[3, Infinity]](表示大于等于 3 的所有值)
  • 边界 B:[[-Infinity, 6]](表示小于等于 6 的所有值)
  • 交集结果:[[3, 6]](仅扫描 3 到 6 之间的值)

对于数组类型的索引字段(多键索引),若查询中用 $elemMatch 连接多个条件,MongoDB 会对多键索引的边界做交集运算,进一步缩小扫描范围。

那么含 $elemMatch 的查询是怎么样的,以 students 集合为例,我们一步步看这个优化过程:

  1. 准备数据:插入包含 name 和数组字段 grades 的文档

    1
    2
    3
    4
    db.students.insertMany([
    { _id: 1, name: "Shawn", grades: [ 70, 85 ] },
    { _id: 2, name: "Elena", grades: [ 92, 84 ] }
    ])
  2. 创建多键索引:对 grades 数组创建升序多键索引

    1
    db.students.createIndex({ grades: 1 })
  3. 执行带 $elemMatch 的查询

    1
    db.students.find({ grades: { $elemMatch: { $gte: 90, $lte: 99 } } })
    • 这个查询的意图是:找到 grades 数组中 至少有一个元素同时满足 “≥90 且 ≤99”的文档。
    • 拆解两个条件的边界:
      • $gte: 90 的边界是 [[90, Infinity]](≥90 的所有值);
      • $lte: 99 的边界是 [[-Infinity, 99]](≤99 的所有值);
    • 因为用 $elemMatch 连接了条件,MongoDB 会计算边界交集,最终扫描范围缩小为 [[90, 99]],只需要扫描 90 到 99 之间的索引值,性能更优。

同样的情况下,不含 $elemMatch 的查询,如果查询写成这样:

1
db.students.find({ grades: { $gte: 90, $lte: 99 } })

它的逻辑是:grades 数组中至少有一个元素≥90,且至少有一个元素≤99(两个条件可以由不同元素满足)。

此时,MongoDB 无法做边界交集,会选择以下两个边界中的一个来扫描:

  • [[90, Infinity]]
  • [[-Infinity, 99]]

这种情况下,索引扫描的范围更大,查询性能不如用 $elemMatch 的场景。

也就是说,在使用 MongoDB 多键索引(数组字段的索引)时,若希望多个条件作用于数组的同一个元素,并让 MongoDB 优化索引扫描范围,建议使用 $elemMatch 来连接查询条件 —— 这样能触发 “边界交集”,缩小索引扫描范围,提升查询性能。

多键索引的复合边界

什么是复合边界

复合边界是针对复合索引(包含多个字段的索引)的边界组合策略。它将复合索引中多个字段的查询边界合并,让 MongoDB 无需单独计算每个字段的边界结果,从而减少查询时间、提升性能。

如果 MongoDB 无法组合多个边界,会优先按索引前导字段(复合索引中最左边的字段,最左优先原则)的边界来限制索引扫描范围。

非数组字段和数组字段的复合边界

survey 集合的案例为例,步骤如下:

  1. 准备数据:插入包含非数组字段 item 和数组字段 ratings 的文档

    1
    2
    3
    4
    db.survey.insertMany([
    { _id: 1, item: "ABC", ratings: [ 2, 9 ] },
    { _id: 2, item: "XYZ", ratings: [ 4, 3 ] }
    ])
  2. 创建复合多键索引:在 item(非数组)和 ratings(数组)上创建复合索引

    1
    db.survey.createIndex({ item: 1, ratings: 1 })
  3. 执行查询

    1
    db.survey.find({ item: "XYZ", ratings: { $gte: 3 } })
    • 拆解两个字段的查询边界:
      • item: "XYZ" 的边界是 [["XYZ", "XYZ"]](精确匹配 “XYZ”);
      • ratings: { $gte: 3 } 的边界是 [[3, Infinity]](≥3 的所有值);
    • MongoDB 会组合这两个边界,生成复合边界 { item: [["XYZ", "XYZ"]], ratings: [[3, Infinity]] },从而精准扫描满足两个条件的索引范围。

非数组字段和多个数组字段的复合边界

survey2 集合的案例为例,场景更复杂(索引包含一个非数组字段和多个数组子字段):

  1. 准备数据:插入包含非数组字段 item 和数组字段 ratings(包含 scoreby 两个子字段)的文档

    1
    2
    3
    4
    db.survey2.insertMany([
    { _id: 1, item: "ABC", ratings: [ { score: 2, by: "mn" }, { score: 9, by: "anon" } ] },
    { _id: 2, item: "XYZ", ratings: [ { score: 5, by: "anon" }, { score: 7, by: "wv" } ] }
    ])
  2. 创建复合多键索引:在 itemratings.scoreratings.by 上创建复合索引

    1
    db.survey2.createIndex({ item: 1, "ratings.score": 1, "ratings.by": 1 })
  3. 执行查询

    1
    2
    3
    4
    5
    db.survey2.find({
    item: "XYZ",
    "ratings.score": { $lte: 5 },
    "ratings.by": "anon"
    })
    • 拆解三个字段的查询边界:

      • item: "XYZ" 的边界是 [["XYZ", "XYZ"]]
      • ratings.score: { $lte: 5 } 的边界是 [[-Infinity, 5]]
      • ratings.by: "anon" 的边界是 [["anon", "anon"]]
    • MongoDB 会选择

      其中两个字段的边界与前导字段(item)复合

      (具体组合方式由查询谓词和索引键值决定,不保证固定顺序):

      • 组合方式 1:{ item: [["XYZ", "XYZ"]], "ratings.score": [[-Infinity, 5]], "ratings.by": [[MinKey, MaxKey]] }ratings.by 无范围限制);
      • 组合方式 2:{ item: [["XYZ", "XYZ"]], "ratings.score": [[MinKey, MaxKey]], "ratings.by": [["anon", "anon"]] }ratings.score 无范围限制);
    • 若要让 ratings.scoreratings.by 的边界同时复合(即要求数组元素同时满足 score ≤5by = "anon"),则必须使用 $elemMatch 操作符。。

哈希索引

什么是哈希索引

哈希索引不能是多键型。

哈希索引会对索引字段的值计算哈希值并存储,而非直接存储字段的原始值。它主要用于支持哈希分片(将数据根据字段哈希值分布到不同分片,实现分布式存储)。

哈希索引支持使用哈希分片键分片基于哈希的分片使用字段的哈希索引作为分片键,在分片集群中对数据分区。

哈希索引特别适合单调变化的字段(如 ObjectId、时间戳)作为分片键。因为这类字段若用范围分片,新数据会集中写入一个分片(上限为 MaxKey 的块),失去分布式写入的优势;而哈希索引能将数据均匀分散到多个分片,充分利用集群性能。

创建方式

单字段哈希索引

1
db.<collection>.createIndex({ <field>: "hashed" })

示例(对 orders 集合的 _id 字段创建哈希索引):

1
db.orders.createIndex({ _id: "hashed" })

复合哈希索引

复合哈希索引中仅一个字段为哈希类型,其余字段为普通排序(1 升序、-1 降序)。

语法:

1
2
3
4
5
6
db.<collection>.createIndex({
<field1>: <sortOrder>,
<field2>: "hashed",
<field3>: <sortOrder>,
...
})

示例(对 customers 集合的 name(升序)、address(哈希)、birthday(降序)创建复合哈希索引):

1
2
3
4
5
db.customers.createIndex({
name: 1,
address: "hashed",
birthday: -1
})

行为特性

  1. 哈希计算自动化:MongoDB 会自动为查询字段计算哈希值,应用程序无需手动计算。
  2. 嵌入式文档处理:哈希函数会 “折叠” 嵌入式文档,计算整个文档的哈希值。
  3. 浮点数处理:哈希索引会先将浮点数截断为 64 位整数再计算哈希。例如,2.32.22.9 会被映射到同一个哈希键(可能导致哈希冲突,影响查询性能)。

限制

  1. 数组字段不支持:无法在数组字段上创建哈希索引,也不能向哈希索引字段插入数组。
  2. 复合索引限制:若复合索引中任一字段是数组(或会触发多键索引),则整个索引不能包含哈希字段。
  3. 覆盖查询不支持:哈希索引无法实现 “覆盖查询”(即无法仅通过索引完成查询,必须回查文档)。
  4. 唯一约束不支持:不能在哈希索引上直接指定唯一约束,需通过额外的非哈希索引来实现唯一性。

哈希分片的延伸价值

当结合分片集群使用时,哈希索引的核心价值是数据均匀分布

  • 单调字段(如 _id)的哈希值是随机分散的,能避免范围分片的 “热点写入” 问题。
  • 适合读操作是 “单文档查询”(而非范围查询)的场景,因为范围查询无法利用哈希索引的性能优势。

文本索引(Text Indexes)

什么是文本索引

文本索引的特点:

  • 分词:MongoDB 在创建文本索引时会对字段内容进行分词处理,将文本分解成单词或术语(tokens)
  • 权重:可以为不同的字段指定不同的权重,以便在搜索时影响文档的相关性得分
  • 停用词:MongoDB 会忽略某些常用词(如 “the”、“and” 等),这些词被称为停用词。MongoDB 有一个内置的停用词列表,也可以自定义停用词列表
  • 语言支持:MongoDB 的文本索引支持多种语言的分词和搜索

这和 ES 的分词文档索引如出一辙

注意事项:

  • 文本索引不存储停止词和词干。这意味着它们不会影响索引的大小
  • 文本索引不能用于文本字段中的二进制数据
  • 文本索引不能用于数组字段中的字符串元素
  • $text 查询不能与 $$$ 运算符一起使用

通配符索引

简介通配符索引

MongoDB 支持在一个字段或一组字段上创建索引,以提高查询性能。

MongoDB 支持 “灵活模式”(集合中文档的字段名可能不一致),而通配符索引就是为了支持对任意未知字段的查询而设计的索引类型。它不像普通索引那样针对特定字段,而是可以覆盖集合中所有可能的字段(或通过配置覆盖指定范围的字段)。

通配符索引仅建议在 “字段未知或可能变更” 的场景下使用,因为它的性能不如针对 “特定字段” 的普通索引。典型适用场景包括:

  • 集合中文档的字段名因文档而异,需要支持对所有可能字段的查询;
  • 频繁查询子字段不一致的嵌入式文档(如不同文档的嵌入对象有不同子字段);
  • 需要覆盖 “具有共同特征但字段组合灵活” 的文档查询(可结合复合通配符索引实现)

通配符索引的行为特性

  1. 多索引支持:一个集合中可以创建多个通配符索引;
  2. 字段覆盖性:通配符索引可覆盖与集合中其他普通索引相同的字段;
  3. _id 字段默认省略:若要将 _id 包含在通配符索引中,需显式配置 { "_id": 1 }wildcardProjection 文档中;
  4. 稀疏索引特性:通配符索引是稀疏索引,仅包含 “具有索引字段” 的文档条目(即使字段值为空也会包含);
  5. 与文本索引不兼容:通配符索引不支持 $text 操作符的文本查询,也不能与通配符文本索引混用。

通配符索引的覆盖查询

“覆盖查询” 是指查询可以完全通过索引完成(无需回查文档),通配符索引支持覆盖查询的条件是:

  • 查询规划器选择了通配符索引来满足查询;
  • 查询谓词能精确指定通配符索引覆盖的某一个字段;
  • 查询投影明确排除 _id,且仅包含查询字段;
  • 指定的查询字段不是数组类型

示例

假设对 employees 集合创建通配符索引:

1
db.employees.createIndex({ "$**": 1 })

执行以下查询(查询 lastName="Doe",并仅返回 lastName 字段、排除 _id):

1
2
3
4
db.employees.find(
{ "lastName": "Doe" },
{ "_id": 0, "lastName": 1 }
)

lastName 不是数组类型,MongoDB 可以通过通配符索引直接完成 “覆盖查询”,无需额外读取文档数据。

复合通配符索引

7.0 版本中的新增功能

MongoDB 支持在一个字段或一组字段上创建通配符索引。 一个复合索引有多个术语。复合通配符索引具有一个通配符术语和一个或多个附加索引术语。

基本没用过,我也说不太好了

索引命名规则和一些情况

要指定索引名称,请在创建索引时包含 name 选项:

1
2
3
4
db.<collection>.createIndex(
{ <field>: <value> },
{ name: "<indexName>" }
)
  • 索引名称必须是唯一的。使用已有索引的名称创建索引会报错。
  • 无法重命名现有索引。相反,您必须删除索引并使用新名称重新创建索引。
  • 索引默认名称为 <字段1>_<方向>_<字段2>_<方向>(如 name_1_age_-1)。
1
2
3
4
db.users.createIndex(
{ firstName: 1, lastName: 1, age: -1 },
{ name: "user_name_age_idx" } // 自定义名称
)

如果您在创建索引时未指定名称,系统会用下划线将每个索引键字段和值连接起来,从而生成新索引的名称。

Index 默认名称
{ score : 1 } score_1
{ content : "text", "description.tags": "text" } content_text_description.tags_text
{ category : 1, locale : "2dsphere"} category_1_locale_2dsphere
{ "fieldA" : 1, "fieldB" : "hashed", "fieldC" : -1 } fieldA_1_fieldB_hashed_fieldC_-1

注意,单个索引键的大小不能超过 1024 字节(对长字符串字段建索引需谨慎)。

其中,索引涉及到选择性质,选择性高的字段(值分布稀疏,如邮箱)适合建索引;选择性低的字段(如性别,只有男 / 女)建索引效果差,甚至可能比全表扫描慢

也就是说,每个索引都会增加写入开销,建议只对高频查询字段建索引。

而且,若查询包含 sort(),且排序字段与索引字段一致,利用索引的有序性,可避免额外排序操作。

1
2
// 索引 { age: -1 } 可优化以下查询的排序
db.users.find({ name: "Alice" }).sort({ age: -1 })

索引的属性

部分索引

仅对满足指定过滤条件的文档创建索引,减少索引存储开销和维护成本。

查询仅针对文档子集的场景(如仅索引 “已激活” 的用户)用的非常多

1
2
3
4
db.users.createIndex(
{ age: 1 },
{ partialFilterExpression: { isActive: true } }
)

稀疏索引

上面说了

仅包含存在索引字段的文档条目,跳过无该字段的文档。

在字段存在性不确定的场景用的多

补充一下

稀疏索引仅包含具有索引字段的文档的条目,即使索引字段包含 null 值也是如此。该索引将跳过缺少索引字段的所有文档。索引是“稀疏”的,因为它不包括集合的所有文档。相比之下,非稀疏索引包含集合中的所有文档,为那些不包含索引字段的文档存储 null 值。

如果稀疏索引会导致查询和排序操作的结果集不完整,则除非 hint() 显式指定该索引,否则 MongoDB 不会使用该索引。

例如,除非显式提示,否则查询 { x: { $exists: false } } 不会在 x 字段上使用稀疏索引。

当您对集合中所有文档执行 count() 时(即,采用空查询谓词),您要纳入指定稀疏索引hint(),即便该稀疏索引产生错误计数也要使用。

1
2
3
4
db.collection.insertOne( { _id: 1, y: 1 } );
db.collection.createIndex( { x: 1 }, { sparse: true } );

db.collection.find().hint( { x: 1 } ).count();

如需获得正确的计数,在对集合中的所有文档进行计数时,请勿在使用 hint() 时指定稀疏索引

1
2
3
4
db.collection.find().count();

db.collection.createIndex( { y: 1 } );
db.collection.find().hint( { y: 1 } ).count();

以下索引类型始终是稀疏的:

TTL 索引

自动删除超过指定时间的文档,适用于临时数据(如日志、会话信息)。

对日期字段创建 TTL 索引,MongoDB 后台线程会定期清理过期文档。

1
2
3
4
db.logs.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 } // 1小时后过期
)

所以说,仅支持日期字段;过期清理是异步的,并非 “精准到秒” 删除。

不分大小写的索引

理解和创建

不区分大小写的索引支持执行字符串比较而不考虑大小写的查询。不区分大小写是由排序规则决定的。

不区分大小写的索引不会提高 $regex 查询的性能,因为 $regex操作符不支持排序规则,因此无法利用此类索引。

可以指定 collation 选项,以使用 db.collection.createIndex() 创建不区分大小写的索引:

1
2
3
4
db.users.createIndex(
{ name: 1 },
{ collation: { locale: "en", strength: 2 } } // strength=2 表示不区分大小写
)

要为不区分大小写的索引指定排序规则,请在 collation 对象中包含以下字段:

字段 说明
locale 指定语言规则。有关可用的区域设置列表,请参阅支持的语言和区域设置
strength 确定比较规则。strength 值为 1 或 2 表示不区分大小写的排序规则。

排序规则(Collation)与大小写敏感的关系

MongoDB 的排序规则(collation) 用于定义字符串的比较规则,其中 strength 参数决定了大小写敏感性:

  • strength: 2 时,字符串比较不区分大小写(如 “Betsy”、“BETSY”、“betsy” 视为相等);

  • 若不指定 strengthstrength 大于 2,则区分大小写

集合默认排序规则对索引的继承

当创建集合时指定了默认排序规则,该集合上的所有索引会自动继承这个排序规则。这意味着索引的 “大小写匹配逻辑” 会与集合的默认排序规则保持一致。

以文档中的示例为例:

  1. 创建带默认排序规则的集合

    1
    db.createCollection("names", { collation: { locale: 'en_US', strength: 2 } })

    这里 locale: 'en_US' 表示使用美式英语的排序规则,strength: 2 表示不区分大小写。

  2. 在集合上创建索引

    1
    db.names.createIndex({ first_name: 1 })

    这个索引会继承集合的默认排序规则(即不区分大小写)。

MongoDB 中 “不分大小写的索引” 是通过集合的默认排序规则(collation 实现的:集合创建时指定 strength: 2 的排序规则后,其索引会继承该规则,从而让查询在不区分大小写的同时利用索引提升性能。若需要区分大小写的查询,可在查询时显式指定排序规则,但此时会失去对应索引的优化效果。这种设计让开发者能灵活控制字符串比较的大小写敏感性,同时兼顾索引性能。

唯一索引

唯一索引可确保索引字段不存储重复值。 单个字段上的唯一索引可确保一个值对于给定字段最多出现一次。 唯一复合索引可确保索引键值的任何给定组合最多只出现一次。 默认, MongoDB在创建集合期间在_id字段上创建唯一索引。

用于需要唯一标识的字段(如用户 ID、邮箱地址),类似关系型数据库的唯一约束。

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

若集合中已有重复数据,创建唯一索引会失败;可结合稀疏索引(sparse: true),仅对存在该字段的文档做唯一性约束。

唯一约束适用于集合中的独立文档。换言之,唯一索引可以防止独立文档为该索引键具有相同的值。

由于该约束适用于独立文档,因此对于唯一多键索引,只要某文档的索引键值与其他文档的索引键值不重复,该文档就可能包含导致重复索引键值的数组元素。在这种情况下,重复的索引条目仅插入索引一次。

唯一符合索引

还可以在复合索引上强制执行唯一性约束。唯一复合索引强制要求索引键值的组合具有唯一性。

例如,要在 members 集合的 groupNumberlastnamefirstname 字段上创建唯一索引,请在 mongosh 中使用以下操作:

1
db.members.createIndex( { groupNumber: 1, lastname: 1, firstname: 1 }, { unique: true } )

创建的索引强制 groupNumberlastnamefirstname 值的组合具备唯一性。

以包含以下文档的集合为例:

1
{ _id: 1, a: [ { loc: "A", qty: 5 }, { qty: 10 } ] }

a.loca.qty 上创建唯一的复合多键索引:

1
db.collection.createIndex( { "a.loc": 1, "a.qty": 1 }, { unique: true } )

唯一索引允许将以下文档插入集合,因为该索引强制 a.loca.qty 值的组合有唯一性:

1
2
3
4
db.collection.insertMany( [
{ _id: 2, a: [ { loc: "A" }, { qty: 5 } ] },
{ _id: 3, a: [ { loc: "A", qty: 10 } ] }
] )

隐藏索引

MongoDB 6.0+ 引入

隐藏索引是指对查询优化器不可见的索引,即查询优化器不会选择隐藏索引来支持查询。它的设计目的是让开发者在不删除索引的前提下,评估 “删除该索引” 对查询性能的影响 —— 若影响负面,可直接取消隐藏,无需重建索引。

除了对查询优化器不可见外,隐藏索引的其他行为与普通索引一致:

  • 唯一约束仍生效:若隐藏索引是唯一索引,仍会强制字段值的唯一性,拒绝重复数据插入 / 更新。
  • TTL 逻辑仍运行:若隐藏索引是 TTL 索引,仍会自动清理过期文档。
  • 可被列举和统计:会出现在 listIndexes 命令、db.collection.getIndexes() 的结果中;也会参与 db.collection.stats()$indexStats 等统计操作,且占用磁盘和内存。
  • 更新与维护不中断:集合写入操作会正常更新隐藏索引,其维护逻辑与普通索引完全一致。
  • $indexStats 重置规则:隐藏 / 取消隐藏操作会重置该索引的 $indexStats(若重复操作同一状态则不重置)。

索引主要用于索引性能的安全测试

  • 当不确定某个索引是否仍有价值时,可先将其隐藏,观察查询性能变化。
  • 若隐藏后性能下降,说明该索引仍需保留,可立即取消隐藏;若性能无影响,则可安全删除该索引。

创建隐藏索引

创建时通过 hidden: true 指定,需确保 Feature Compatibility Version(FCV)为 6.0 或更高版本。

示例:

1
2
3
4
db.addresses.createIndex(
{ borough: 1 },
{ hidden: true }
);

隐藏已有索引

可通过 collMod 命令或 db.collection.hideIndex() 方法实现,支持通过索引键模式索引名称指定目标索引。

示例(隐藏 restaurants 集合的 { borough: 1, ratings: 1 } 索引):

1
2
3
4
5
// 方式1:通过索引键模式
db.restaurants.hideIndex({ borough: 1, ratings: 1 });

// 方式2:通过索引名称(默认名称为 borough_1_ratings_1)
db.restaurants.hideIndex("borough_1_ratings_1");

取消隐藏索引

通过 collMod 命令或 db.collection.unhideIndex() 方法实现,同样支持索引键模式索引名称指定。

示例(取消上述隐藏索引):

1
2
3
4
5
// 方式1:通过索引键模式
db.restaurants.unhideIndex({ borough: 1, ratings: 1 });

// 方式2:通过索引名称
db.restaurants.unhideIndex("borough_1_ratings_1");

限制

  • 版本要求:仅 FCV 6.0 及以上版本支持隐藏索引。
  • _id 索引例外:无法隐藏集合默认的 _id 索引。
  • hint() 无法指定:不能通过 cursor.hint() 强制查询使用隐藏索引。

索引的核心操作

创建索引(createIndex()

语法

1
2
3
4
db.<集合名>.createIndex(
<索引规则>, // 指定索引的字段和索引类型,定义索引字段及排序方向(1:升序,-1:降序)
<选项参数> // 可选,如唯一索引、索引名称等
)

都有这些选项可以选择

image-20251025180203900

示例

单字段索引

1
2
3
4
5
// 为 "age" 字段创建升序索引
db.users.createIndex({ age: 1 })

// 为 "createTime" 字段创建降序索引(适合按时间倒序查询)
db.orders.createIndex({ createTime: -1 })
image-20251025180352976

复合索引

复合索引的字段顺序影响查询效率(遵循 “最左前缀原则”,即查询条件需包含索引的前序字段才能命中索引)。

1
2
3
4
5
6
7
8
9
// 创建 {name:1, age:-1} 复合索引
db.users.createIndex({ name: 1, age: -1 })

// 能命中索引的查询(包含前序字段 "name")
db.users.find({ name: "Alice", age: { $gt: 25 } }) // 命中
db.users.find({ name: "Bob" }) // 命中(仅用前序字段)

// 无法命中索引的查询(缺少前序字段 "name")
db.users.find({ age: { $gt: 25 } }) // 未命中
  • 最左前缀原则:复合索引中,查询条件必须包含索引的前序字段才能命中索引(如 {a:1, b:1} 索引,{a: x} 可命中,{b: x} 无法命中)。
image-20251025180415414

唯一索引

确保索引字段的值不重复,插入重复值会报错。

1
2
3
4
5
6
// 为 "email" 字段创建唯一索引(防止重复注册)
db.users.createIndex({ email: 1 }, { unique: true })

// 插入重复 email 会报错
db.users.insertOne({ email: "a@test.com" }) // 成功
db.users.insertOne({ email: "a@test.com" }) // 报错:E11000 duplicate key error

注意:唯一索引对 null 值也视为唯一,即只能有一个文档的索引字段为 null

通配符索引

通配符索引通过通配符说明符 $\*\* 来定义索引键,语法如下:

1
db.collection.createIndex({ "$**": <sortOrder> })

也可以通过 createIndexes 命令、db.collection.createIndexes() 方法来创建(和普通索引的创建方式兼容)。

局部索引

局部索引(Partial Indexes)顾名思义,只对collection的一部分添加索引。

创建索引的时候,根据过滤条件判断是否对document添加索引,对于没有添加索引的文档查找时采用的全表扫描,对添加了索引的文档查找时使用索引。

1
2
3
4
5
6
7
8
9
//userinfos集合中age>25的部分添加age字段索引
db.userinfos.createIndex(
{age:1},
{ partialFilterExpression: {age:{$gt: 25 }}}
)
//查询age<25的document时,因为age<25的部分没有索引,会全表扫描查找(stage:COLLSCAN)
db.userinfos.find({age:23})
//查询age>25的document时,因为age>25的部分创建了索引,会使用索引进行查找(stage:IXSCAN)
db.userinfos.find({age:26})

TTL 索引

需基于日期类型字段创建,MongoDB 会定期删除过期文档(过期时间 = 文档字段值 + expireAfterSeconds)。

1
2
3
4
5
6
7
8
9
10
11
// 为 "expireAt" 字段创建 TTL 索引,文档在 "expireAt" 时间后 0 秒过期
db.sessions.createIndex(
{ expireAt: 1 },
{ expireAfterSeconds: 0 }
)

// 插入文档:1小时后过期(expireAt 设为当前时间+1小时)
db.sessions.insertOne({
userId: "123",
expireAt: new Date(Date.now() + 3600 * 1000) // 单位:毫秒
})

注意

  • TTL 索引仅支持单字段,不支持复合索引。

  • 过期清理由后台线程执行,可能存在几分钟延迟。

文本索引

用于全文检索,支持对多个字符串字段创建联合文本索引,查询时使用 $text 操作符。

1
2
3
4
5
6
7
8
// 为 "title" 和 "content" 字段创建文本索引
db.articles.createIndex({ title: "text", content: "text" })

// 搜索包含 "mongodb" 或 "database" 的文档
db.articles.find({ $text: { $search: "mongodb database" } })

// 搜索包含 "mongodb" 但不包含 "sql" 的文档
db.articles.find({ $text: { $search: "mongodb -sql" } })

查看索引(getIndexes()

查看集合中所有索引的详细信息(名称、字段、类型等)。

image-20251025175643565

结果中显示的是默认的 _id_ 索引(MongoDB 在创建集合的过程中,会在 _id 字段上创建一个唯一的索引,默认名字为 _id_,该索引可防止插入两个具有相同 _id 值的文档)

  • _id 索引是唯一索引,因此 _id 值不能重复
  • 在分片集群中,通常使用 _id 作为片键
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 查看 users 集合的所有索引
db.users.getIndexes()

// 输出示例(关键信息):
[
{
v: 2, // 索引版本
key: { _id: 1 }, // 索引字段及方向(默认 _id 索引)
name: "_id_", // 索引名称(自动生成或自定义)
unique: true // 是否为唯一索引
},
{
v: 2,
key: { email: 1 },
name: "email_1", // 单字段索引名称格式:<字段>_<方向>
unique: true
},
{
v: 2,
key: { name: 1, age: -1 },
name: "name_1_age_-1" // 复合索引名称格式:<字段1>_<方向>_<字段2>_<方向>
}
]

删除索引

删除指定索引(按名称或规则)

1
2
3
4
5
// 按索引名称删除(推荐,名称可通过 getIndexes() 获取)
db.users.dropIndex("email_1")

// 按索引规则删除
db.users.dropIndex({ name: 1, age: -1 })

删除所有索引(保留 _id 索引)

要删除 _id 索引之外的所有索引,请使用 dropIndexes() 方法:

1
db.users.dropIndexes() // 仅删除用户创建的索引,_id 索引不会被删除

要删除多个索引,请使用 dropIndexes() 方法并指定索引名称数组:

1
db.<collection>.dropIndexes( [ "<index1>", "<index2>", "<index3>" ] )

性能相关操作

查询执行计划(explain()

分析查询性能(Analyze Query Performance)通常使用执行计划(Explain Plan)来查看查询的情况,如查询耗费的时间、是否基于索引查询等

1
2
// 查看查询的执行计划(重点看 "executionStats" 或 "winningPlan")
db.users.find({ name: "Alice", age: { $gt: 25 } }).explain("executionStats")
  • winningPlan.inputStage.stageIXSCAN,表示使用索引扫描(高效)。
  • 若为 COLLSCAN,表示全集合扫描(低效,需优化索引)。
image-20251025180609874

而打印的执行 explain 后,输出各个字段的含义

字段名 描述
explainVersion 解释输出的版本号。
queryPlanner 查询计划器的详细信息。
namespace 执行查询的命名空间(数据库和集合)。
parsedQuery 解析后的查询条件。
indexFilterSet 是否设置了索引过滤器。
queryHash 查询的哈希值。
planCacheKey 用于查询缓存的键。
optimizationTimeMillis 查询优化所花费的时间(毫秒)。
maxIndexedOrSolutionsReached 是否达到了索引 OR 解决方案的最大数量。
maxIndexedAndSolutionsReached 是否达到了索引 AND 解决方案的最大数量。
maxScansToExplodeReached 是否达到了索引爆炸扫描的最大数量。
prunedSimilarIndexes 是否修剪了相似的索引。
winningPlan 被选中的查询计划。
isCached 是否从计划缓存中检索到计划。
stage 查询执行的阶段。
inputStage 当前阶段的输入阶段(用于嵌套阶段)。
keyPattern 索引的键模式。
indexName 索引的名称。
isMultiKey 索引是否是多键索引。
multiKeyPaths 包含多键路径的索引字段。
isUnique 索引是否是唯一索引。
isSparse 索引是否是稀疏索引。
isPartial 索引是否是部分索引。
indexVersion 索引的版本。
direction 索引扫描的方向。
indexBounds 索引扫描的边界。
rejectedPlans 被拒绝的查询计划列表。
command 执行的命令的详细信息。
serverInfo 服务器信息,包括主机名、端口、版本等。
serverParameters 影响查询执行的服务器参数。
ok 命令是否成功执行的标志。

其中 stage字段的取值及含义

阶段名称 描述
COLLSCAN 集合扫描,即全集合扫描,没有使用索引。
IXSCAN 索引扫描,使用索引来查找文档。
FETCH 获取阶段,用于检索索引扫描后找到的文档的其余字段。
SHARD_MERGE 在分片集群中,合并来自不同分片的查询结果。
SORT 排序阶段,对结果进行排序。
LIMIT 限制阶段,限制返回的文档数量。
SKIP 跳过阶段,跳过指定数量的文档。
IDHACK 对于 _id 的查询,MongoDB 可以使用特殊的优化。
SHARDING_FILTER 在分片集群中,用于过滤掉不属于当前查询的分片数据的阶段。
PROJECTION 投影阶段,只返回文档中的特定字段。
TEXT 文本搜索阶段,用于文本索引的搜索。
GEONEAR 地理空间查询阶段,用于查找最接近某个点的文档。
GEOFILTER 地理空间过滤阶段,用于过滤地理空间查询的结果。
COUNT 计数阶段,用于 count 操作。
COUNT_SCAN 使用索引进行计数扫描的阶段。
COUNT_SCAN_WITH_FILTER 使用索引进行计数扫描,并且应用过滤器的阶段。
DISTINCT_SCAN 用于 distinct 操作的索引扫描阶段。
SUBPLAN 子计划阶段,用于处理复杂查询的一部分。
IXHASH 使用散列索引的阶段。
FORCED_SCAN 强制进行集合扫描,即使存在索引。
COVERED 索引覆盖查询,所有需要的字段都在索引中,不需要回表查询。
EOF 查询结束。

$indexStats 聚合阶段之外,MongoDB 还提供各种索引统计信息,而当您在分析数据库的索引使用情况时可能需考虑这些统计信息:

索引使用情况统计(aggregate()

通过 $indexStats 查看索引的使用频率,删除未使用的冗余索引。

1
2
3
4
// 查看 users 集合所有索引的使用情况
db.users.aggregate([{ $indexStats: {} }])

// 输出示例:包含 "name"(索引名)、"accesses"(访问次数)等信息
image-20251025180944140

使用 hint() 控制对索引的使用

MongoDB 的查询优化器会根据索引信息、数据分布等自动选择 “它认为最优” 的索引执行查询。但在某些场景下(如数据分布特殊、统计信息过时),优化器可能选错索引,导致查询效率低下。hint() 的作用就是绕过优化器的自动选择,强制查询使用指定索引,确保查询按预期路径执行。

hint() 并非日常查询的必需工具,但在以下场景中尤为重要:

  1. 优化器选择的索引非最优:例如查询条件同时匹配多个索引,但优化器选择的索引扫描行数更多,此时可用 hint() 指定更高效的索引。
  2. 验证索引性能:对比不同索引的执行效率(如测试新创建的复合索引是否比单字段索引更快)。
  3. 数据分布特殊时:如某字段存在大量重复值,但优化器未识别,仍选择该字段的索引,可通过 hint() 切换到其他索引。
  4. 临时规避统计信息偏差:当集合数据频繁变更导致统计信息过时(优化器依赖的基数、分布等信息不准确)时,hint() 可作为临时解决方案(长期需更新统计信息)。

hint() 需附加在 find() 之后,参数为索引的键模式(创建索引时的字段及排序方向)或索引名称

1
2
3
4
5
// 方式1:通过索引键模式指定(推荐,更直观)
db.collection.find(<query>).hint({ <field1>: <order1>, <field2>: <order2>, ... })

// 方式2:通过索引名称指定(需先知道索引名称,如默认的"field1_order1_field2_order2")
db.collection.find(<query>).hint("<indexName>")

强制 MongoDB 使用特定索引进行 db.collection.find() 操作,使用 hint() 方法指定该索引。将 hint() 方法附加到 find() 方法,执行查询时强制使用 zipcode 单字段索引。以以下示例为例:

1
2
3
db.people.find(
{ name: "John Doe", zipcode: { $gt: "63000" } }
).hint( { zipcode: 1 } )

要查看特定索引的执行统计信息,请将 hint() 方法附在 db.collection.find()后面,再接上 cursor.explain(),因为hint() 常与 explain() 配合使用,用于验证指定索引的执行效率。explain() 提供查询的详细执行计划,包括扫描行数、耗时、是否使用索引等关键信息。

1
2
3
4
5
6
7
8
9
// 方式1:hint() 后接 explain()
db.people.find(
{ name: "John Doe", zipcode: { $gt: "63000" } }
).hint({ zipcode: 1 }).explain("executionStats")

// 方式2:explain() 后接 find().hint()(推荐,更符合逻辑流)
db.people.explain("executionStats").find(
{ name: "John Doe", zipcode: { $gt: "63000" } }
).hint({ zipcode: 1 })

或者,将 hint() 方法附加到 db.collection.explain().find()

1
2
3
db.people.explain("executionStats").find(
{ name: "John Doe", zipcode: { $gt: "63000" } }
).hint( { zipcode: 1 } )

关键分析指标(executionStats 层级)我们一般看这些内容

  • executionStats.executionTimeMillis:查询总耗时(毫秒),值越小越好。
  • executionStats.totalKeysExamined:索引扫描的键数量(理想情况下应接近 nReturned)。
  • executionStats.totalDocsExamined:文档扫描数量(若为索引覆盖查询,此值应为 0)。
  • executionStats.nReturned:返回的文档数量。

少使这玩意,容易魔怔,长期依赖可能掩盖索引设计问题

因为我们优先应通过优化索引结构让优化器自动选择最优方案。

重说覆盖查询

当查询条件和查询的投影仅包含索引字段时,MongoDB 会直接从索引返回结果,而不扫描任何文档或将文档带入内存,这些覆盖的查询非常高效(类似于 MySQL 中的覆盖索引)

1
2
3
4
db.comment.find(
{ user_id: "1003" },
{ user_id: 1, _id: 0 }
).explain();