对于任何数据库(包括 MongoDB)来说,索引都是开发过程中的关键组成部分。没有索引时,随着数据库规模增长,性能会随之下降;而有了合理的索引,数据量增加的同时,访问性能仍能保持在最佳状态。
在本文中,我们将探讨 MongoDB 中可用的多种索引类型,分享一些优化使用体验的技巧,并说明如何确保您创建的索引真正被使用。如果您仍在学习 MongoDB,建议查看 Introduction to MongoDB 课程。
MongoDB 索引的重要性与原因
谈到索引,本质上就是在谈性能。索引决定了我们是要遍历集合中的每一条文档来匹配查询,还是只扫描一小批更可能匹配查询的文档。如果集合很小、需要扫描的文档不多,无论如何性能都不差。但如果集合里有 100,000 条文档,而只有 100 条会匹配查询,那么在没有基于这 100 条文档特征建立索引的情况下,扫描全部 100,000 条文档去找那 100 条,很可能会耗时更长。
MongoDB 中有多种索引类型:
- 单字段(Single field):对文档中的某一个特定字段建立的索引。默认的
_id 字段索引可视为单字段索引。
- 复合(Compound):基于文档的多个字段建立的索引,查询中字段的使用顺序以及索引中字段的顺序都很重要。
- 多键(Multikey):用于数组字段的索引。
- 通配(Wildcard):支持不同文档间字段名略有差异的场景。这类索引的性能通常不如其他索引。
- 地理空间(Geospatial):用于提升基于经纬度坐标数据的地理空间查询性能的索引。
- 哈希(Hashed):最适合与分片键(shard key)配合使用的索引。
关于各种索引类型的更多信息,请查看 MongoDB 文档。
多数情况下,您会使用单字段、复合或多键索引,但最终还是要视您的应用类型、查询方式和数据而定。
凡是涉及读取文档的数据库交互,索引都很重要。这不仅限于使用 find 或 findOne 检索文档。在更新和删除文档时,您同样会使用匹配条件,数据库会先根据该条件找到文档再执行更新或删除。
不仅 find 和 findOne 这类检索操作能从索引中受益。当您在聚合管道的首个阶段使用 $match 时,索引也能显著加速。
MongoDB 索引优化的实用技巧
随意创建索引在 MongoDB 中可能跟不使用索引一样低效。相反,考虑以下几点,有助于构建更高效的应用。
复合索引的 ESR 原则:Equality、Sort、Range
创建复合索引的经验法则,是确定查询中每个字段的用法,并尽量遵循 ESR 最佳实践(等值、排序、范围)。
ESR 是什么?我们来拆解一下:
- Equality(等值):字段按精确匹配(而非模糊匹配)。例如,在查询中精确匹配某人的姓名就是等值阶段。
- Sort(排序):用于按升序或降序排序的字段。
- Range(范围):用于非精确匹配的字段。例如,使用
$lt 或 $gt 运算符会在某个范围内匹配文档,而非精确匹配。
为了更好地理解其含义,请看下面的查询:
db.employee.find(
{
"department": "Developer Relations",
"salary": {
"$lt": 80000
}
}
).sort(
{
"name": 1
}
);技术支持
上述查询对 department 做了等值匹配,对 salary 做了范围匹配,并按 name 进行排序。
遵循 ESR 原则,索引可以这样设计:
db.employee.createIndex({
"department": 1,
"name": 1,
"salary": 1
});技术支持
我们遵循 ESR 的目的是避免内存排序,并尽可能缩小搜索空间,从而实现高性能的查询执行。
覆盖查询(Covered queries)
一般来说,索引能让查询时需要扫描的文档子集更小,从而提升性能。但如果您希望进一步提升应用性能,可以考虑覆盖查询。覆盖查询意味着所有结果都直接来自索引,而无需扫描集合中的任何文档。
要实现覆盖查询,有几点限制,是否可行取决于您的应用:
- 匹配条件和排序必须与索引的定义完全一致。
- 结果集中请求返回的字段也必须包含在索引中。
- 在分片场景下,分片键需要是索引的一部分。
这具体意味着什么?
以如下索引为例:
db.department.createIndex({
"country": 1,
"salary": 1
});技术支持
在上例中,我们希望基于 country 和 salary 字段建立索引。然后可以运行如下查询:
db.department.find(
{
"country": "US"
},
{
"country": 1,
"salary": 1
}
);技术支持
上述带有投影的查询会使用我们的索引,但并非覆盖查询。因为在 MongoDB 中,_id 字段默认会返回,而在本例中我们并未将 _id 添加到索引中。因此可以更改索引,或修改查询。若选择修改查询,可以这样做:
db.department.find(
{
"country": "US"
},
{
"country": 1,
"salary": 1,
"_id": 0
}
);技术支持
请记住,若要使用覆盖查询,索引必须有效,且请求的字段需包含在索引中。否则,您只是借助索引来定位文档,而非直接在索引中返回结果。
您可以通过在 explain 操作符中使用 executionStats 参数(本文稍后会提到)来验证是否使用了覆盖查询。totalDocsExamined 的值可用于判断:如果为 0,说明您在使用覆盖查询。
减少或移除不必要的索引
从服务器资源的角度看,索引并非“免费”。每个索引都需要由服务器维护,而维护会消耗 CPU、内存和磁盘。创建更多索引会带来更多开销,在资源不足的服务器上可能导致成本增加或性能降低。这种性能下降不仅会影响读取,也可能会波及写入和删除操作,因为 MongoDB 需要消耗更多资源。
建议仅为高频查询创建索引。每月只执行一次的查询真的需要索引吗?大概不需要。每 10 秒执行一次的查询需要索引吗?当然需要!
在 MongoDB 博客上了解更多索引 最佳实践。
如何判断您的 MongoDB 索引是否在工作?
在 MongoDB 中创建了索引,并不意味着您的查询一定会使用它。我们实际上可以对任一查询进行排查,了解其使用了哪个索引。
以如下索引为例:
db.people.createIndex({
"name": 1,
"is_active": 1
});技术支持
在上述示例中,我们为 name 和 is_active 字段建立了索引。现在我们运行如下查询:
db.people.find({
"is_active": true
});技术支持
无论是否有有效索引,上述查询都能运行;如果数据集很小,无论如何都会很快。但如果我们想确认它是否使用了索引,可以对查询稍作调整:
db.people.find({
"is_active": true
}).explain();技术支持
这一次,我们使用 explain 方法来查看查询的执行计划。在执行计划中,可以看到如下信息:
winningPlan: {
isCached: false,
stage: 'COLLSCAN',
filter: { is_active: { '$eq': true } },
direction: 'forward'
}技术支持
对于该查询,MongoDB 使用了 "COLLSCAN",这意味着即使查询包含了我们索引中的一个字段,它也没有使用该索引。如果我们将查询改为如下形式:
db.people.find({
"name": "Nic Raboy",
"is_active": true
}).explain();技术支持
这次 explain 的结果发生了变化:
winningPlan: {
isCached: false,
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { name: 1, is_active: 1 },
indexName: 'name_1_is_active_1',
isMultiKey: false,
multiKeyPaths: { name: [], is_active: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
name: [ '["Nic Raboy", "Nic Raboy"]' ],
is_active: [ '[true, true]' ]
}
}
},技术支持
这一次,执行计划显示我们使用了 "IXSCAN",并给出了使用的索引名称,在本例中即 name_1_is_active_1。该名称是在创建索引时由系统自动定义的。
对于 update 操作,流程基本类似,不同之处在于 explain 函数的位置。
一次 update 操作可能如下所示:
db.people.explain().update(
{
“name”: “Nic Raboy”,
“Is_active”: true
},
{
“$set”: { “is_active”: false }
}
);技术支持
请注意,上述示例中,explain 出现在 update 函数之前。
如果在更新操作中使用了预期的索引,您会看到 "IXSCAN" 以及索引名称。了解更多 更新操作及其与索引的配合使用。
另一种查看查询执行计划细节的方法是对数据库使用 runCommand。尽管结果不如前者直观,但功能很强大,类似如下:
db.runCommand({
"explain": {
"update": "people",
"updates": [
{
"q": {
"name": "Nic Raboy",
"is_active": true
},
"u": {
"$set": { "is_active": false }
}
}
]
},
"verbosity": "allPlansExecution"
});技术支持
上述方式与带 explain 的 update 操作作用相同。
如果您对创建索引不太有把握,或发现查询执行比预期更慢,不妨查看执行细节。
结语
本文介绍了 MongoDB 索引的一些最佳实践。虽然我们没有覆盖所有可能的索引情境,但这些内容可以作为起点,帮助您确保用户在使用数据时获得尽可能好的体验。
重申几点要点:
- 尽可能遵循 ESR(等值、排序、范围)原则。
- 如有可能,尝试使用覆盖查询。
- 不要盲目创建索引,而应找出最常用的查询,以及这些查询会提取多少数据。
- 借助
explain 操作符测试索引及其性能。
以上并非提升应用性能的全部索引最佳实践。还有其他因素,例如 基数(cardinality) 等。
若需更多索引方面的帮助,请查看 MongoDB 文档。如果您在 MongoDB Atlas 中使用付费层,可以考虑使用内置的一些 性能顾问工具来获取索引和查询建议。
对于任何数据库(包括 MongoDB)来说,索引都是开发过程中的关键组成部分。没有索引时,随着数据库规模增长,性能会随之下降;而有了合理的索引,数据量增加的同时,访问性能仍能保持在最佳状态。
在本文中,我们将探讨 MongoDB 中可用的多种索引类型,分享一些优化使用体验的技巧,并说明如何确保您创建的索引真正被使用。如果您仍在学习 MongoDB,建议查看 Introduction to MongoDB 课程。
MongoDB 索引的重要性与原因
谈到索引,本质上就是在谈性能。索引决定了我们是要遍历集合中的每一条文档来匹配查询,还是只扫描一小批更可能匹配查询的文档。如果集合很小、需要扫描的文档不多,无论如何性能都不差。但如果集合里有 100,000 条文档,而只有 100 条会匹配查询,那么在没有基于这 100 条文档特征建立索引的情况下,扫描全部 100,000 条文档去找那 100 条,很可能会耗时更长。
MongoDB 中有多种索引类型:
_id字段索引可视为单字段索引。关于各种索引类型的更多信息,请查看 MongoDB 文档。
多数情况下,您会使用单字段、复合或多键索引,但最终还是要视您的应用类型、查询方式和数据而定。
凡是涉及读取文档的数据库交互,索引都很重要。这不仅限于使用
find或findOne检索文档。在更新和删除文档时,您同样会使用匹配条件,数据库会先根据该条件找到文档再执行更新或删除。不仅
find和findOne这类检索操作能从索引中受益。当您在聚合管道的首个阶段使用$match时,索引也能显著加速。MongoDB 索引优化的实用技巧
随意创建索引在 MongoDB 中可能跟不使用索引一样低效。相反,考虑以下几点,有助于构建更高效的应用。
复合索引的 ESR 原则:Equality、Sort、Range
创建复合索引的经验法则,是确定查询中每个字段的用法,并尽量遵循 ESR 最佳实践(等值、排序、范围)。
ESR 是什么?我们来拆解一下:
$lt或$gt运算符会在某个范围内匹配文档,而非精确匹配。为了更好地理解其含义,请看下面的查询:
上述查询对 department 做了等值匹配,对 salary 做了范围匹配,并按 name 进行排序。
遵循 ESR 原则,索引可以这样设计:
我们遵循 ESR 的目的是避免内存排序,并尽可能缩小搜索空间,从而实现高性能的查询执行。
覆盖查询(Covered queries)
一般来说,索引能让查询时需要扫描的文档子集更小,从而提升性能。但如果您希望进一步提升应用性能,可以考虑覆盖查询。覆盖查询意味着所有结果都直接来自索引,而无需扫描集合中的任何文档。
要实现覆盖查询,有几点限制,是否可行取决于您的应用:
这具体意味着什么?
以如下索引为例:
在上例中,我们希望基于
country和salary字段建立索引。然后可以运行如下查询:上述带有投影的查询会使用我们的索引,但并非覆盖查询。因为在 MongoDB 中,
_id字段默认会返回,而在本例中我们并未将_id添加到索引中。因此可以更改索引,或修改查询。若选择修改查询,可以这样做:请记住,若要使用覆盖查询,索引必须有效,且请求的字段需包含在索引中。否则,您只是借助索引来定位文档,而非直接在索引中返回结果。
您可以通过在
explain操作符中使用executionStats参数(本文稍后会提到)来验证是否使用了覆盖查询。totalDocsExamined的值可用于判断:如果为 0,说明您在使用覆盖查询。减少或移除不必要的索引
从服务器资源的角度看,索引并非“免费”。每个索引都需要由服务器维护,而维护会消耗 CPU、内存和磁盘。创建更多索引会带来更多开销,在资源不足的服务器上可能导致成本增加或性能降低。这种性能下降不仅会影响读取,也可能会波及写入和删除操作,因为 MongoDB 需要消耗更多资源。
建议仅为高频查询创建索引。每月只执行一次的查询真的需要索引吗?大概不需要。每 10 秒执行一次的查询需要索引吗?当然需要!
在 MongoDB 博客上了解更多索引 最佳实践。
如何判断您的 MongoDB 索引是否在工作?
在 MongoDB 中创建了索引,并不意味着您的查询一定会使用它。我们实际上可以对任一查询进行排查,了解其使用了哪个索引。
以如下索引为例:
在上述示例中,我们为
name和is_active字段建立了索引。现在我们运行如下查询:无论是否有有效索引,上述查询都能运行;如果数据集很小,无论如何都会很快。但如果我们想确认它是否使用了索引,可以对查询稍作调整:
这一次,我们使用
explain方法来查看查询的执行计划。在执行计划中,可以看到如下信息:对于该查询,MongoDB 使用了 "COLLSCAN",这意味着即使查询包含了我们索引中的一个字段,它也没有使用该索引。如果我们将查询改为如下形式:
这次
explain的结果发生了变化:这一次,执行计划显示我们使用了 "IXSCAN",并给出了使用的索引名称,在本例中即
name_1_is_active_1。该名称是在创建索引时由系统自动定义的。对于
update操作,流程基本类似,不同之处在于explain函数的位置。一次
update操作可能如下所示:请注意,上述示例中,
explain出现在update函数之前。如果在更新操作中使用了预期的索引,您会看到 "IXSCAN" 以及索引名称。了解更多 更新操作及其与索引的配合使用。
另一种查看查询执行计划细节的方法是对数据库使用
runCommand。尽管结果不如前者直观,但功能很强大,类似如下:上述方式与带
explain的 update 操作作用相同。如果您对创建索引不太有把握,或发现查询执行比预期更慢,不妨查看执行细节。
结语
本文介绍了 MongoDB 索引的一些最佳实践。虽然我们没有覆盖所有可能的索引情境,但这些内容可以作为起点,帮助您确保用户在使用数据时获得尽可能好的体验。
重申几点要点:
explain操作符测试索引及其性能。以上并非提升应用性能的全部索引最佳实践。还有其他因素,例如 基数(cardinality) 等。
若需更多索引方面的帮助,请查看 MongoDB 文档。如果您在 MongoDB Atlas 中使用付费层,可以考虑使用内置的一些 性能顾问工具来获取索引和查询建议。