MongoDB7.0--索引

摘要

  • 本文介绍如何使用MongoDB7.0的索引

  • MongoDB版本7.0.6

  • MongoDB Indexes

MongoDB索引简介

  • 索引支持在MongoDB中高效执行查询。如果没有索引,MongoDB必须扫描集合中的每个文档才能返回查询结果。如果查询存在适当的索引,MongoDB使用该索引来限制它必须扫描的文档数量。

  • 虽然索引可以提高查询性能,但添加索引对写入操作的性能有负面影响。对于写入读数比较高的集合,索引很昂贵,因为每个插入还必须更新任何索引。

  • 所以合理的创建索引,即可以提升查询性能,又不会对写操作造成太大的影响。

  • MongoDB采用 B-Tree (准确的说是 B+Tree) 做索引,索引创建在colletions上。

创建索引语法

1
db.collection.createIndex( keys, options )
  • Key 值为你要创建的索引字段,1 按升序创建索引, -1 按降序创建索引

  • options 选项

参数 类型 描述
background Boolean 建索引过程是否阻塞其它数据库操作,设置为 true 则以后台方式创建索引
unique Boolean 建立的索引是否唯一,设置为 true 则创建唯一索引
name string 索引的名称
dropDups Boolean 3.0+版本已废弃,在建立唯一索引时是否删除重复记录
sparse Boolean 对文档中不存在的字段数据是否启用索引
expireAfterSeconds integer 指定一个以秒为单位的数值,完成 TTL 设定,设定集合的生存时间
v index version 索引的版本号
weights document 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重
default_language string 对于文本索引,该参数决定了停用词及词干和词器的规则的列表
language_override string 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的 language

索引类型

  • 与大多数数据库一样,MongoDB支持各种丰富的索引类型,包括单键索引、复合索引,唯一索引等一些常用的结构。由于采用了灵活可变的文档类型,因此它也同样支持对嵌套字段、数组进行索引。通过建立合适的索引,我们可以极大地提升数据的检索速度。在一些特殊应用场景,MongoDB还支持地理空间索引、文本检索索引、TTL索引等不同的特性。

单键索引(Single Field Indexes)

  • 单键索引是MongoDB最简单的索引类型,它将一个字段作为索引键,索引键值唯一。

  • 默认情况下,MongoDB会在ID字段上创建一个单键索引,ID字段是文档的唯一标识符,MongoDB会自动创建一个ID字段,如果用户自己创建ID字段,MongoDB会自动将ID字段作为单键索引。

  • 单键索引的创建方式如下:

1
2
3
4
# 1表示正序,-1表示倒序
db.books.createIndex({title:1})
# 对内嵌文档字段创建索引:
db.books.createIndex({"author.name":1})

复合索引(Compound Index)

  • 复合索引是将多个字段作为索引键,其性质和单字段索引类似。但不同的是,复合索引中字段的顺序、字段的升降序对查询性能有直接的影响,因此在设计复合索引时则需要考虑不同的查询场景。

  • 复合索引的创建方式如下:

1
2
3
db.books.createIndex({title:1, author.name:1})
# 对内嵌文档字段创建索引:
db.books.createIndex({"author.name":1, "author.age":1})

多键(数组)索引(Multikey Index)

  • 多键索引是将一个字段作为索引键,该字段可以是嵌套文档、数组等复杂数据类型。

  • 多键索引的创建方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# tags字段为数组
db.books.createIndex({"tags":1})
# 创建复合多值索引,这里仅能有一个字段是数组,MongoDB并不支持一个复合索引中同时出现多个数组字段
db.books.createIndex({title:1, tags:1})

# 示例数据格式
{
_id: 1,
item: "abc",
stock: [
{ size: "S", color: "red", quantity: 25 },
{ size: "S", color: "blue", quantity: 10 },
{ size: "M", color: "blue", quantity: 50 }
]
}
# 可以对嵌入文档创建多键索引
db.collection.createIndex( { "stock.size": 1, "stock.quantity": 1 } )

Hash索引(Hashed Indexes)

  • Hash索引是MongoDB中的一种特殊的索引类型,它将字段的值计算出一个哈希值,然后将该哈希值作为索引键。由于哈希值的唯一性,因此MongoDB在创建Hash索引时,不会对索引键值进行排序。

  • Hash索引的创建方式如下:

1
2
3
db.books.createIndex({title:"hashed"})
# 对内嵌文档字段创建索引:
db.books.createIndex({"author.name":"hashed"})

通配符索引(Wildcard Indexes)

  • 通配符索引是MongoDB中的一种特殊的索引类型,它允许在索引键值中包含通配符,从而支持对通配符匹配的查询。

  • MongoDB 4.2 引入了通配符索

  • 通配符索引的创建方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 示例数据
{
"product_name" : "Spy Coat",
"product_attributes" : {
"material" : [ "Tweed", "Wool", "Leather" ],
"size" : {
"length" : 72,
"units" : "inches"
}
}
}

db.products.createIndex( { "product_attributes.$**" : 1 } )
  • 通配符索引是稀疏的,不索引空字段。因此,通配符索引不能支持查询字段不存在的文档。

1
2
3
4
5
# 通配符索引不能支持以下查询
db.products.find( {"product_attributes" : { $exists : false } } )
db.products.aggregate([
{ $match : { "product_attributes" : { $exists : false } } }
])
  • 通配符索引为文档或数组的内容生成条目,而不是文档/数组本身。因此通配符索引不能支持精确的文档/数组相等匹配。通配符索引可以支持查询字段等于空文档{}的情况

1
2
3
4
5
6
#通配符索引不能支持以下查询:
db.products.find({ "product_attributes.colors" : [ "Blue", "Black" ] } )

db.products.aggregate([{
$match : { "product_attributes.colors" : [ "Blue", "Black" ] }
}])

索引属性

唯一索引(Unique Indexes)

  • 唯一索引的创建方式如下:

1
2
3
4
5
6
# 单键索引支持唯一约束
db.books.createIndex({title:1}, {unique:true})
# 复合索引支持唯一性约束
db.books.createIndex({title:1,type:1}, {unique:true})
# 多键索引支持唯一性约束,这里tags是数组
db.books.createIndex({"tags":1}, {unique:true})
  • 唯一性索引对于文档中缺失的字段,会使用null值代替,因此不允许存在多个文档缺失索引字段的情况。

  • 对于分片的集合,唯一性约束必须匹配分片规则。换句话说,为了保证全局的唯一性,分片键必须作为唯一性索引的前缀字段。

部分索引(Partial Indexes)

  • 部分索引仅对满足指定过滤器表达式的文档进行索引。通过在一个集合中为文档的一个子集建立索引,部分索引具有更低的存储需求和更低的索引创建和维护的性能成本。

  • 部分索引的创建方式如下:

1
2
# 符合条件{author: {$exists: true}},即存在作者,才对title创建升序索引
db.books.createIndex({title:1}, {partialFilterExpression: {author: {$exists: true}}})

partialFilterExpression(筛选器表达式)选项接受指定过滤条件的文档:
- 等式表达式(例如:field: value或使用$eq操作符)
- $exists: true
- $gt, $gte, $lt, $lte
- $type
- 顶层的$and

  • 注意:如果同时指定了partialFilterExpression和唯一约束,那么唯一约束只适用于满足筛选器表达式的文档。如果文档不满足筛选条件,那么带有惟一约束的部分索引不会阻止插入不满足惟一约束的文档。

稀疏索引(Sparse Indexes)

  • 索引的稀疏属性确保索引只包含具有索引字段的文档的条目,索引将跳过没有索引字段的文档。即只对存在字段的文档进行索引(包括字段值为null的文档)。

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

  • 稀疏索引的创建方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 数据准备
db.scores.insertMany([
{"userid" : "newbie"},
{"userid" : "abby", "score" : 82},
{"userid" : "nina", "score" : 90}
])

# 创建稀疏索引
db.scores.createIndex( { score: 1 } , { sparse: true } )

# 测试
# 使用稀疏索引
db.scores.find( { score: { $lt: 90 } } )

# 即使排序是通过索引字段,MongoDB也不会选择稀疏索引来完成查询,以返回完整的结果
db.scores.find().sort( { score: -1 } )

# 要使用稀疏索引,使用hint()显式指定索引
db.scores.find().sort( { score: -1 } ).hint( { score: 1 } )
  • 同时具有稀疏性和唯一性的索引可以防止集合中存在字段值重复的文档,但允许不包含此索引字段的文档插入。

1
2
3
4
5
6
7
8
9
10
11
12
# 删除之前创建的索引
db.scores.dropIndex({score:1})
# 创建具有唯一约束的稀疏索引
db.scores.createIndex( { score: 1 } , { sparse: true, unique: true } )

# 测试插入
db.scores.insertMany( [
{ "userid": "AAAAAAA", "score": 50 },
{ "userid": "BBBBBBB", "score": 64 },
{ "userid": "CCCCCCC" },
{ "userid": "CCCCCCC" }
] )

TTL索引(TTL Indexes)

  • MongoDB 可以使用它在一定时间或特定时钟时间后自动从集合中删除文档,就是带有过期时间的索引,到期后,MongoDB会自动删除这些过期的文档。

  • TTL索引只能创建在日期字段上,当文档过期后,MongoDB会自动删除这些文档。

  • TTL索引的创建方式如下:

1
2
3
4
5
6
7
8
# 数据准备
db.log_events.insertOne( {
"createdAt": new Date(),
"logEvent": 2,
"logMessage": "Success!"
} )
# 创建TTL索引
db.scores.createIndex( { "createdAt": 1 }, { expireAfterSeconds: 3600 } )
  • TTL 索引不保证过期数据会在过期后立即被删除。文档过期和 MongoDB 从数据库中删除文档的时间之间可能存在延迟。删除过期文档的后台任务每 60 秒运行一次。因此,在文档到期和后台任务运行之间的时间段内,文档可能会保留在集合中。

  • TTL索引在创建之后,仍然可以对过期时间进行修改。这需要使用collMod命令对索引的定义进行变更

1
2
3
4
5
6
7
db.runCommand( {
collMod: "log_events",
index: {
keyPattern: { createdAt: 1 },
expireAfterSeconds: 3600
}
} )

隐藏索引(Hidden Indexes)

  • 隐藏索引对查询规划器不可见,不能用于查询。

  • 通过对规划器隐藏索引,用户可以在不实际删除索引的情况下评估删除索引的潜在影响。如果影响是负面的,用户可以取消隐藏索引,而不必重新创建已删除的索引。

  • 隐藏索引的创建方式如下:

1
2
3
4
5
6
7
8
# 创建隐藏索引
db.restaurants.createIndex({ borough: 1 },{ hidden: true });
# 隐藏现有索引
db.restaurants.hideIndex( { borough: 1} );
db.restaurants.hideIndex( "索引名称" )
# 取消隐藏索引
db.restaurants.unhideIndex( { borough: 1} );
db.restaurants.unhideIndex( "索引名称" );

索引其它操作

查看索引

1
2
3
4
# 查看索引信息
db.books.getIndexes()
# 查看索引键
db.books.getIndexKeys()

删除索引

1
2
3
4
# 删除集合指定索引
db.col.dropIndex("索引名称")
# 删除集合所有索引,不能删除主键索引
db.col.dropIndexes()

索引使用建议

  • 为每一个查询建立合适的索引

    • 这个是针对于数据量较大比如说超过几十上百万(文档数目)数量级的集合。
    • 如果没有索引MongoDB需要把所有的Document从盘上读到内存,这会对MongoDB服务器造成较大的压力并影响到其他请求的执行。
  • 创建合适的复合索引,不要依赖于交叉索引

    • 如果你的查询会使用到多个字段,MongoDB有两个索引技术可以使用:交叉索引和复合索引。
    • 交叉索引就是针对每个字段单独建立一个单字段索引,然后在查询执行时候使用相应的单字段索引进行索引交叉而得到查询结果。
    • 交叉索引目前触发率较低,所以如果你有一个多字段查询的时候,建议使用复合索引能够保证索引正常的使用。
1
2
3
4
#查找所有年龄小于30岁的深圳市马拉松运动员
db.athelets.find({sport: "marathon", location: "sz", age: {$lt: 30}}})
#创建复合索引
db.athelets.createIndex({sport:1, location:1, age:1})
  • 复合索引字段顺序:匹配条件在前,范围条件在后(Equality First, Range After)

    • 前面的例子,在创建复合索引时如果条件有匹配和范围之分,那么匹配条件(sport: “marathon”) 应该在复合索引的前面。范围条件(age: <30)字段应该放在复合索引的后面。
  • 尽可能使用覆盖索引(Covered Index)

    • 建议只返回需要的字段,同时,利用覆盖索引来提升性能。
  • 建索引要在后台运行

    • 在对一个集合创建索引时,该集合所在的数据库将不接受其他读写操作。对大数据量的集合建索引,建议使用后台运行选项 {background: true}
  • 避免设计过长的数组索引

    • 数组索引是多值的,在存储时需要使用更多的空间。如果索引的数组长度特别长,或者数组的增长不受控制,则可能导致索引空间急剧膨胀。

explain执行计划

  • explain执行计划的作用是:查看MongoDB执行查询时的执行计划。

  • explain执行计划的使用方式如下:

1
2
3
4
5
6
7
# 语法
db.collection.find().explain(<verbose>)

# 示例
db.books.find({title: "MongoDB 教程"}).explain()
db.books.find({title: "MongoDB 教程"}).explain("executionStats")
db.books.find({title: "MongoDB 教程"}).explain("allPlansExecution")
  • verbose :可选参数,表示执行计划的输出模式,默认queryPlanner

模式名字 描述
queryPlanner 执行计划的详细信息,包括查询计划、集合信息、查询条件、最佳执行计划、查询方式和 MongoDB 服务信息等
executionStats 最佳执行计划的执行情况和被拒绝的计划等信息
allPlansExecution 选择并执行最佳执行计划,并返回最佳执行计划和其他执行计划的执行情况
  • 输出结果中重点查看stage,比如queryPlanner下的winningPlan.stage

  • stage类型

状态 描述
COLLSCAN 全表扫描
IXSCAN 索引扫描
FETCH 根据索引检索指定文档
SHARD_MERGE 将各个分片返回数据进行合并
SORT 在内存中进行了排序
LIMIT 使用 limit 限制返回数
SKIP 使用 skip 进行跳过
IDHACK 对 _id 进行查询
SHARDING_FILTER 通过 mongos 对分片数据进行查询
COUNTSCAN count 不使用索引进行 count 时的 stage 返回
COUNT_SCAN count 使用了索引进行 count 时的 stage 返回
SUBPLAN 未使用到索引的 $or 查询的 stage 返回
TEXT 使用全文索引进行查询时候的 stage 返回
PROJECTION 限定返回字段时候 stage 的返回
  • 执行计划的返回结果中尽量不要出现以下stage:

    1
    2
    3
    4
    5
    - COLLSCAN(全表扫描)
    - SORT(使用sort但是无index)
    - 不合理的SKIP
    - SUBPLA(未用到index的$or)
    - COUNTSCAN(不使用index进行count)

实战

  • 下面是一个结合各种索引类型的示例,假设我们正在为一个电商应用创建和管理一个MongoDB集合products,其中包含以下字段:

1
2
3
4
5
6
_id: 默认的ObjectId类型,作为主键(已自动带有唯一性索引)。
category: 商品分类,字符串类型。
brand: 商品品牌,字符串类型。
tags: 商品标签数组,包含多个字符串元素。
price: 商品价格,数字类型。
lastUpdated: 商品最后更新时间,日期类型。
  • 创建集合与插入文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 假设已经连接到数据库并选择了一个database
db.createCollection("products");

// 插入一些示例数据
db.products.insertMany([
{
category: "Electronics",
brand: "Apple",
tags: ["smartphone", "ios"],
price: 999,
lastUpdated: ISODate("2023-03-01T00:00:00Z")
},
// ...其他商品文档
]);
  • 单键索引 - 查询商品按价格排序

1
2
db.products.createIndex({ price: 1 });
db.products.find().sort({ price: 1 }); // 获取所有商品按价格升序排列
  • 复合索引 - 按品牌和价格查询,并进行排序:

1
2
db.products.createIndex({ brand: 1, price: -1 });
db.products.find({ brand: "Apple" }).sort({ price: -1 }); // 获取指定品牌的商品并按价格降序排列
  • 多键索引 - 根据商品标签进行搜索

1
2
db.products.createIndex({ "tags": 1 }, { "sparse": true }); // 如果不是每个文档都有tags,可以使用sparse选项以节省空间
db.products.find({ "tags": "smartphone" }); // 找到所有带有“smartphone”标签的商品
  • 唯一索引 - 确保品牌名称不重复

1
2
3
4
db.products.createIndex({ brand: 1 }, { unique: true });
// 尝试插入重复记录时
db.products.insertOne({ category: "Electronics", brand: "Apple", ... }); // 正常插入
db.products.insertOne({ category: "Computers", brand: "Apple", ... }); // 若数据库中已有Apple品牌,则会因违反唯一性而报错