文章目录
阅读本文前可以先阅读以下文章:
- MongoDB快速入门(MongoDB简介、MongoDB的应用场景、MongoDB中的基本概念、MongoDB的数据类型、MongoDB的安装与部署、MongoDB的常用命令)
- MongoDB的常用命令(数据库操作、集合操作、文档操作)
1. 索引概述
MongoDB 索引的官网文档:索引-MongoDB手册
索引支持在 MongoDB 中高效执行查询。如果没有索引,MongoDB 就必须扫描集合中的每个文档以返回查询结果。如果查询存在适当的索引,MongoDB 就可以使用该索引来限制其必须扫描的文档数
索引可提高查询性能,但添加索引会影响写入操作的性能。对于写入读取率高的集合,由于每次插入操作都必须同时更新所有索引,因此会带来较高的索引成本
MongoDB 索引使用 B-Tree 数据结构(MySQL 是 B+Tree)
2. 索引的类型
2.1 单字段索引
MongoDB 支持在文档的单个字段上创建用户定义的升序索引或降序索引,称为单字段索引(Single Field Index)
对于单个字段索引和排序操作,索引键的排序顺序(即升序或降序)并不重要,因为 MongoDB 可以在任何方向上遍历索引
2.2 复合索引
MongoDB 支持多个字段的自定义索引,即复合索引(Compound Index)
复合索引中列出的字段顺序具有重要意义。例如,如果复合索引由 { user_id: 1, score: -1 } 组成,则索引首先会按 user_id 正序排序,然后在每个 user_id 的值内,再按 score 倒序排序
2.3 其他索引
2.3.1 地理空间索引(Geospatial Index)
为了支持对地理空间坐标数据的有效查询,MongoDB 提供了两种特殊的索引:返回结果时使用平面几何的二维索引和返回结果时使用球面几何的二维球面索引
2.3.2 文本索引(Text Indexes)
文本索引的特点:
- 分词:MongoDB 在创建文本索引时会对字段内容进行分词处理,将文本分解成单词或术语(tokens)
- 权重:可以为不同的字段指定不同的权重,以便在搜索时影响文档的相关性得分
- 停用词:MongoDB 会忽略某些常用词(如 “the”、“and” 等),这些词被称为停用词。MongoDB 有一个内置的停用词列表,也可以自定义停用词列表
- 语言支持:MongoDB 的文本索引支持多种语言的分词和搜索
注意事项:
- 文本索引不存储停止词和词干。这意味着它们不会影响索引的大小
- 文本索引不能用于文本字段中的二进制数据
- 文本索引不能用于数组字段中的字符串元素
$text
查询不能与$
或$$
运算符一起使用
2.3.3 哈希索引(Hashed Indexes)
为了支持基于散列的分片,MongoDB 提供了散列索引类型,它对字段值的散列进行索引。这些索引在其范围内的值分布更加随机,但只支持相等匹配,不支持基于范围的查询
3. 索引相关操作
3.1 查看索引
查看索引的语法
db.collection.getIndexes()
查看 comment 集合中所有的索引
db.collection.getIndexes()
查询结果如下
[ { v: 2, key: { _id: 1 }, name: '_id_' } ]
结果中显示的是默认的 _id_
索引(MongoDB 在创建集合的过程中,会在 _id
字段上创建一个唯一的索引,默认名字为 _id_
,该索引可防止插入两个具有相同 _id
值的文档)
- _id 索引是唯一索引,因此 _id 值不能重复
- 在分片集群中,通常使用 _id 作为片键
3.2 创建索引
创建索引的语法
db.collection.createIndex(keys, options)
参数名 | 类型 | 描述 | 必需 |
---|---|---|---|
keys | 文档 | 指定索引的字段和索引类型。对于文本索引,字段类型应该设置为 "text" 。可以指定单个字段或多个字段 | 是 |
options | 文档 | 索引的额外选项。这是一个可选参数,可以包含多个字段 | 否 |
options
参数的详细说明:
选项 | 类型 | 描述 | 默认值 |
---|---|---|---|
background | 布尔 | 是否在后台创建索引。如果为 true ,MongoDB 将在后台创建索引,这样就不会阻塞其他数据库操作 | false |
unique | 布尔 | 是否创建唯一索引。如果为 true ,则索引字段的值必须是唯一的 | false |
name | 字符串 | 索引的名称。如果未指定,MongoDB 会自动生成一个名称 | 自动生成 |
partialFilterExpression | 文档 | 指定部分索引的过滤条件。只有满足条件的文档才会包含在索引中 | 无 |
sparse | 布尔 | 是否创建稀疏索引。如果为 true ,则索引只包含具有指定字段的文档,忽略没有该字段的文档 | false |
expireAfterSeconds | 整数 | 对于具有日期类型的字段,指定文档在集合中的生存时间(TTL) | 无 |
storageEngine | 文档 | 指定索引的存储引擎配置 | 使用默认存储引擎配置 |
weights | 文档 | 对于文本索引,为索引中的每个字段指定权重。权重越高,字段在文本搜索中的相关性得分越高 | 所有字段权重相等 |
3.3.1 创建单字段索引
对 user_id 字段建立单字段索引
db.comment.createIndex({ user_id: 1 });
再次查看 comment 集合中所有的索引
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { user_id: 1 }, name: 'user_id_1' }
]
3.3.2 创建复合索引
对 user_id 和 nickname 建立复合(Compound)索引
db.comment.createIndex({ userid: 1, nickname: -1 });
再次查看 comment 集合中所有的索引
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { user_id: 1 }, name: 'user_id_1' },
{
v: 2,
key: { userid: 1, nickname: -1 },
name: 'userid_1_nickname_-1'
}
]
3.3.3 创建文本索引
对 content 字段建立单字段索引
db.comment.createIndex({ content: "text" });
3.4 移除索引
3.4.1 移除指定索引
db.collection.dropIndex(index);
删除 comment 集合中 user_id 字段上的升序索引
db.comment.dropIndex(
{
user_id: 1
}
);
3.4.2 移除所有索引
db.collection.dropIndexes()
删除 comment 集合中的所有索引
db.comment.dropIndexes()
_id 的字段的索引是无法删除的,只能删除非 _id 字段的索引
4. 索引的使用
4.1 执行计划
分析查询性能(Analyze Query Performance)通常使用执行计划(Explain Plan)来查看查询的情况,如查询耗费的时间、是否基于索引查询等
db.collection.find(query,options).explain(options)
参数名 | 类型 | 描述 | 必需 |
---|---|---|---|
query | document | 查询选择器,用于指定查询条件 | 是 |
options | document | 可选的。用于修改查询的默认行为的各种选项,如排序、限制等 | 否 |
explain | function | 用于获取查询执行计划的详细信息 | 是(调用时) |
explainOptions | document | 可选的。用于修改解释操作的默认行为的各种选项 | 否 |
每个参数的详细解释:
query
: 这是一个文档,用于定义查询条件。它可以是简单的字段等值查询,也可以是复杂的条件组合,包括逻辑运算符和正则表达式options
: 这是一个文档,可以包含多个键,用于控制查询的行为。常见的选项包括:sort
: 排序条件limit
: 限制返回的文档数量skip
: 跳过文档的数量。projection
: 投影,用于指定返回的字段
explain
: 这是一个函数,当你在查询后面调用它时,MongoDB 不会返回查询结果,而是返回查询执行计划的详细信息。这有助于理解查询是如何执行的,以及如何优化查询explainOptions
: 这是一个文档,用于控制解释操作的输出。例如,可以指定是否返回所有阶段的执行计划或者只返回获胜计划
根据 user_id 字段查询数据
db.comment.find({ user_id: "1003" }).explain();
MongoDB 返回的结果
{
explainVersion: '1',
queryPlanner: {
namespace: 'test.comment',
parsedQuery: {
user_id: {
'$eq': '1003'
}
},
indexFilterSet: false,
queryHash: 'B7F3AE51',
planCacheKey: '8C1EE785',
optimizationTimeMillis: 0,
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
prunedSimilarIndexes: false,
winningPlan: {
isCached: false,
stage: 'COLLSCAN',
filter: {
user_id: {
'$eq': '1003'
}
},
direction: 'forward'
},
rejectedPlans: []
},
command: {
find: 'comment',
filter: {
user_id: '1003'
},
'$db': 'test'
},
serverInfo: {
host: 'LAPTOP-G7HILK54',
port: 27017,
version: '8.0.3',
gitVersion: '89d97f2744a2b9851ddfb51bdf22f687562d9b06'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
internalQueryFrameworkControl: 'trySbeRestricted',
internalQueryPlannerIgnoreIndexWithCollationForRegex: 1
},
ok: 1
}
重点关注 winningPlan 中的 stage 字段(COLLSCAN 表示全盘扫描)
下面对 user_id 字段建立索引
db.comment.createIndex({ user_id: 1 });
再次查看执行计划
{
explainVersion: '1',
queryPlanner: {
namespace: 'test.comment',
parsedQuery: {
user_id: {
'$eq': '1003'
}
},
indexFilterSet: false,
queryHash: 'B7F3AE51',
planCacheKey: '57E4C731',
optimizationTimeMillis: 0,
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
prunedSimilarIndexes: false,
winningPlan: {
isCached: false,
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: {
user_id: 1
},
indexName: 'user_id_1',
isMultiKey: false,
multiKeyPaths: {
user_id: []
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
user_id: [
'["1003", "1003"]'
]
}
}
},
rejectedPlans: []
},
command: {
find: 'comment',
filter: {
user_id: '1003'
},
'$db': 'test'
},
serverInfo: {
host: 'LAPTOP-G7HILK54',
port: 27017,
version: '8.0.3',
gitVersion: '89d97f2744a2b9851ddfb51bdf22f687562d9b06'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
internalQueryFrameworkControl: 'trySbeRestricted',
internalQueryPlannerIgnoreIndexWithCollationForRegex: 1
},
ok: 1
}
可以发现,stage 字段已经变成了 FETCH,inputStage 属性里面的 stage 字段变成了 IXSCAN(基于索引的扫描)
4.2 执行计划中各个字段的含义
字段名 | 描述 |
---|---|
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 | 命令是否成功执行的标志。 |
4.3 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 | 查询结束。 |
4.4 覆盖查询
当查询条件和查询的投影仅包含索引字段时,MongoDB 会直接从索引返回结果,而不扫描任何文档或将文档带入内存,这些覆盖的查询非常高效(类似于 MySQL 中的覆盖索引)
db.comment.find(
{ user_id: "1003" },
{ user_id: 1, _id: 0 }
).explain();
MongoDB 返回的结果
{
explainVersion: '1',
queryPlanner: {
namespace: 'test.comment',
parsedQuery: {
user_id: {
'$eq': '1003'
}
},
indexFilterSet: false,
queryHash: 'DC80EEEF',
planCacheKey: 'B8237218',
optimizationTimeMillis: 0,
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
prunedSimilarIndexes: false,
winningPlan: {
isCached: false,
stage: 'PROJECTION_COVERED',
transformBy: {
user_id: 1,
_id: 0
},
inputStage: {
stage: 'IXSCAN',
keyPattern: {
user_id: 1
},
indexName: 'user_id_1',
isMultiKey: false,
multiKeyPaths: {
user_id: []
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
user_id: [
'["1003", "1003"]'
]
}
}
},
rejectedPlans: []
},
command: {
find: 'comment',
filter: {
user_id: '1003'
},
projection: {
user_id: 1,
_id: 0
},
'$db': 'test'
},
serverInfo: {
host: 'LAPTOP-G7HILK54',
port: 27017,
version: '8.0.3',
gitVersion: '89d97f2744a2b9851ddfb51bdf22f687562d9b06'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
internalQueryFrameworkControl: 'trySbeRestricted',
internalQueryPlannerIgnoreIndexWithCollationForRegex: 1
},
ok: 1
}