分享到

简介

就像百科全书一样,数据库是可访问信息的丰富存储库。要在百科全书中找到特定的信息,就需要翻阅每一页,直到找到你要查找的内容。这种低效率正是百科全书有索引的原因,索引会指向你需要翻到的确切页面以获取你要查找的信息。

数据库 索引 同样可以更有效地将你指向正确的信息位置。在 MongoDB 中,未建立索引的查询(“搜索整本书”)称为集合扫描

索引可以被认为是访问数据的快捷方式,这样就不需要扫描整个数据库来查找你要查找的内容。在本文中,我们将介绍 MongoDB 中的索引,讨论何时使用索引以及如何管理索引。

Database Indexes

何时应该使用索引

继续我们的百科全书类比,有人可能会想到为书中的每个单词都建立索引行。如果总是使用索引更快,那么这似乎是有益的。然而,你可以想象,索引中的单词行越多,书就越大。在某些时候,为每个单词建立索引所需的书的大小变得无效。“the”或“because”等许多词语不如“hippopotamus”那样有用的搜索价值。

这类似于 MongoDB 和通用数据库中的索引。虽然是的,为查询可能使用的任何数据建立索引会更快,但确实存在不需要索引的数据。就像书的大小一样,向数据库添加过多的索引也会占用空间,并且如果不加以控制,会对数据库的写入操作产生不利影响。

索引是一种非常有效的方式,可以优化对特定数据的访问,这些数据通常用作查询中的选择标准。了解何时使用它们非常重要,因此请确保将它们添加到经常被查询的数据库字段中,这将使你的读取保持高效,而不会对数据库大小和写入效率产生负面影响。

如何创建索引

现在我们已经了解了什么是索引以及何时使用索引,我们可以开始介绍创建索引的方法。

一旦你确定了可以从索引中受益的字段,你就可以使用 MongoDB 的 createIndex() 方法。基本语法如下

db.COLLECTION_NAME.createIndex( { "FIELD_NAME": 1 } )

FIELD_NAME 是你要在其上创建索引的字段的名称,1 表示升序。

该方法的示例用法如下所示

db.mycoll.createIndex( { "country": 1 } )

你还可以使用 createIndex() 方法在多个字段上创建索引,方法是创建一个逗号分隔的列表,如下所示

db.COLLECTION_NAME.createIndex( { "FIELD_NAME_1": 1, "FIELD_NAME_2": -1 } )

如何显示索引

一旦你开始创建索引,你可能想要检查数据库实例上存在哪些索引。在 MongoDB 中,你可以使用 getIndexes() 方法返回集合中所有索引的描述。

查看集合所有索引的基本语法是

db.COLLECTION_NAME.getIndexes()

使用我们之前创建索引的示例,以下显示了该方法及其将返回的内容。

db.mycoll.getIndexes()

返回结果为

[
{
"v" : 2,
"key" : {
"country" : 1
},
"name" : "country"
}
]

索引信息包括用于创建索引的键和选项。

如何理解索引性能

现在你已经能够创建和检查集合上存在的索引,你将希望查看你的索引是否按预期执行。

为了开始示例,我们将使用 sample_mflix 数据库和 comments 集合,该集合包含约 50.3k 个文档。这是一个由 MongoDB 大学 提供的示例集合,模拟电影和电视评论的数据存储。

要理解索引的性能,我们将首先运行一个没有索引的查询。以下查询将返回集合中所有 273 个由 Ramsay Bolton 发表的评论文档

db.comments.find( { "name" : "Ramsay Bolton" } )

现在,如果我们将 MongoDB 的 explain plan 附加到查询中,我们将看到此查询的性能。

db.comments.find( { "name" : "Ramsay Bolton" } ).explain("executionStats")

这将产生以下结果

{
queryPlanner: {
plannerVersion: 1,
namespace: 'sample_mflix.comments',
indexFilterSet: false,
parsedQuery: { name: { '$eq': 'Ramsay Bolton' } },
winningPlan: {
stage: 'COLLSCAN',
filter: { name: { '$eq': 'Ramsay Bolton' } },
direction: 'forward'
},
rejectedPlans: []
},
executionStats: {
executionSuccess: true,
nReturned: 273,
executionTimeMillis: 23,
totalKeysExamined: 0,
totalDocsExamined: 50303,
executionStages: {
stage: 'COLLSCAN',
filter: { name: { '$eq': 'Ramsay Bolton' } },
nReturned: 273,
executionTimeMillisEstimate: 6,
works: 50305,
advanced: 273,
needTime: 50031,
needYield: 0,
saveState: 50,
restoreState: 50,
isEOF: 1,
direction: 'forward',
docsExamined: 50303
}
}
}

此输出中有几个关键结果需要关注。首先,我们可以在 winningPlan 中看到此查询的 stageCOLLSCAN。这意味着发生了集合扫描来完成此查询,totalDocsExamined 为 50,303,executionTimeMillis 为 23 毫秒。即使 nReturned 仅为 273 个文档,该查询也必须检查集合中的每个文档,并花费了 23 毫秒。虽然 23 毫秒听起来不多,但对于包含一百万个文档的集合来说,时间可能会更长。

如果查询 name 将成为访问此集合的应用程序的经常性模式,我们可能需要在该字段上创建索引。为此,我们编写以下内容

db.comments.createIndex( {"name":1} )

如果我们使用与之前相同的带有 explain plan 的查询

db.comments.find( { "name" : "Ramsay Bolton" } ).explain("executionStats")
{
queryPlanner: {
plannerVersion: 1,
namespace: 'sample_mflix.comments',
indexFilterSet: false,
parsedQuery: { name: { '$eq': 'Ramsay Bolton' } },
winningPlan: {
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { name: 1 },
indexName: 'name_1',
isMultiKey: false,
multiKeyPaths: { name: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { name: [ '["Ramsay Bolton", "Ramsay Bolton"]' ] }
}
},
rejectedPlans: []
},
executionStats: {
executionSuccess: true,
nReturned: 273,
executionTimeMillis: 0,
totalKeysExamined: 273,
totalDocsExamined: 273,
executionStages: {
stage: 'FETCH',
nReturned: 273,
executionTimeMillisEstimate: 0,
works: 274,
advanced: 273,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 273,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 273,
executionTimeMillisEstimate: 0,
works: 274,
advanced: 273,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { name: 1 },
indexName: 'name_1',
isMultiKey: false,
multiKeyPaths: { name: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { name: [ '["Ramsay Bolton", "Ramsay Bolton"]' ] },
keysExamined: 273,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
}
}

与未建立索引的查询相比,我们现在看到 winningPlan.inputstage 现在是 IXSCAN,这表明使用了索引。

此外,我们看到 totalDocsExamined 现在仅为 273 个文档,其中 name"Ramsay Bolton",而不是整个 50,303 个文档。这种效率的提高尤其体现在 executionTimeMillis 总计 0 毫秒。我们新的 name 索引向查询传达了精确的查找位置,以找到它正在查找的数据。

分析最重要的查询的 explain plan 将向你显示索引的性能,或者突出显示何时需要创建索引以提高应用程序的效率。

如何删除索引

虽然 explain plan 可能会显示需要索引,但它也可能做相反的事情。例如,如果不再需要索引或索引没有显着提高性能,那么删除该索引以保留空间或获得一些写入性能可能是最符合你利益的做法。

要在集合上删除索引,使用 dropIndexes() 方法的基本语法如下

db.COLLECTION_NAME.dropIndex( { "FIELD_NAME": 1 } )

如果我们想删除之前示例中的 country 索引,我们将编写以下内容

db.mycoll.dropIndex( { "country":1 } )

结论

在本指南中,我们讨论了高效查询数据库如何改善应用程序的用户体验。此外,那些使用数据进行分析或其他内部工作的人员将获得更快的性能和更轻松的数据库工作。了解如何索引以及索引的工作原理是实现查询效率的关键。

我们介绍了在 MongoDB 中创建、分析和删除索引的基础知识。了解这些索引基础知识将为继续使用 MongoDB 进行更高级的索引奠定正确的基础。

常见问题解答

对于存储为二维平面上的点的数据,请使用 2d 索引。它适用于旧版 MongoDB 版本上的旧坐标对。

2d 索引可以引用两个字段。第一个必须是位置字段。2d 复合索引构造首先在位置字段上选择,然后按其他条件过滤这些结果的查询。

无论集合大小,你仍然可以使用 createIndex() 方法。

如果在大集合上构建索引时遇到问题,那么你可能需要考虑水平扩展,以便更容易管理。

MongoDB 还建议使用 滚动索引构建 方法。

为了在 MongoDB 中索引嵌入式对象字段,你可以使用点表示法。

例如,如果你有一个用于跟踪已读书籍的应用,那么每个用户可能都有一个集合,结构如下

db.users.insertOne({
"first_name": "Alex",
"last_name": "Emerich",
"books": {
"first_book": {
"title": "Flights",
"author": "Olga Tokarczuk"
},
"second book": {
"title": "The Master and Margarita",
"author": "Mikhail Bulgakov"
},
"total": 2
}
})

为了在嵌入的 total 字段上创建索引,请编写以下语句

db.users.createIndex( {"books.total": 1 } )

复合索引是单个索引结构,它保存对集合文档中多个字段的引用。

创建复合索引的基本语法如下

db.collection.createIndex( { <field1>: <type>, <field2>: <type2>, ... } )

唯一索引确保表的任何两行在索引列或列中都没有重复值。在 MongoDB 的情况下,它是文档的字段或字段中的重复值。

非唯一索引不施加此限制。

关于作者
Alex Emerich

Alex Emerich

Alex 是你典型的观鸟、热爱嘻哈的爱书之人,他也喜欢撰写有关数据库的文章。他目前居住在柏林,在那里你可以看到他像利奥波德·布卢姆一样漫无目的地在城市中漫步。