MongoDB 8.0——索引

索引

在MongoDB数据库中,索引(Indexes)是用于加速查询操作的数据结构。MongoDB索引基于B-tree(或B-tree)数据结构实现,针对字符串、整数等数据类型进行了优化,可以显著提高数据库读取操作的性能。

1、索引介绍

MongoDB数据库索引支持高效执行查询。如果没有索引,MongoDB就必须扫描集合中的每个文档以返回查询结果。如果数据库存在适当的索引,MongoDB就可以使用该索引来限制查询必须扫描的文档数。

MongoDB索引可提高查询性能,但添加索引会影响写入操作的性能。对于写入读取率高的集合,由于每次插入操作都必须同时更新所有索引,因此会带来较高的索引成本。如果应用程序对相同字段重复运行查询,则可以为这些字段创建索引以提高性能。

MongoDB索引是一种特殊的数据结构,以易于遍历的形式存储小部分集合数据集。MongoDB索引使用B-tree数据结构,索引可以存储某个特定字段或多个字段的值,并按字段的值进行排序。索引条目的排序支持高效的相等匹配和基于范围的查询操作。此外,MongoDB还可以使用索引中的顺序来返回排序后的结果。

MongoDB索引包括默认索引(Default Index)、索引名称和索引构建性能三方面的内容,具体介绍如下。

  1. 默认索引:MongoDB数据库在创建集合时,会在_id字段上创建一个唯一索引。_id索引可防止客户端插入两个具有相同_id字段值的文档。同时,设计人员是无法删除该索引的。
  2. 索引名称:索引的默认名称是索引键和索引中每个键的方向(1或-1)的连接,使用下画线作为分隔符。例如,在{item: 1, quantity: -1}上创建的索引的名称为item_1_quantity_-1。另外,索引一旦创建,便无法重命名。如果要重命名索引,就必须删除索引并使用新名称重新创建索引。
  3. 索引构建性能:应用程序在索引构建期间可能会遭遇性能下降,包括对集合的读/写访问将会受到限制。

2、创建索引

MongoDB数据库索引支持应用程序在相同字段上重复运行查询,并且可以在这些字段上创建索引以提高查询的性能。

如果要在MongoDB数据库中创建索引,可以使用Shell中的createIndex()方法或适用于驱动程序的等效方法。在MongoDB Shell或驱动程序中运行创建索引的命令时,MongoDB仅在没有相同规格索引存在时才会成功创建索引。

同时,尽管在MongoDB数据库中创建索引可以提高查询性能,但添加索引会对写入操作的性能产生负面影响。对于具有高写入读取比率的集合,索引的成本很高,因为每次插入和更新还必须更新所有索引。

如果要使用Node.js驱动程序创建索引,可以使用createIndex()方法:

js 复制代码
db.collection.createIndex(keys, options)

下面的代码会在name字段上创建一个单键降序索引:

js 复制代码
db.person.createIndex({ name: -1 })

若要确认索引已创建,可以使用mongosh运行db.collection.getIndexes()方法:

js 复制代码
db.person.getIndexes()

输出结果如下:

js 复制代码
[
  { v: 2, key: { _id: 1 }, name: '_id_' },
  { v: 2, key: { name: -1 }, name: 'name_-1' }
]

3、指定索引名称

MongoDB数据库在创建索引时,可以为索引指定自定义名称。为索引指定名称有助于区分集合中的不同索引。例如,如果索引具有不同名称,则可以更轻松地在查询计划的解释结果中识别查询使用的索引。想要指定索引名称,可以在创建索引时包含name选项,具体代码如下:

js 复制代码
db.<collection>.createIndex(
	{<field>: <value>},
	{name: "<indexName>"}
)

在指定索引名称之前,需考虑以下几点:

  • 索引名称必须是唯一的。使用已有索引的名称创建索引会报错。
  • 无法重命名现有索引。相反,必须删除索引并使用新名称重新创建索引。

在下面的应用示例中,博客(Blog)集合包含有关博文与用户交互的数据,在content、users.comments和users.profiles字段上创建文本索引,同时将索引name设置为InteractionsTextIndex。具体代码如下:

js 复制代码
db.blog.createIndex(
	{
		content: "text",
		"users.comments": "text",
		"users.profiles": "text"
	},
	{
		name: "InteractionsTextIndex"
	}
)

在创建索引后,可以使用db.collection.getIndexes()方法获取索引名称。具体代码如下:

js 复制代码
db.blog.getIndexes()

输出结果如下:

js 复制代码
[
  { v: 2, key: { _id: 1 }, name: '_id_' },
  {
    v: 2,
    key: { _fts: 'text', _ftsx: 1 },
    name: 'InteractionsTextIndex',
    weights: { content: 1, 'users.comments': 1, 'users.profiles': 1 },
    default_language: 'english',
    language_override: 'language',
    textIndexVersion: 3
  }
]

4、删除索引

MongoDB数据库在删除索引时,可以从集合中删除特定索引。如果发现对性能有负面影响,想用新索引替代特定索引,或者不再需要该索引,则可以删除索引。想要删除索引,可以使用表所示的Shell方法。

设计人员可以删除_id字段上的默认索引之外的任何索引,如要删除_id索引,则必须删除整个集合。删除生产环境中频繁使用的索引可能会导致性能下降。在删除索引之前,需要考虑隐藏索引以评估删除索引的潜在影响。

如果要删除索引,则需要知道它的名称。想要获取集合的所有索引名称,可以运行getIndexes()方法,具体代码如下:

js 复制代码
db.<collection>.getIndexes()

在确定要删除的索引后,需要对指定的集合使用下列一种删除方法,具体说明如下。

删除单个索引

要删除特定索引,可使用dropIndex()方法并指定索引名称:

js 复制代码
db.<collection>.dropIndex("<indexName>")

删除多个索引

要删除多个索引,可使用dropIndexes()方法并指定索引名称数组:

js 复制代码
db.<collection>.dropIndexes( [ "<index1>", "<index2>", "<index3>" ] )

删除_id索引之外的所有索引

要删除_id索引之外的所有索引,可使用dropIndexes()方法:

js 复制代码
db.<collection>.dropIndexes()

在删除索引后,系统返回有关操作状态的信息,输出示例如下:

js 复制代码
...
{ "nIndexesWas" : 3, "ok" : 1 }
...

在上面的代码中,nIndexesWas的值反映了删除索引前的索引数。

想要确认索引已删除,可以运行db.collection.getIndexes()方法,具体代码如下:

js 复制代码
db.<collection>.getIndexes()

删除的索引不再显示在getIndexes()输出中。

5、单字段索引

MongoDB数据库单字段索引用于存储集合中单字段的信息。在默认情况下,所有集合都在_id字段上有一个索引,同时还可以添加其他索引以加快执行重要的查询和操作。

设计人员可以对文档中的任意字段创建单字段索引,具体内容如下:

  • 顶级文档字段。
  • 嵌入式文档。
  • 嵌入式文档中的字段。

在创建索引时,需要指定以下内容:

  • 要在其上创建索引的字段。
  • 带有索引的值的排序顺序(升序或降序)。例如,1的排序顺序是按升序对值进行排序,-1的排序顺序按降序对值进行排序。

想要创建单字段索引,请使用db.collection.createIndex()方法:

js 复制代码
db.<collection>.createIndex( { <field>: <sortOrder> } )

对于单字段索引,索引键的排序顺序(即升序或降序)并不重要,因为MongoDB可以支持沿任意一个方向遍历索引。

在下面的应用示例中,创建了一个students集合,其中包含以下文档:

js 复制代码
db.students.insertMany([
	{
		"name": "king",
		"gpa": 3.8,
		"location": {city: "XC", state: "PEK"}
	},
	{
		"name": "tina",
		"gpa": 3.6,
		"location": {city: "PD", state: "SH"}
	}
])

然后,可以尝试在单个字段上创建索引。考虑一位经常通过gpa查找学生的学校管理员,设计人员可以在gpa字段上创建索引,以提高这些查询的性能,具体代码如下:

js 复制代码
db.students.createIndex( { gpa: 1 } )

该索引支持选择字段gpa的查询,具体示例如下:

js 复制代码
db.students.find( { gpa: 3.8 } )
db.students.find( { gpa: { $lt: 3.7 } } )

另外,还可以尝试对嵌入字段创建索引。设计人员可以对嵌入式文档中的字段创建索引,嵌入式字段上的索引可以完成使用点表示法的查询。location字段是嵌入式文档,其中包含嵌入式字段city和state。

在location.state字段上创建索引,具体代码如下:

js 复制代码
db.students.createIndex( { "location.state": 1 } )

该索引支持对字段location.state进行查询,具体示例如下:

js 复制代码
db.students.find( { "location.state": "PEK" } )
db.students.find( { "location.city": "PD", "location.state": "SH" } )

6、对嵌入式文档创建索引

在MongoDB数据库中,可以对整个嵌入式文档创建索引。但是,只有指定整个嵌入式文档的查询才会使用索引。另外,对文档中的特定字段的查询不使用该索引。

想要在嵌入式文档上使用索引,查询必须指定整个嵌入式文档。如果模式模型发生更改,并且在已索引的文档中添加或删除了字段,这可能会导致意外行为。在查询嵌入式文档时,在查询中指定的字段顺序很重要,查询中的嵌入式文档和返回的文档必须完全匹配。

在为嵌入式文档创建索引之前,需要考虑是应该为该文档中的特定字段编制索引,还是使用通配符索引来索引该文档的所有子字段。

在下面的应用示例中,创建了一个students集合,其中包含以下文档:

js 复制代码
db.students.insertMany([
	{
		"name": "king",
		"gpa": 3.8,
		"location": {city: "XC", state: "PEK"}
	},
	{
		"name": "tina",
		"gpa": 3.6,
		"location": {city: "PD", state: "SH"}
	}
])

然后,在location字段上创建索引,具体代码如下:

js 复制代码
db.students.createIndex( { location: 1 } )

以下查询使用location字段上的索引,具体示例如下:

js 复制代码
db.students.find( { location: { city: "XC", state: "SH" } } )

以下查询不使用location字段上的索引,因为查询的是嵌入式文档中的特定字段,具体示例如下:

js 复制代码
db.students.find( { "location.city": "XC" } )
db.students.find( { "location.state": "PEK" } )

为了使点表示法查询能够使用索引,必须在想要查询的特定嵌入式字段上创建索引,而不是在整个嵌入式对象上创建索引。

以下查询不会返回任何结果,因为查询谓词中嵌入字段的指定顺序与它们在文档中出现的顺序不同:

js 复制代码
db.students.find( { location: { state: "PEK", city: "XC" } } )

7、复合索引

7.1、复合索引介绍

MongoDB数据库中的复合索引从集合中每个文档的两个或多个字段中收集数据,并对其进行排序操作。数据先按索引中的第一个字段分组,再按每个后续字段分组。对经常查询的字段进行索引,可以提高实现覆盖查询的可能性。

复合查询是指可使用某一索引而不必检查任何文档便可完成的查询,使用此类索引可大幅提升性能。设计人员想要创建复合索引,需要使用以下代码:

如果应用程序重复运行包含多个字段的查询,则可以创建复合索引来提高查询性能。对经常查询的字段使用复合索引,可以增加覆盖这些查询的机会。复合查询是可以完全使用索引进行的查询,而无须检查任何文档,这样可以优化查询性能。

关于复合索引的技术细节和使用限制,具体说明如下。

  • 字段限制:单个复合索引最多可包含32个字段。
  • 字段排序:索引字段的顺序会影响复合索引的有效性,复合索引根据索引中字段的顺序包含对文档的引用。
  • 排序顺序:索引会按升序或降序存储对字段的引用。对于复合索引,排序顺序可以决定索引是否支持排序操作。
  • 哈希索引字段:复合索引可以包含单个哈希索引字段。
  • 索引前缀:索引前缀是索引字段的起始子集,复合索引支持对索引前缀中包含的所有字段进行查询。

请参考如下关于索引的代码示例:

js 复制代码
{ "item": 1, "location": 1, "price": 1 }

上述索引具有如下索引前缀:

  • { item: 1 }
  • { item: 1, location: 1 }

MongoDB可以使用复合索引来支持对这些字段组合的查询:

  • item。
  • item和location。
  • item、location和price。

MongoDB还可以使用索引来支持对item和price字段的查询,因为item字段对应于前缀。但是,只有索引中的item字段可以支持此查询。查询不能使用location后面的price字段。索引字段按顺序解析,如果查询省略了索引前缀,将无法使用该前缀后面的任何索引字段。MongoDB无法使用复合索引来支持对以下这些字段组合的查询:

  • location
  • price
  • location和price

如果没有item字段,则以上的字段组合都不对应前缀索引。

7.2、创建复合索引

MongoDB复合索引是包含对多个字段的引用的索引。复合索引可以提高对索引中的字段或索引前缀中的字段的查询性能。为常用查询字段建立索引,可以增加查询覆盖的机会,这表明MongoDB可以完全利用索引来满足查询需求,而无须检查文档。

设计人员要创建复合索引,可以使用db.collection.createIndex()方法:

注意:在单个复合索引中,限制最多仅可以指定32个字段。

在下面的应用示例中,创建了一个students集合,其中包含以下文档:

js 复制代码
db.students.insertMany([
	{
		"name": "king",
		"gpa": 3.8,
		"location": {city: "XC", state: "PEK"}
	},
	{
		"name": "tina",
		"gpa": 3.6,
		"location": {city: "PD", state: "SH"}
	}
])

然后,创建一个包含name和gpa字段的复合索引,具体代码如下:

js 复制代码
db.students.createIndex({
	name: 1,
	gpa: -1
})

在上面的代码中,name参数上的索引是升序(1),gpa参数上的索引为降序(-1)。基于上述索引,支持下面的查询方式:

js 复制代码
db.students.find( { name: "king", gpa: 3.8 } )
db.students.find( { name: "tina" } )

注意:在使用索引查询时,不支持仅对gpa字段进行查询,因为gpa不是索引前缀的一部分。

7.3、复合索引排序顺序

MongoDB索引会按升序(1)或降序(-1)存储对字段的引用。对于复合索引,排序顺序可以决定索引是否支持排序操作。复合索引支持与索引的排序顺序或索引的反向排序顺序匹配的排序操作。如果索引中的排序顺序与查询中的排序顺序匹配,则复合索引可以提高排行榜的性能。

在下面的应用示例中,创建了一个leaderboard集合,其中包含以下文档:

js 复制代码
db.leaderboard.insertMany([
	{
		"score": 90,
		"username": "king",
		"date": ISODate("2024-09-01T00:00:00Z")
	},
	{
		"score": 85,
		"username": "tina",
		"date": ISODate("2024-09-02T00:00:00Z")
	},
	{
		"score": 95,
		"username": "cici",
		"date": ISODate("2024-09-03T00:00:00Z")
	},
	{
		"score": 80,
		"username": "king",
		"date": ISODate("2024-09-04T00:00:00Z")
	},
	{
		"score": 75,
		"username": "tina",
		"date": ISODate("2024-09-05T00:00:00Z")
	}
])

查询leaderboard集合返回排行榜结果,具体如下:

js 复制代码
db.leaderboard.find().sort( { score: -1, username: 1 } )

输出结果如下:

js 复制代码
[
  {
    _id: ObjectId('6a6ff63899e291eadc41adc7'),
    score: 95,
    username: 'cici',
    date: ISODate('2024-09-03T00:00:00.000Z')
  },
  {
    _id: ObjectId('6a6ff63899e291eadc41adc5'),
    score: 90,
    username: 'king',
    date: ISODate('2024-09-01T00:00:00.000Z')
  },
  {
    _id: ObjectId('6a6ff63899e291eadc41adc6'),
    score: 85,
    username: 'tina',
    date: ISODate('2024-09-02T00:00:00.000Z')
  },
  {
    _id: ObjectId('6a6ff63899e291eadc41adc8'),
    score: 80,
    username: 'king',
    date: ISODate('2024-09-04T00:00:00.000Z')
  },
  {
    _id: ObjectId('6a6ff63899e291eadc41adc9'),
    score: 75,
    username: 'tina',
    date: ISODate('2024-09-05T00:00:00.000Z')
  }
]

输出结果先按分数降序进行排序,然后按用户名升序(字母顺序)进行排序。

8、多键索引

8.1、多键索引介绍

MongoDB数据库多键索引从包含数组值的字段中收集数据并进行排序。多键索引可以提高对数组字段的查询性能。设计人员无须显式指定多键类型,对包含数组值的字段创建索引时,MongoDB会自动将该索引设为多键索引。

MongoDB可以在包含标量值(例如字符串和数字)和嵌入式文档的数组上创建多键索引。如果数组包含同一值的多个实例,则索引仅包含该值的一个条目。设计人员要创建多键索引,可以使用以下原型:

js 复制代码
db.<collection>.createIndex( { <arrayField>: <sortOrder> } )

下面详细介绍关于多键索引的技术细节和使用限制。

  1. 索引边界:索引扫描的边界定义了查询期间要搜索的索引组成部分,多键索引边界的计算遵循特殊规则。
  2. 唯一多键索引:在唯一多键索引中,只要一个文档的索引键值不与另一个文档的索引键值重复,该文档可能包含数组元素,这些元素就会导致索引键值重复。
  3. 复合多键索引:在复合多键索引中,每个索引文档最多可以有一个值为数组的索引字段。

如果索引规范中的多个字段是数组,则无法创建复合多键索引。例如,考虑包含以下文档的集合:

js 复制代码
{ _id: 1, scores_spring: [ 8, 6 ], scores_fall: [ 5, 9 ] }

设计人员无法创建复合多键索引{scores_spring: 1,scores_fall: 1},因为索引中的两个字段都是数组。

如果复合多键索引已存在,则无法插入会违反此限制的文档。例如,考虑包含以下文档的集合:

js 复制代码
{ _id: 1, scores_spring: [8, 6], scores_fall: 9 }
{ _id: 2, scores_spring: 6, scores_fall: [5, 7] }
  1. 排序 :当设计人员根据通过多键索引来创建索引的数组字段进行排序时,除非以下两个条件均成立,否则查询计划将包括阻塞排序阶段。
    • 所有排序字段的索引边界均为MinKey, MaxKey
    • 任何多键已索引字段的边界均不得与排序模式的路径前缀相同。
  2. 分片键:设计人员无法将多键索引指定为分片键索引。但是,在分片键索引是复合索引前缀的情况下,如果尾随键之一(不是分片键的一部分)对数组进行索引,则复合索引可能会成为复合多键索引。
  3. 覆盖查询:多键索引无法涵盖对数组字段的查询。但如果多键索引会跟踪哪个或哪些字段致使该索引成为多键,则该索引可涵盖对非数组字段的查询。
  4. 对数组字段进行整体查询:当查询过滤器指定了与整个数组完全匹配的匹配项时,MongoDB可使用多键索引来查找查询数组的第一个元素,但无法使用多键索引扫描来查找整个数组。相反,在使用多键索引查找查询数组的第一个元素后,MongoDB会检索关联的文档并筛选其数组与查询中的数组匹配的文档。

8.2、在数组字段上创建索引

MongoDB数据库支持设计人员在包含数组值的字段上创建索引,以优化对该字段的查询性能。当设计人员在含有数组值的字段上创建索引时,MongoDB会将该索引存储为多键索引。

如果要创建索引,可以使用db.collection.createIndex()方法,具体代码如下:

js 复制代码
db.<collection>.createIndex( { <field>: <sortOrder> } )

在下面的应用示例中,创建了一个students集合,其中包含以下文档:

js 复制代码
db.students.insertMany([
	{
		"name": "king",
		"test_scores": [86, 95]
	},
	{
		"name": "tina",
		"test_scores": [72, 63]
	},
	{
		"name": "cici",
		"test_scores": [99, 98]
	}
])

设计人员可以定期运行一个查询,该查询至少返回一个test_score大于85的学生,可对test_scores字段创建索引,从而为此查询提高性能。

下面的代码将在students集合的test_scores字段上创建升序多键索引。

js 复制代码
db.students.createIndex( { test_scores: 1 } )

由于test_scores包含数组值,因此MongoDB会将此索引存储为多键索引。该索引包含test_scores字段中显示的每个单独值的键。索引为升序,即按此顺序存储键值:​63, 72, 86, 95, 98, 99​。

该索引支持选择test_scores字段的查询。例如,以下查询返回test_scores数组中至少有一个元素大于85的文档:

js 复制代码
db.students.find({
	test_scores: {$elemMatch: {$gt: 85}}
})

输出结果如下:

js 复制代码
[
  {
    _id: ObjectId('6a6ffa2d99e291eadc41adca'),
    name: 'king',
    test_scores: [ 86, 95 ]
  },
  {
    _id: ObjectId('6a6ffa2d99e291eadc41adcc'),
    name: 'cici',
    test_scores: [ 99, 98 ]
  }
]

8.3、为数组中的嵌入字段创建索引

MongoDB数据库可以在数组中的嵌入式文档字段上创建索引。这些索引可以提高对数组中的特定嵌入字段进行查询的性能。当设计人员在数组中的字段上创建索引时,MongoDB会将该索引存储为多键索引。

想要创建索引,可以使用db.collection.createIndex()方法,具体代码如下:

js 复制代码
db.<collection>.createIndex( { <field>: <sortOrder> } )

在下面的应用示例中,创建了一个inventory集合,其中包含以下文档:

js 复制代码
db.inventory.insertMany([
	{
		"item": "t-shirt",
		"stock": [
			{
				"size": "small",
				"quantity": 8
			},
			{
				"size": "large",
				"quantity": 10
			}
		]
	},
	{
		"item": "sweater",
		"stock": [
			{
				"size": "small",
				"quantity": 4
			},
			{
				"size": "large",
				"quantity": 7
			}
		]
	},
	{
		"item": "vest",
		"stock": [
			{
				"size": "small",
				"quantity": 6
			},
			{
				"size": "large",
				"quantity": 1
			}
		]
	}
])

每当库存少于5件商品时,都需要通过订购增加更多的库存。如果想要查找重新排序的项目,可以查询stock数组中某个元素的quantity小于5的文档。若要提高此查询的性能,则可在stock.quantity字段上创建索引。

下面的操作将在inventory集合的stock.quantity字段上创建升序多键索引,具体代码如下:

js 复制代码
db.inventory.createIndex( { "stock.quantity": 1 } )

由于stock包含数组值,因此MongoDB会将此索引存储为多键索引。该索引包含stock.quantity字段中显示的每个单独值的键。索引为升序,即按此顺序存储键值:​1, 4,6, 7, 8, 10​。

该索引支持选择stock.quantity字段的查询。例如,以下查询会返回stock数组中至少有一个元素的quantity少于5的文档:

js 复制代码
db.inventory.find({
	"stock.quantity": {$lt: 5}
})

输出结果如下:

js 复制代码
[
  {
    _id: ObjectId('6a6ffb7999e291eadc41adcf'),
    item: 'vest',
    stock: [ { size: 'small', quantity: 6 }, { size: 'large', quantity: 1 } ]
  },
  {
    _id: ObjectId('6a6ffb7999e291eadc41adce'),
    item: 'sweater',
    stock: [ { size: 'small', quantity: 4 }, { size: 'large', quantity: 7 } ]
  }
]

该索引还支持对stock.quantity字段进行排序操作,请看下面的查询代码:

js 复制代码
db.inventory.find().sort( { "stock.quantity": -1 } )

输出结果如下:

js 复制代码
[
  {
    _id: ObjectId('6a6ffb7999e291eadc41adcd'),
    item: 't-shirt',
    stock: [ { size: 'small', quantity: 8 }, { size: 'large', quantity: 10 } ]
  },
  {
    _id: ObjectId('6a6ffb7999e291eadc41adce'),
    item: 'sweater',
    stock: [ { size: 'small', quantity: 4 }, { size: 'large', quantity: 7 } ]
  },
  {
    _id: ObjectId('6a6ffb7999e291eadc41adcf'),
    item: 'vest',
    stock: [ { size: 'small', quantity: 6 }, { size: 'large', quantity: 1 } ]
  }
]

当设计人员对对象数组进行降序排序时,MongoDB会首先根据拥有最大值元素的字段进行排序。

8.4、多键索引边界

MongoDB数据库的索引边界定义了MongoDB使用索引执行查询时搜索的索引值的范围。当设计人员在索引字段上指定多个查询关键词时,MongoDB会尝试合并这些关键词的边界,以生成边界更小的索引扫描。这样做的好处是,较小的索引边界可以加快查询速度并减少资源的使用。同时,MongoDB数据库通过相交或复合边界来组合边界。

MongoDB多键索引的边界交集是指多个边界重叠的点。例如,给定边界​\[1, Infinity​]和​\[ -Infinity, 8 ​]​,那么边界的交集结果为​\[ 1, 8 ​]​。给定一个索引数组字段,考虑一个在数组上指定多个查询关键词并使用多键索引来完成查询的查询。如果$elemMatch操作符连接查询关键词,则MongoDB数据库可以与多键索引边界相交。

在下面的代码示例中,演示了MongoDB如何使用边界交集来定义要查询的较小范围的值,从从而提高MongoDB的查询性能,具体步骤如下。

步骤1:填充样本集合并创建students集合,其中包含具有字段name和数组字段grades的文档:

js 复制代码
db.students.insertMany([
	{_id: 1, name: "king", grades: [85, 90]},
	{_id: 2, item: "cici", grades: [95, 99]}
])

步骤2:在grades数组上创建多键索引:

js 复制代码
db.students.createIndex({ grades: 1 })

步骤3:运行以下代码进行集合查询:

js 复制代码
db.students.find({ grades: { $elemMatch: { $gte: 91, $lte: 100 }}})

[ { _id: 2, item: 'cici', grades: [ 95, 99 ] } ]

上面的查询使用$elemMatch操作符返回文档,其中grades数组至少包含一个与两个指定条件匹配的元素,具体说明如下:

  • 大于或等于91关键词($gte: 91)的边界为\[ 91, Infinity]。
  • 小于或等于100关键词($lte: 99)的边界为\[ -Infinity,100 ]。

由于查询使用$elemMatch连接这些关键词,因此MongoDB与边界相交:

js 复制代码
ratings: [[ 91, 100 ]]

如果查询未使用$elemMatch连接数组字段上的条件,则MongoDB无法与多键索引边界相交。那么可以考虑以下查询代码:

js 复制代码
db.students.find({ grades: { $gte: 91, $lte: 100 } })

[ { _id: 2, item: 'cici', grades: [ 95, 99 ] } ]

上述查询代码在grades数组中搜索:

  • 至少一个元素大于或等于91。
  • 至少一个元素小于或等于100。

同一元素可以同时满足这两个条件。

由于前面的查询未使用$elemMatch操作符,因此MongoDB不会与边界相交。如果使用$elemMatch操作符,则MongoDB会使用以下任一边界:

  • \[ 91, Infinity ]。
  • \[ -Infinity, 100 ]。

MongoDB数据库不保证它会选择这两个边界中的哪一个。

8.5、多键索引的复合边界

MongoDB数据库的复合边界组合复合索引的多个键的边界。使用多个键的边界可以减少处理查询所需的时间,因为MongoDB不需要单独计算每个边界的结果。

在下面的代码示例中,考虑具有以下边界的复合索引{temperature: 1, humidity: 1 },具体说明如下:

  • temperature的边界为 \[ 80, Infinity ]。
  • humidity的边界为 \[ -Infinity, 20 ]。

对边界进行复合会导致使用两个边界:

js 复制代码
{ temperature: [ [ 80, Infinity ] ], humidity: [ [ -Infinity, 20 ] ] }

如果MongoDB无法组合这两个边界,那么MongoDB将按前导字段上的边界限制索引扫描。在上面的示例中,前导字段为temperature,因此约束条件为temperature: ​\[80, Infinity ​]​。

在下面非数组字段和数组字段的复合边界的代码示例中,演示了MongoDB数据库如何使用复合边界来定义更高效的查询约束,从而提高查询性能。

首先,填充样本集合并创建survey集合,其中包含具有字段item和数组字段ratings的文档。

js 复制代码
db.survey.insertMany([
	{_id: 1, item: "ABC", ratings: [1, 9]},
	{_id: 2, item: "XYZ", ratings: [6, 3]}
])

然后,在item和ratings字段上创建复合多键索引:

js 复制代码
db.survey.createIndex({ item: 1, ratings: 1 })

最后,运行以下代码进行集合查询:

js 复制代码
db.survey.find( { item: "XYZ", ratings: { $gte: 3 } } )

[ { _id: 2, item: 'XYZ', ratings: [ 6, 3 ] } ]

上面的查询在索引的两个键(item和ratings)上指定了一个条件,分别采用以下关键词:

  • item: "XYZ":谓词的边界为\[ "XYZ", "XYZ" ]。
  • ratings:{ $gte: 3 }:谓词的边界为\[ 3, Infinity ]。

MongoDB数据库使用以下各项的组合边界:

js 复制代码
{ item: [ [ "XYZ", "XYZ" ] ], ratings: [ [ 3, Infinity ] ] }

在下面非数组字段和多个数组字段的复合边界的代码示例中,演示了当索引包含一个非数组字段和多个数组字段时,MongoDB如何使用复合边界。

首先,填充样本集合并创建survey2集合,其中包含具有字段item和数组字段ratings的文档。

js 复制代码
db.survey2.insertMany([
	{
		_id: 1,
		item: "ABC",
		ratings: [
			{score: 2, by: "mn"},
			{score: 9, by: "anon"}
		]
	},
	{
		_id: 2,
		item: "XYZ",
		ratings: [
			{score: 5, by: "anno"},
			{score: 7, by: "wv"}
		]
	}
])

然后,创建复合多键索引,在item和ratings字段上创建复合多键索引。

js 复制代码
db.survey2.createIndex({
	"item": 1,
	"ratings.score": 1,
	"ratings.by": 1
})

在上面的码证中,描述了如下内容:

  • item(non-array)。
  • ratings.score(数组)。
  • ratings.by(数组)。

最后,运行以下代码进行集合查询:

js 复制代码
db.survey2.find({
	item: "XYZ",
	"ratings.score": {$lte: 5},
	"ratings.by": "anno"
})

[
  {
    _id: 2,
    item: 'XYZ',
    ratings: [ { score: 5, by: 'anno' }, { score: 7, by: 'wv' } ]
  }
]

上面的查询分别采用如下关键词:

  • item: "XYZ":谓词的边界为\[ "XYZ", "XYZ" ]。
  • score: { $lte: 5 }:谓词的边界为 \[ -Infinity, 5 ]。
  • by: "anon":谓词的边界为 "anon", "anon"

MongoDB将item键的边界与ratings.score的边界或ratings.by的边界进行复合,具体取决于查询谓词和索引键值。另外,MongoDB数据库不保证它与item字段进行复合的边界。

MongoDB通过以下方式之一完成查询:

  • MongoDB将item边界与ratings.score边界相结合:
  • MongoDB将item边界与ratings.by边界相结合:

    在上面的代码中,想要将ratings.score的边界与ratings.by的边界复合,查询必须使用$elemMatch操作符。

对于同一数组中多个字段的复合边界的示例,想要复合同一数组中索引键的边界,以下两个条件必须为true:

  • 索引键必须共享相同的字段路径(Field Path),但不包括字段名称。
  • 查询必须在该路径上使用$elemMatch指定字段的谓词。

对于嵌入式文档中的字段,虚线字段名称(例如a.b.c.d)是d的字段路径(Field Path)。想要复合来自同一数组的索引键的边界,$elemMatch必须位于路径上,但不包括字段名称本身(例如a.b.c)​。

在下面的代码示例中,演示了MongoDB如何组合来自同一数组的索引键的边界,此示例使用前一示例中的survey2集合。

首先,在ratings.score字段和ratings.by字段上创建复合索引:

js 复制代码
db.survey2.createIndex( { "ratings.score": 1, "ratings.by": 1 } )

其中,字段ratings.score和ratings.by共享字段路径ratings。

然后,运行以下代码进行集合查询:

js 复制代码
db.survey2.find({ ratings: { $elemMatch: { score: { $lte: 5 }, by: "anon" } } })

上面的查询在ratings字段上使用$elemMatch操作符,以要求数组至少包含一个同时匹配这两个条件的单个元素,分别采用如下的关键词。

  • score: { $lte: 5 }:关键词的边界为 \[ -Infinity, 5 ]。
  • by: "anon"​:关键词的边界为 \[ "anon", "anon" ]。

MongoDB数据库将两个边界复合为以下边界:

js 复制代码
{ "ratings.score" : [ [ -Infinity, 5 ] ], "ratings.by" : [ [ "anon", "anon" ] ] }

如果查询在偏离公共路径的字段上指定$elemMatch操作符,那么MongoDB无法复合来自同一数组的索引键的边界。

在下面的代码示例中,演示了在分叉字段路径(FieldPath)上如何使用$elemMatch操作符。

首先,填充样本集合并创建collectionsurvey3集合,其中包含具有字符串字段item和数组字段ratings的文档。

js 复制代码
db.survey3.insertMany([
	{
		_id: 1,
		item: "ABC",
		ratings: [
			{scores: [{q1: 2, q2: 4}, {q1: 3, q2: 8}], loc: "A"},
			{scores: [{q1: 2, q2: 5}], loc: "B"}
		]
	},
	{
		_id: 2,
		item: "XYZ",
		ratings: [
			{scores: [{q1: 7}, {q1: 2, q2: 8}], loc: "B"}
		]
	}
])

然后,在ratings.scores.q1和ratings.scores.q2字段上创建复合索引:

js 复制代码
db.survey3.createIndex({ "ratings.scores.q1": 1, "ratings.scores.q2": 1 })

在上面的代码中,字段ratings.scores.q1和ratings.scores.q2共享字段路径ratings.scores。为了复合索引边界,查询必须在公共字段路径(Field Path)上使用$elemMatch操作符。

最后,以下查询使用不在所需路径上的$elemMatch操作符:

js 复制代码
db.survey3.find({ ratings: { $elemMatch: { 'scores.q1': 2, 'scores.q2': 8 } } })

[
  {
    _id: 1,
    item: 'ABC',
    ratings: [
      { scores: [ { q1: 2, q2: 4 }, { q1: 3, q2: 8 } ], loc: 'A' },
      { scores: [ { q1: 2, q2: 5 } ], loc: 'B' }
    ]
  },
  {
    _id: 2,
    item: 'XYZ',
    ratings: [ { scores: [ { q1: 7 }, { q1: 2, q2: 8 } ], loc: 'B' } ]
  }
]

MongoDB无法复合索引边界,并且ratings.scores.q2字段在索引扫描期间不受约束。想要复合边界,查询必须在公共路径$elemMatch操作符上使用ratings.scores:

js 复制代码
db.survey3.find({ 'ratings.scores': { $elemMatch: { 'q1': 2, 'q2': 8 } } })

[
  {
    _id: 2,
    item: 'XYZ',
    ratings: [ { scores: [ { q1: 7 }, { q1: 2, q2: 8 } ], loc: 'B' } ]
  }
]

9、通配符索引

9.1、通配符索引介绍

MongoDB数据库支持在一个字段或一组字段上创建索引,以提高查询性能。MongoDB支持灵活模式,这表明文档字段名在一个集合中可能会有所不同。我们可以使用通配符索引来支持对任意或未知字段的查询。

例如,在MongoDB数据库中创建通配符索引,需要使用通配符说明符($**)作为索引键,具体代码如下:

js 复制代码
db.collection.createIndex({ "$**": <sortOrder> })

在MongoDB数据库中,可以使用以下几个命令创建通配符索引:

  • createIndexes。
  • db.collection.createIndex()。
  • db.collection.createIndexes()。

MongoDB数据库仅当需要索引的字段未知或可能更改时,才可以使用通配符索引。通配符索引的性能通常不及针对特定字段的目标索引。如果集合包含阻止目标索引建立的任意字段名称,则应考虑重新构建模式以获得一致的字段名称。

通配符索引的具体行为描述如下:

  • 设计人员可以在一个集合中创建多个通配符索引。
  • 通配符索引可涵盖与集合中其他索引相同的字段。
  • 默认情况下,通配符索引会省略_id字段。如果想要将_id字段包含在通配符索引中,则必须明确指定{ "_id" : 1}字段。
  • 通配符索引是稀疏索引,仅包含具有索引字段的文档的条目,即使索引字段包含空值也是如此。
  • 通配符索引不同于通配符文本索引,并且与通配符文本索引不兼容。
  • 通配符索引不支持使用$text操作符的查询。

另外,仅当满足以下所有条件时,通配符索引才支持覆盖查询:

  • 查询规划器选择通配符索引来满足查询条件。
  • 查询关键词可精确指定通配符索引所涵盖的一个字段。
  • 查询投影明确排除_id,仅包含查询字段。
  • 查询指定的字段绝对不能是数组。

请参考employees集合中以下通配符索引:

js 复制代码
db.employees.createIndex({ "$**" : 1 })

以下操作查询单个字段lastName并从结果文档中投影出所有其他字段:

js 复制代码
db.employees.find(
  { "lastName" : "king" },
  { "_id" : 0, "lastName" : 1 }
)

在上面的代码中,如果指定的lastName不是数组,则MongoDB数据库可以使用$**通配符索引来支持覆盖的查询。

9.2、对单个字段创建通配符索引

在MongoDB数据库中,单个字段上的通配符索引支持对索引字段的任何子字段进行查询,使用通配符索引来支持对事先不知道或因文档而异的字段名称的查询。

想要对单个字段创建通配符索引,可使用db.collection.createIndex()方法并在索引键中包含通配符说明符($**):

js 复制代码
db.collection.createIndex( { "<field>.$**": <sortOrder> } )

首先,创建一个products集合,其中包含一个名称为collectionsurvey3的集合文档,具体代码如下:

js 复制代码
db.products.insertMany([
	{
		"product_name": "Spy Coat",
		"attributes": {
			"material": ["Tweed", "Wool", "Leather"],
			"size": {
				"length": 72,
				"units": "inches"
			}
		}
	},
	{
		"product_name": "Spy Pen",
		"attributes": {
			"colors": ["Blue", "Black"],
			"secret_feature": {
				"name": "laser",
				"power": "1000",
				"units": "watts"
			}
		}
	}
])

然后,通过以下操作在attributes字段上创建一个通配符索引:

js 复制代码
db.products.createIndex({ "attributes.$**" : 1 })

最后,通配符索引支持对attributes或其嵌入式字段进行单字段查询,请看下面的查询代码示例:

js 复制代码
db.products.find({ "attributes.size.length" : { $gt : 60 } })

结果输出如下:

js 复制代码
[
  {
    _id: ObjectId('6a7009a199e291eadc41add0'),
    product_name: 'Spy Coat',
    attributes: {
      material: [ 'Tweed', 'Wool', 'Leather' ],
      size: { length: 72, units: 'inches' }
    }
  }
]

9.3、在通配符索引中包含或排除字段

在MongoDB数据库中创建通配符索引时,设计人员可以指定想要在索引中包含或排除的字段,具体内容如下:

  • 创建仅涵盖特定字段的通配符索引。例如,如果有多个包含多个子字段的嵌入式文档,则可以创建索引以涵盖对嵌入式文档及其子字段的查询。
  • 创建省略特定字段的通配符索引。例如,如果有一个集合,其中包含一个从未查询过的字段,则可以从索引中省略该字段。

如要在通配符索引中包含或排除字段,可以在wildcardProjection选项中指定所选的字段:

在上面的代码中,wildcardProjection选项的值0或1指示索引是包含还是排除该字段,具体说明如下。

  • 0:表示已排除该字段。
  • 1:表示包含该字段。

另外,使用wildcardProjection选项时有些限制,具体内容如下:

  • 想要使用wildcardProjection选项,索引键必须为$**。
  • 通配符索引不支持在wildcardProjection选项中混用包含和排除语句,除非显示包含_id字段时。

在下面的应用示例中,创建了一个products集合,其中包含以下文档:

js 复制代码
db.products.insertMany([
	{
		"item": "t-shirt",
		"price": "39.99",
		"attributes": {
			"material": "cotton",
			"color": "blue",
			"size": {
				"units": "cm",
				"length": 76
			}
		}
	},
	{
		"item": "milk",
		"price": "6.99",
		"attributes": {
			"sellBy": "02-06-2024",
			"type": "oat"
		}
	},
	{
		"item": "laptop",
		"price": "339.99",
		"attributes": {
			"memory": "8GB",
			"size": {
				"units": "inches",
				"height": 10,
				"width": 15
			}
		}
	}
])

在上面的代码中,每个文档都有一个包含产品详细信息的attributes字段,attributes的子字段因产品而异。

然后,可以使用wildcardProjection选项在通配符索引中包含特定字段。如果设计人员需要经常查询某些文档字段,可以在wildcardProjection选项中指定这些字段以支持这些查询,不给索引增加不必要的臃肿。

下面的代码会创建一个通配符索引,其中包含attributes.size字段和attributes.color字段的所有标量值(即字符串和数字)​:

js 复制代码
db.products.createIndex(
	{ "$**": 1 },
	{
		wildcardProjection: {
			"attributes.size": 1,
			"attributes.color": 1
		}
	}
)

虽然索引键模式$**涵盖文档中的所有字段,但wildcardProjection选项将索引限制为仅包含所包含的字段。如果字段是嵌入式文档或数组(如attributes.size)​,则通配符索引将对该字段进行递归并对所有内嵌的标量字段值进行索引。创建的索引支持对wildcardProjection选项中包含的任何标量值进行查询。

例如,索引支持以下查询:

js 复制代码
db.products.find( { "attributes.size.height" : 10 } )
db.products.find( { "attributes.color" : "blue" } )

上述索引仅支持对wildcardProjection选项中包含的字段进行查询。在该示例中,MongoDB数据库对以下查询执行集合扫描,因为其包含wildcardProjection选项中不存在的字段。

js 复制代码
db.products.find ( { "item": "milk" } )

如果存在很少查询的文档字段,则可以创建省略这些字段的通配符索引。下面的代码在products集合中的所有文档字段上创建通配符索引,但在索引中省略了attributes.memory字段。

js 复制代码
db.products.createIndex(
	{"$**": 1},
	{
		"wildcardProjection": {
			"attributes.memory": 0
		}
	}
)

虽然索引键模式$**涵盖文档中的所有字段,但wildcardProjection选项从索引中排除了attributes.memory字段的值。

如果字段是嵌入式文档或数组(如attributes.size)​,则通配符索引将对该字段进行递归并对所有内嵌的标量字段值进行索引。例如,索引支持以下查询:

js 复制代码
db.products.find( { "attributes.color" : "blue" } )
db.products.find( { "attributes.size.height" : 10 } )

上述索引不支持对attributes.memory字段进行查询,因为索引中省略了该字段。

9.4、对所有字段创建通配符索引

在MongoDB数据库中,可以通过创建通配符索引来支持对所有可能的文档字段进行查询,通配符索引支持对任意或未知字段名称进行查询。如果要在所有字段(不包括_id)上创建通配符索引,可以使用通配符说明符($**)作为索引键,具体代码如下:

js 复制代码
db.<collection>.createIndex({ "$**": <sortOrder> })

一般只有当需要索引的字段未知或可能更改时,才需要使用通配符索引。通配符索引的性能不及针对特定字段的目标索引。如果集合包含阻止目标索引建立的任意字段名称,则应考虑重新构建模式以获得一致的字段名称。

在下面的应用示例中,创建了一个artwork集合,其中包含以下文档:

js 复制代码
db.artwork.insertMany([
	{
		"name": "Scream",
		"artist": "king",
		"style": "modern",
		"themes": ["humanity", "horror"]
	},
	{
		"name": "Acrobats",
		"artist": {
			"name": "tina",
			"nationality": "French",
			"yearBorn": 1877
		},
		"originalTitle": "Les acrobates",
		"dimensions": [65, 49]
	},
	{
		"name": "Thinker",
		"type": "sculpture",
		"materials": ["bronze"],
		"year": 1904
	}
])

在上面的代码中,每个文档都包含有关艺术品的详细信息。字段名称因文档而异,具体取决于作品的可用信息。

首先,以下操作会在artwork集合中的所有文档字段(不包括_id)上创建通配符索引。

js 复制代码
db.artwork.createIndex({ "$**" : 1 })

以上索引支持对集合中任意字段进行单字段查询。如果文档包含嵌入式文档或数组,则通配符索引会遍历文档或数组,并将所有字段的值存储在文档或数组中。例如,该索引支持以下几种查询。

1)查询一

js 复制代码
db.artwork.find({ "style": "modern" })

结果输出如下:

js 复制代码
[
  {
    _id: ObjectId('6a700e5099e291eadc41add5'),
    name: 'Scream',
    artist: 'king',
    style: 'modern',
    themes: [ 'humanity', 'horror' ]
  }
]

2)查询二

js 复制代码
db.artwork.find({ "artist.nationality": "French" })

结果输出如下:

js 复制代码
[
  {
    _id: ObjectId('6a700e5099e291eadc41add6'),
    name: 'Acrobats',
    artist: { name: 'tina', nationality: 'French', yearBorn: 1877 },
    originalTitle: 'Les acrobates',
    dimensions: [ 65, 49 ]
  }
]

3)查询三

js 复制代码
db.artwork.find({ "materials": "bronze" })

结果输出如下:

js 复制代码
[
  {
    _id: ObjectId('6a700e5099e291eadc41add7'),
    name: 'Thinker',
    type: 'sculpture',
    materials: [ 'bronze' ],
    year: 1904
  }
]
相关推荐
影寂ldy1 小时前
WinForm 完整版分页查询(多条件搜索+页码切换+每页条数切换)
数据库
_oP_i2 小时前
Another Redis Desktop Manager更新
数据库·redis·缓存
宋浮檀s2 小时前
春秋云境——CVE-2022-28512
数据库·安全·web安全
MC皮蛋侠客2 小时前
SQLAlchemy 系列(三):声明式映射与 Schema——让类型、默认值和约束一致
数据库·python
石小千2 小时前
排查MongoDB慢日志问题
数据库·mongodb
安_2 小时前
如何为 RAG 项目选择向量数据库?Milvus、Pinecone、Chroma 怎么选?
数据库·milvus
lupai2 小时前
短信接口快速接入与调用实战指南
java·开发语言·数据库
snpgroupcn3 小时前
企业有必要做 SAP 数据归档吗?优势与价值分析
数据库·oracle
2601_965798473 小时前
Build a Consulting Site in 2026: Nifty Theme Practical Review
java·linux·数据库