作者选择了 开放式互联网 / 自由言论基金作为 写给捐赠计划的一部分接受捐款。
介绍
MongoDB 是一个以文档为导向的数据库管理系统,允许您在文档中存储大量数据,其大小和结构可能不同。MongoDB 具有强大的查询机制,允许您根据特定标准过滤文档。
MongoDB在查询方面所提供的灵活性可能会使数据库引擎难以预测使用的查询类型最频繁;它必须准备好寻找文档,无论收藏的大小如何。
索引是数据库管理员可以用来有意识地帮助数据库引擎并提高其性能的最重要的工具之一. 在本教程中,您将学习索引是什么,如何创建它们,以及如何检查它们在数据库执行查询时如何使用。
前提条件
要遵循本教程,您将需要:
- 一个拥有
sudo
特权和与 UFW 配置的防火墙的常规非根用户的服务器. 本教程是使用运行 Ubuntu 20.04 的服务器进行验证的,您可以通过遵循此(Ubuntu 20.04 的初始服务器设置教程)(https://andsky.com/tech/tutorials/initial-server-setup-with-ubuntu-20-04)来准备您的服务器。 - MongoDB 安装在您的服务器上。 要设置此项,请遵循我们的教程(How to Install MongoDB on Ubuntu 20.04)(https://andsky.com/tech/tutorials/how-to-install-mongodb-on-ubuntu-20-04)。
- 您的服务器的 MongoDB 实例通过允许身份验证和创建管理用户进行安全。 为了确保 MongoDB 像这样,请遵循我们的教程(How To Secure MongoDB on Ubuntu 20.04)(https://andsky.com/tech/tutorials/how-to-secure-mongodb-on-ubuntu-20-04). _
<$>[注] **注:**有关如何配置服务器,安装,然后安全MongoDB安装的链接教程参考Ubuntu 20.04.本教程专注于MongoDB本身,而不是潜在的操作系统。
了解指数
通常情况下,当您查询 MongoDB 数据库以获取符合特定条件的文档时,例如高达 8000 米的山峰时,该数据库必须进行集合扫描以找到它们,这意味着它会从集合中获取每个文档以验证它们是否符合条件。
这种机制在许多用例中工作得很好,但当收藏越来越大时,它会变得显著缓慢;如果收藏中存储的文档很复杂,那么读取并分析其内容可能是一个昂贵的操作。
索引是特殊的数据结构,仅存储一个集合的文档中包含的数据的一个小子集合,与文档本身分开。
为了帮助理解索引,想象一个数据库收集存储产品在一个在线商店. 每个产品都由一个包含图像,详细描述,类别关系和许多其他字段的文档表示。
没有任何索引,MongoDB 将需要从收藏中获取每个产品,并在文档结构中检查库存信息. 然而,通过索引,MongoDB 将保持一个单独的,更小的列表,只包含指向库存产品。
在以下步骤中,您将准备一个样本数据库,并使用它创建不同类型的索引. 您将学习如何验证索引是否在执行查询时使用。
步骤 1 – 准备样本数据库
为了了解索引是如何工作的,以及如何创建索引,此步骤概述了如何打开 MongoDB 壳以连接到本地安装的 MongoDB 实例。
要创建此样本集合,请连接到 MongoDB 壳作为您的管理用户。本教程遵循前提教程(MongoDB 安全教程)(https://andsky.com/tech/tutorials/how-to-secure-mongodb-on-ubuntu-20-04)的惯例,并假定该管理用户的名称是 AdminSammy,其身份验证数据库是 admin
。
1mongo -u AdminSammy -p --authenticationDatabase admin
输入您在安装过程中设置的密码以获取存取壳. 提供密码后,您的提示将更改为大于符号(>
)。
<$>[注]
注: 新连接时,MongoDB 壳将默认情况下自动连接到测试
数据库,您可以安全地使用此数据库来实验 MongoDB 和 MongoDB 壳。
或者,您也可以切换到另一个数据库以运行本教程中提供的所有示例命令. 要切换到另一个数据库,请运行使用
命令,然后是您的数据库名称:
1use database_name
美元
为了说明指数是如何工作的,我们需要一系列具有不同类型的多个字段的文档,我们将使用世界五座最高山的样本集合。
1[label The Everest document]
2{
3 "name": "Everest",
4 "height": 8848,
5 "location": ["Nepal", "China"],
6 "ascents": {
7 "first": {
8 "year": 1953,
9 },
10 "first_winter": {
11 "year": 1980,
12 },
13 "total": 5656,
14 }
15}
本文件包含以下信息:
- `名称':最高峰的名称。 *'高':最高峰为高地,以米为单位.
- " 地点 " :山地所在国家。 此字段存储值为数组, 允许位于一个以上国家的山脉 。
scents
: 此字段的值是另一个文档 。 当一个文档存储在这样的另一个文档中时,它被称为_嵌入_或_nested_文档. 每一份 " 升起 " 文件都描述了该山的成功升起。 具体地说,每份 " 升起 " 文件都包含一个 " 总 " 字段,列出每个高峰的成功升起总数。 此外,每个嵌入式文件包含两个字段,其值也是嵌入式文件: -第一':这个字段的价值是一个嵌入式文件,其中包含一个字段
年份',它描述了第一个总体成功上升的年份。 -`第一个冬季': 这个字段的值是一个嵌入式文件,其中也包含一个"年"字段,其值代表给定山上第一个成功冬季起落的年份. (英语)
在 MongoDB 壳中运行以下insertMany()
方法,同时创建一个名为peaks
的集合,并将五个样本文档插入其中。
1db.peaks.insertMany([
2 {
3 "name": "Everest",
4 "height": 8848,
5 "location": ["Nepal", "China"],
6 "ascents": {
7 "first": {
8 "year": 1953
9 },
10 "first_winter": {
11 "year": 1980
12 },
13 "total": 5656
14 }
15 },
16 {
17 "name": "K2",
18 "height": 8611,
19 "location": ["Pakistan", "China"],
20 "ascents": {
21 "first": {
22 "year": 1954
23 },
24 "first_winter": {
25 "year": 1921
26 },
27 "total": 306
28 }
29 },
30 {
31 "name": "Kangchenjunga",
32 "height": 8586,
33 "location": ["Nepal", "India"],
34 "ascents": {
35 "first": {
36 "year": 1955
37 },
38 "first_winter": {
39 "year": 1986
40 },
41 "total": 283
42 }
43 },
44 {
45 "name": "Lhotse",
46 "height": 8516,
47 "location": ["Nepal", "China"],
48 "ascents": {
49 "first": {
50 "year": 1956
51 },
52 "first_winter": {
53 "year": 1988
54 },
55 "total": 461
56 }
57 },
58 {
59 "name": "Makalu",
60 "height": 8485,
61 "location": ["China", "Nepal"],
62 "ascents": {
63 "first": {
64 "year": 1955
65 },
66 "first_winter": {
67 "year": 2009
68 },
69 "total": 361
70 }
71 }
72])
输出将包含新插入对象分配的对象标识符列表。
1[secondary_label Output]
2{
3 "acknowledged" : true,
4 "insertedIds" : [
5 ObjectId("61212a8300c8304536a86b2f"),
6 ObjectId("61212a8300c8304536a86b30"),
7 ObjectId("61212a8300c8304536a86b31"),
8 ObjectId("61212a8300c8304536a86b32"),
9 ObjectId("61212a8300c8304536a86b33")
10 ]
11}
您可以通过运行无参数的 find()
方法来验证文档是否正确插入,该方法将检索所有文档:
1db.peaks.find()
1[secondary_label Output]
2{ "_id" : ObjectId("61212a8300c8304536a86b2f"), "name" : "Everest", "height" : 8848, "location" : [ "Nepal", "China" ], "ascents" : { "first" : { "year" : 1953 }, "first_winter" : { "year" : 1980 }, "total" : 5656 } }
3
4...
请注意,本示例集合不够大,可以直接说明索引的性能影响或缺少索引,但是,本指南将概述MongoDB如何使用索引来限制经过的文档数量,通过凸显数据库引擎报告的查询细节。
有了样本数据,您可以继续到下一步,了解如何基于单个字段创建索引。
步骤 2 – 创建单域索引并评估索引使用
本步骤解释了如何创建单个字段索引,以便加速使用该字段过滤数据的文档查询,作为过滤条件的一部分。
首先,运行以下查询:通常,查询文档{
height: { $gt: 8700 }
会导致此查询检索描述山峰的任何文档,其height
值大于8700。但是,此操作包括explain(executionStats)
方法,这会导致查询返回有关查询如何执行的信息。由于您尚未创建任何索引,这将为您提供一个基准,您可以使用它来比较使用索引的查询的性能:
1db.peaks.find(
2 { "height": { $gt: 8700 } }
3).explain("executionStats")
此操作返回了大量信息. 下面的示例输出删除了本教程中不重要的几个行:
1[secondary_label Output]
2{
3 "queryPlanner" : {
4 . . .
5 "winningPlan" : {
6 "stage" : "COLLSCAN",
7 . . .
8 },
9 },
10 . . .
11 "executionStats" : {
12 . . .
13 "nReturned" : 1,
14 "executionTimeMillis" : 0,
15 "totalKeysExamined" : 0,
16 "totalDocsExamined" : 5,
17 . . .
18 },
19 . . .
20}
此输出中返回的下列字段对于了解索引如何工作尤为重要:
- 获奖者 计划: 在`queryPlanner ' 一节中,这份文件描述了MongoDB如何决定执行查询。 根据查询类型, " 获奖计划 " 的详细结构可能有所不同,但这里需要注意的关键是 " COLLSCAN " 。 这个值的存在意味着MongoDB需要通过全部的集合而没有任何辅助物来找到所要求的文件.
- `返回': 此值告诉您通过给定查询返回了多少文档 。 这里,只有一个山峰 符合查询。
- 执行时间: 此值代表执行时间 。 由于收藏量如此之小,其重要性微不足道. 然而,在分析对较大或更复杂的集合的查询的性能时,这是必须铭记的重要尺度.
总键快取' : 这能告诉你有多少**index**条目Mongo DB 检查以找到请求的文件 。 因为收集扫描被使用,你还没有创建任何索引,所以值是"0". *
总的DocsExcined': 这一数值表明MongoDB需要从收藏中读取多少文件。 因为蒙哥 DB进行了收藏扫描,其价值为 " 5 " ,即收藏中所有文件的总数。 收藏量越大,当不使用索引时,这一领域的价值就越大. (英语)
请注意检查的文件总数和返回的文件计数之间的差异:MongoDB必须检查5份文件才能返回一份。
本教程将在以后的部分中参考这些值,以分析索引如何影响查询执行的方式。
为此,使用createIndex()
方法创建高
集合中的高
字段的索引。该方法接受描述你要创建的索引的JSON文档。本示例将创建一个单一的字段索引,这意味着文档中包含我们想要使用的字段的单个密钥(高
在本示例中)。
1db.peaks.createIndex( { "height": 1 } )
<$>[注] 注: 对于单个字段的索引,排序并不重要,因为索引结构可以有效地跨越两条方向。选择索引字段的顺序对于基于多个字段的复合索引变得更加重要,如在 [步骤 4]中所述(#step-4-%E2%80%94-creating-an-index-on-an-embedded-field)。
MongoDB 返回一个确认,说明收集中现在已经定义了多少索引,以及该索引如何与以前的状态不同。
1[secondary_label Output]
2{
3 "createdCollectionAutomatically" : false,
4 "numIndexesBefore" : 1,
5 "numIndexesAfter" : 2,
6 "ok" : 1
7}
但是,这一次,通过explain(
executionStats)
方法返回的信息将有所不同,因为存在一个索引:
1db.peaks.find(
2 { "height": { $gt: 8700 } }
3).explain("executionStats")
1[secondary_label Output]
2{
3 "queryPlanner" : {
4 . . .
5 "winningPlan" : {
6 . . .
7 "inputStage" : {
8 "stage" : "IXSCAN",
9 . . .
10 "indexName" : "height_1",
11 . . .
12 }
13 },
14 . . .
15 },
16 "executionStats" : {
17 . . .
18 "nReturned" : 1,
19 "executionTimeMillis" : 0,
20 "totalKeysExamined" : 1,
21 "totalDocsExamined" : 1,
22 . . .
23 },
24 . . .
25}
请注意,winningPlan
不再显示COLLSCAN
。相反,存在IXSCAN
,表示该索引被用作查询执行的一部分。MongoDB还告知您通过indexName
值使用哪个索引。
最重要的变化是在executionStats
部分。 再次,这个查询只返回了一个文档,标记为nReturned
。 然而,这次totalDocsExamined
仅为1. 这意味着数据库仅从收集中获取了一份文档以满足查询。
通过创建该索引,您将 MongoDB 必须检查的文档数量从 5 减少到 1,减少了 5 倍。
步骤三:创建独特的索引
在 MongoDB 中,如果它们都具有相同的 _id
值,则无法将两个文档插入集合中,这是因为数据库在 _id
字段上自动保持单域索引,这除了帮助加快文档搜索之外,还确保了 _id
字段值的独特性。
为了说明,运行下面的createIndex()
方法. 这个命令的语法类似于前一步使用的语法,但这一次,第二个参数被转移到createIndex()
并为该索引提供额外的设置。
1db.peaks.createIndex( { "name": 1 }, { "unique": true } )
再次,MongoDB 将确认该索引已成功创建:
1[secondary_label Output]
2{
3 "createdCollectionAutomatically" : false,
4 "numIndexesBefore" : 2,
5 "numIndexesAfter" : 3,
6 "ok" : 1
7}
接下来,检查索引是否符合其主要目的,并通过避免收集扫描来更快地对山名执行任何查询。
1db.peaks.find(
2 { "name": "Everest" }
3).explain("executionStats")
返回的查询计划使用IXSCAN
策略与新创建的索引,就像上一步的山高查询一样:
1[secondary_label Output]
2{
3 "queryPlanner" : {
4 . . .
5 "winningPlan" : {
6 . . .
7 "inputStage" : {
8 "stage" : "IXSCAN",
9 . . .
10 "indexName" : "name_1",
11 . . .
12 }
13 },
14 . . .
15 },
16 . . .
17}
接下来,检查你是否可以添加第二份代表 Everest 的文档到收藏中,现在该索引已经存在。
1db.peaks.insertOne({
2 "name": "Everest",
3 "height": 9200,
4 "location": ["India"],
5 "ascents": {
6 "first": {
7 "year": 2020
8 },
9 "first_winter": {
10 "year": 2021
11 },
12 "total": 2
13 }
14})
MongoDB 不会创建文档,而是返回错误消息:
1[secondary_label Output]
2WriteError({
3 "index" : 0,
4 "code" : 11000,
5 "errmsg" : "E11000 duplicate key error collection: test.peaks index: name_1 dup key: { name: \"Everest\" }",
6 "op" : {
7 . . .
此重复键错误
消息指向name_1
索引,表示它正在对该字段施加独特性限制。
通过此,您已经学会了如何创建一个独特的索引,以防止给定的字段包含重复值。
步骤 4 – 在嵌入式字段上创建索引
每当您使用没有索引的嵌入文档中的一个字段查询集合时,MongoDB 不仅必须从集合中获取所有文档,而且还必须通过每个嵌入文档。
例如,运行以下查询. 这将返回任何文件,其总数
- 在顶峰
集合中的每个文档中发现的上升
文档中嵌入的字段大于 300,并以下降顺序排序结果:
1db.peaks.find(
2 { "ascents.total": { $gt: 300 } }
3).sort({ "ascents.total": -1 })
此查询将返回收集中的四个顶峰, Everest 是登顶最多的顶峰,其次是 Lhotse, Makalu 和 K2:
1[label Output]
2{ "_id" : ObjectId("61212a8300c8304536a86b2f"), "name" : "Everest", "height" : 8848, "location" : [ "Nepal", "China" ], "ascents" : { "first" : { "year" : 1953 }, "first_winter" : { "year" : 1980 }, "total" : 5656 } }
3{ "_id" : ObjectId("61212a8300c8304536a86b32"), "name" : "Lhotse", "height" : 8516, "location" : [ "Nepal", "China" ], "ascents" : { "first" : { "year" : 1956 }, "first_winter" : { "year" : 1988 }, "total" : 461 } }
4{ "_id" : ObjectId("61212a8300c8304536a86b33"), "name" : "Makalu", "height" : 8485, "location" : [ "China", "Nepal" ], "ascents" : { "first" : { "year" : 1955 }, "first_winter" : { "year" : 2009 }, "total" : 361 } }
5{ "_id" : ObjectId("61212a8300c8304536a86b30"), "name" : "K2", "height" : 8611, "location" : [ "Pakistan", "China" ], "ascents" : { "first" : { "year" : 1954 }, "first_winter" : { "year" : 1921 }, "total" : 306 } }
现在运行相同的查询,但包括以前使用的explain(
executionStats)
方法:
1db.peaks.find(
2 { "ascents.total": { $gt: 300 } }
3).sort({ "ascents.total": -1 }).explain("executionStats")
正如输出此部分的COLLSCAN
值所表明的那样,MongoDB 使用了一个完整的集合扫描,并通过了来自峰值
集合的所有文档,以对比它们与查询条件:
1[secondary_label Output]
2{
3 . . .
4 "winningPlan" : {
5 "stage" : "COLLSCAN",
6 . . .
7 },
8 . . .
9}
由于该集合只有五个条目,因此缺乏索引并没有显著影响性能,并且该查询立即执行,但是,存储在数据库中的文档越复杂,性能影响查询的数量就越大。
为了帮助 MongoDB 执行此查询,让我们在上涨
文档中的总
字段创建一个索引。由于总
字段嵌入上涨
,在创建该索引时无法指定总
字段名称。
1db.peaks.createIndex( { "ascents.total": 1 } )
MongoDB 將以成功訊息回應,告知您現在已定義了四個索引。
1{
2 "createdCollectionAutomatically" : false,
3 "numIndexesBefore" : 3,
4 "numIndexesAfter" : 4,
5 "ok" : 1
6}
<$>[注] 注: 在本教程中,我们将逐步添加额外的索引,以说明如何使用不同类型的索引。
对于数据库中的每个索引,MongoDB 必须每当新文档被插入到收集中或被更改时保持适当的更新。 有许多索引的性能处罚可以抵消他们通过增加查询速度提供的益处。
再次运行之前的查询,以检查索引是否帮助MongoDB避免执行完整集合扫描:
1db.peaks.find(
2 { "ascents.total": { $gt: 300 } }
3).sort({ "ascents.total": -1 }).explain("executionStats")
1[secondary_label Output]
2{
3 "queryPlanner" : {
4 . . .
5 "winningPlan" : {
6 . . .
7 "inputStage" : {
8 "stage" : "IXSCAN",
9 . . .
10 "indexName" : "ascents.total_-1",
11 . . .
12 }
13 },
14 . . .
15 },
16 "executionStats" : {
17 . . .
18 "nReturned" : 4,
19 "executionTimeMillis" : 0,
20 "totalKeysExamined" : 4,
21 "totalDocsExamined" : 4,
22 . . .
23 "direction" : "backward",
24 . . .
25 },
26 . . .
27}
请注意,现在IXSCAN
被用来对比新创建的ascents.total_-1
索引,并且只审查了四个文档,这是在索引中返回和审查的相同数量的文档,因此没有其他文档被检索以完成查询。
方向
,在executionStats
部分的另一个字段,表示MongoDB决定穿过索引的方向.由于索引被创建为上升,使用ascents.total
: 1`语法,并且请求的山峰以下降顺序排序,数据库引擎决定向后移动.当从索引的一部分的字段中获取特定顺序的文档时,MongoDB将使用索引提供最终排序,而无需在全部获取后进一步排序文档。
步骤 5 – 创建组合字段索引
迄今为止本指南中的示例对了解使用索引的好处有帮助,但在现实世界应用程序中使用的文档过滤查询很少如此简单。
回想起第2步,当你在高度
字段上创建了一个单个字段索引时,以便更有效地查询峰值
集合以找到最高的山峰。有了这个索引,让我们分析MongoDB将如何执行类似但稍微更复杂的查询。
1db.peaks.find(
2 {
3 "ascents.first_winter.year": { $gt: 1990 },
4 "height": { $lt: 8600 }
5 }
6).sort({ "height": -1 })
只有一个山 - 马卡卢 - 满足了这两个条件:
1[secondary_label Output]
2{ "_id" : ObjectId("61212a8300c8304536a86b33"), "name" : "Makalu", "height" : 8485, "location" : [ "China", "Nepal" ], "ascents" : { "first" : { "year" : 1955 }, "first_winter" : { "year" : 2009 }, "total" : 361 } }
现在添加explaion(
executionStats)
方法来查找MongoDB如何执行此查询:
1db.peaks.find(
2 {
3 "ascents.first_winter.year": { $gt: 1990 },
4 "height": { $lt: 8600 }
5 }
6).sort({ "height": -1 }).explain("executionStats")
虽然没有索引可能影响第一个冬季登顶日期,但MongoDB使用了一个先前创建的索引,而不是进行完整的收藏扫描:
1[secondary_label Output]
2{
3 "queryPlanner" : {
4 . . .
5 "winningPlan" : {
6 "stage" : "IXSCAN",
7 . . .
8 "indexName" : "height_1",
9 . . .
10 }
11 },
12 . . .
13 },
14 "executionStats" : {
15 . . .
16 "nReturned" : 1,
17 "executionTimeMillis" : 0,
18 "totalKeysExamined" : 3,
19 "totalDocsExamined" : 3,
20 . . .
21 },
22 . . .
23}
请注意,这一次,与以前的索引支持的查询执行不同,表示返回文档数量的nReturned
值不同于totalKeysExamined
和totalDocsExamined
。
如果一个索引仅适用于查询的一部分,MongoDB 会使用它来缩小结果,然后在进行集合扫描之前。
在许多情况下,这完全足够了. 如果最常见的查询检查单个索引字段,并且只需要偶尔执行额外的过滤,那么有一个单个字段索引通常足够好。
想象一下,您对满足与其首次冬季登山和高度相关的条件的山峰的数据库进行足够定期的查询,以便成为性能问题,并从有一个索引中受益。
1db.peaks.createIndex(
2 {
3 "ascents.first_winter.year": 1,
4 "height": -1
5 }
6)
请注意,此操作的语法类似于单个字段索引创建,但这次两个字段都列在索引定义对象中。
MongoDB 将确认该索引已成功创建:
1[secondary_label Output]
2{
3 "createdCollectionAutomatically" : false,
4 "numIndexesBefore" : 4,
5 "numIndexesAfter" : 5,
6 "ok" : 1
7}
使用单个字段索引,数据库引擎可以自由地向前或向后穿过索引,但是,在复合索引中,这种情况并不总是如此。如果对一个字段组合的特定排序顺序进行更频繁的查询,则可以进一步提高性能,将该顺序纳入索引定义中。
再次运行之前的查询,以测试查询的执行方式是否有任何变化:
1db.peaks.find(
2 {
3 "ascents.first_winter.year": { $gt: 1990 },
4 "height": { $lt: 8600 }
5 }
6).sort({ "height": -1 }).explain("executionStats")
这一次,查询再次使用索引扫描,但索引不同. 现在,您刚刚创建的 ascents.first_winter.year_1_height_-1
索引被选择在之前使用的 height_1
索引上:
1[secondary_label Output]
2{
3 "queryPlanner" : {
4 . . .
5 "winningPlan" : {
6 "stage" : "IXSCAN",
7 . . .
8 "indexName" : "ascents.first_winter.year_1_height_-1",
9 . . .
10 }
11 },
12 . . .
13 },
14 "executionStats" : {
15 . . .
16 "nReturned" : 1,
17 "executionTimeMillis" : 0,
18 "totalKeysExamined" : 1,
19 "totalDocsExamined" : 1,
20 . . .
21 },
22 . . .
23}
重要区别在于executionStats
。在新的索引中,一个单一的文档被直接从索引中检查,然后返回,而不是三个文档需要进一步的文档扫描来缩小结果。
现在,您已经学会了如何创建覆盖多个字段的索引,您可以继续了解多键索引以及它们的使用方式。
步骤 6 – 创建一个多密钥索引
在以前的示例中,用于索引的字段中存储了单个值,例如高度、年份或名称. 在这些情况下,MongoDB 将字段值直接存储为索引密钥,使索引快速可穿越。
首先,尝试找到藏品中位于尼泊尔的所有山脉:
1db.peaks.find(
2 { "location": "Nepal" }
3)
四个峰值返回:
1[secondary_label Output]
2{ "_id" : ObjectId("61212a8300c8304536a86b2f"), "name" : "Everest", "height" : 8848, "location" : [ "Nepal", "China" ], "ascents" : { "first" : { "year" : 1953 }, "first_winter" : { "year" : 1980 }, "total" : 5656 } }
3{ "_id" : ObjectId("61212a8300c8304536a86b31"), "name" : "Kangchenjunga", "height" : 8586, "location" : [ "Nepal", "India" ], "ascents" : { "first" : { "year" : 1955 }, "first_winter" : { "year" : 1986 }, "total" : 283 } }
4{ "_id" : ObjectId("61212a8300c8304536a86b32"), "name" : "Lhotse", "height" : 8516, "location" : [ "Nepal", "China" ], "ascents" : { "first" : { "year" : 1956 }, "first_winter" : { "year" : 1988 }, "total" : 461 } }
5{ "_id" : ObjectId("61212a8300c8304536a86b33"), "name" : "Makalu", "height" : 8485, "location" : [ "China", "Nepal" ], "ascents" : { "first" : { "year" : 1955 }, "first_winter" : { "year" : 2009 }, "total" : 361 } }
请注意,这些峰值中没有一个仅在尼泊尔。这四个峰值中的每个峰值以位置
字段表示的范围超过一个国家,所有这些都是多个值的组合。此外,这些值可以以不同的顺序出现。例如,Lhotse列为[
尼泊尔,
中国 ]
,而Makalu列为[
中国,
尼泊尔 ]
。
由于没有可用位置
字段的索引,MongoDB 目前正在进行完整的集合扫描以执行该查询。
1db.peaks.createIndex( { "location": 1 } )
请注意,此语法与任何其他单个字段索引都不相同,MongoDB 将返回成功消息,该索引现在可用:
1[secondary_label Output]
2{
3 "createdCollectionAutomatically" : false,
4 "numIndexesBefore" : 5,
5 "numIndexesAfter" : 6,
6 "ok" : 1
7}
现在您已经为位置
字段创建了索引,然后使用解释(
executionStats)
方法再次运行之前的查询,以了解它如何执行:
1db.peaks.find(
2 { "location": "Nepal" }
3).explain("executionStats")
结果显示,MongoDB 使用索引扫描作为策略,指向新创建的location_1
索引:
1[secondary_label Output]
2{
3 "queryPlanner" : {
4 . . .
5 "winningPlan" : {
6 . . .
7 "inputStage" : {
8 "stage" : "IXSCAN",
9 . . .
10 "indexName" : "location_1",
11 "isMultiKey" : true,
12 . . .
13 }
14 },
15 . . .
16 },
17 "executionStats" : {
18 . . .
19 "nReturned" : 4,
20 "executionTimeMillis" : 0,
21 "totalKeysExamined" : 4,
22 "totalDocsExamined" : 4,
23 . . .
24 }
25 . . .
26}
返回的文档数量与审查的索引密钥和审查的文档总数相匹配,这意味着索引被用作查询的唯一信息来源。
注意输出中列出的isMultiKey
属性为true
。MongoDB 会自动为位置
字段创建一个多键索引. 如果您创建一个字段持有数组的索引,MongoDB 会自动确定需要创建一个多键索引,并为这些数组中的每一个元素创建单独的索引条目。
因此,对于具有存储数组的位置
字段的文档,中国
、`尼泊尔``字段,对于同一文档出现两个单独的索引条目,一个是针对中国,另一个是针对尼泊尔。
步骤 7 — 列出和删除集合中的索引
在之前的步骤中,您已经学会了如何创建不同类型的索引。当数据库增长或要求发生变化时,重要的是能够知道哪些索引被定义,有时还可以删除不必要的索引。
要在本教程中列出您在峰值
集合中定义的所有索引,您可以使用getIndexes()
方法:
1db.peaks.getIndexes()
MongoDB 将返回索引列表,描述它们的性质并列出它们的名称:
1[secondary_label Output]
2[
3 {
4 "v" : 2,
5 "key" : {
6 "_id" : 1
7 },
8 "name" : "_id_"
9 },
10 {
11 "v" : 2,
12 "key" : {
13 "height" : 1
14 },
15 "name" : "height_1"
16 },
17 {
18 "v" : 2,
19 "unique" : true,
20 "key" : {
21 "name" : 1
22 },
23 "name" : "name_1"
24 },
25 {
26 "v" : 2,
27 "key" : {
28 "ascents.total" : 1
29 },
30 "name" : "ascents.total_1"
31 },
32 {
33 "v" : 2,
34 "key" : {
35 "ascents.first_winter.year" : 1,
36 "height" : -1
37 },
38 "name" : "ascents.first_winter.year_1_height_-1"
39 },
40 {
41 "v" : 2,
42 "key" : {
43 "location" : 1
44 },
45 "name" : "location_1"
46 }
47]
在本教程中,您已经定义了6个索引,每个索引的关键
属性列出了索引定义,匹配了之前创建索引的方式,每个索引的名称
属性包含了创建索引时自动生成的MongoDB名称。
要删除现有索引,您可以使用dropIndex()
方法使用这些属性中的任何一个,下面的示例将使用其内容的定义删除height_1
索引:
1db.peaks.dropIndex( { "height": 1 } )
由于{高度
: 1 }匹配名为
height_1的
高度`上的单个字段索引,MongoDB将删除该索引,并以成功消息回复,说明在删除此索引之前有多少索引:
1[secondary_label Output]
2{ "nIndexesWas" : 6, "ok" : 1 }
如果指数定义更为复杂,这样指定要删除的索引可能会变得不便,就像复合索引一样。作为替代方法,您可以使用索引的名称删除索引。
1db.peaks.dropIndex("ascents.first_winter.year_1_height_-1")
再次,MongoDB 会删除索引并返回成功消息:
1[secondary_label Output]
2{ "nIndexesWas" : 5, "ok" : 1 }
您可以通过再次拨打getIndexes()
来确认这两个索引确实已从收集索引列表中删除:
1db.peaks.getIndexes()
这一次,仅列出了剩余的四个指数:
1[secondary_label Output]
2[
3 {
4 "v" : 2,
5 "key" : {
6 "_id" : 1
7 },
8 "name" : "_id_"
9 },
10 {
11 "v" : 2,
12 "unique" : true,
13 "key" : {
14 "name" : 1
15 },
16 "name" : "name_1"
17 },
18 {
19 "v" : 2,
20 "key" : {
21 "ascents.total" : 1
22 },
23 "name" : "ascents.total_1"
24 },
25 {
26 "v" : 2,
27 "key" : {
28 "location" : 1
29 },
30 "name" : "location_1"
31 }
32]
作为最后的注意事项,请注意,在 MongoDB 中无法修改现有索引. 如果您需要更改索引,您必须先放下该索引并创建一个新的索引。
结论
通过阅读本文,您将熟悉索引的概念 - 特殊数据结构可以通过减少MongoDB在查询执行过程中必须分析的数据量来提高查询性能,您已经学会了如何创建单个字段,组合和多钥匙索引,以及如何检查它们的存在是否影响查询执行。
该教程只描述了一些由MongoDB提供的索引功能,以塑造忙碌的数据库中的查询性能。我们鼓励您研究官方的MongoDB文档(https://docs.mongodb.com/v4.4/mongo/),以了解更多关于索引以及它如何影响不同场景的性能。