简介
就像百科全书一样,数据库是可访问信息的丰富存储库。要在百科全书中找到特定的信息,需要翻阅每一页,直到找到您要查找的内容。这种低效性就是百科全书有索引的原因,索引会引导您找到包含您要查找信息的准确页码。
数据库索引也以类似的方式更有效地引导您找到信息。在 MongoDB 中,未索引的查询(“搜索整本书”)称为集合扫描。
索引可以被认为是访问数据的快捷方式,这样就不需要扫描整个数据库来查找您要查找的内容。在本文中,我们将介绍 MongoDB 中的索引,讨论何时使用索引,以及如何管理索引。
如果您正在使用 MongoDB,请查看 Prisma 的 MongoDB 连接器!您可以使用 Prisma Client 来自信地管理生产 MongoDB 数据库。
要开始使用 MongoDB 和 Prisma,请查看我们的从零开始入门指南,或如何添加到现有项目。
何时应该使用索引
继续使用我们的百科全书类比,有人可能会想到为书中的每个单词都设置一个索引行。如果总是使用索引更快,那么这似乎是有益的。但是,您可以想象,索引中的单词行越多,书就越大。在某些时候,为了容纳索引每个单词所需的书的大小变得无效。许多单词,例如“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"}]
索引信息包括用于创建索引的键和选项。
如果您有兴趣在 MongoDB 中使用全文索引,Prisma 具有 fullTextIndex
预览功能,它允许您轻松地将全文索引迁移到 Prisma 模式中,以实现类型安全并防止验证错误。
如何理解索引性能
现在您已经能够创建和检查集合上存在的索引,您将希望查看您的索引是否按预期执行。
为了开始这个示例,我们将使用 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
中看到此查询的 stage
是 COLLSCAN
。这意味着发生了集合扫描以完成此查询,其中 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
现在只是 name
为 "Ramsay Bolton"
的 273 个文档,而不是整个 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 索引奠定正确的基础。
如果您正在使用 MongoDB,请查看 Prisma 的 MongoDB 连接器!您可以使用 Prisma Client 来自信地管理生产 MongoDB 数据库。
要开始使用 MongoDB 和 Prisma,请查看我们的从零开始入门指南,或如何添加到现有项目。
常见问题解答
对于存储为二维平面上的点的数据,请使用 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 的情况下,它是文档的字段或字段中的重复值。
非唯一索引不施加此限制。