首页 > 编程笔记 > MongoDB 阅读:3

MongoDB多键索引详解(附带实例)

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

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

设计人员要创建多键索引,可以使用以下原型:
db.<collection>.createIndex( { <arrayField>: <sortOrder> } )
下面详细介绍关于多键索引的技术细节和使用限制。

1) 索引边界

索引扫描的边界定义了查询期间要搜索的索引组成部分,多键索引边界的计算遵循特殊规则。

2) 唯一多键索引

在唯一多键索引中,只要一个文档的索引键值不与另一个文档的索引键值重复,该文档可能包含数组元素,这些元素就会导致索引键值重复。

3) 复合多键索引

在复合多键索引中,每个索引文档最多可以有一个值为数组的索引字段。

如果索引规范中的多个字段是数组,则无法创建复合多键索引。例如,考虑包含以下文档的集合:
{ _id: 1, scores_spring: [ 8, 6 ], scores_fall: [ 5, 9 ] }
设计人员无法创建复合多键索引{scores_spring: 1, scores_fall: 1},因为索引中的两个字段都是数组。

如果复合多键索引已存在,则无法插入会违反此限制的文档。例如,考虑包含以下文档的集合:
{ _id: 1, scores_spring: [8, 6], scores_fall: 9 }
{ _id: 2, scores_spring: 6, scores_fall: [5, 7] }
设计人员可以创建复合多键索引 {scores_spring: 1, scores_fall: 1},因为对于每个文档,只有一个使用复合多键索引形式来构建索引的字段是数组。没有文档同时包含 scores_spring 和 scores_fall 字段的数组值。但是,在创建复合多键索引后,如果尝试插入 scores_spring 和 scores_fall 字段均为数组的文档,则插入操作会失败。

4) 排序

当设计人员根据通过多键索引来创建索引的数组字段进行排序时,除非以下两个条件均成立,否则查询计划将包括阻塞排序阶段:

5) 分片键

设计人员无法将多键索引指定为分片键索引。但是,在分片键索引是复合索引前缀的情况下,如果尾随键之一(不是分片键的一部分)对数组进行索引,则复合索引可能会成为复合多键索引。

6) 覆盖查询

多键索引无法涵盖对数组字段的查询。但如果多键索引会跟踪哪个或哪些字段致使该索引成为多键,则该索引可涵盖对非数组字段的查询。

7) 对数组字段进行整体查询

当查询过滤器指定了与整个数组完全匹配的匹配项时,MongoDB 可使用多键索引来查找查询数组的第一个元素,但无法使用多键索引扫描来查找整个数组。

相反,在使用多键索引查找查询数组的第一个元素后,MongoDB 会检索关联的文档并筛选其数组与查询中的数组匹配的文档。

在数组字段上创建索引

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

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

在下面的应用示例中,创建了一个 students 集合,其中包含以下文档:
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 字段上创建升序多键索引。
db.students.createIndex( { test_scores: 1 } )
由于 test_scores 包含数组值,因此 MongoDB 会将此索引存储为多键索引。该索引包含 test_scores 字段中显示的每个单独值的键。索引为升序,即按此顺序存储键值:[63, 72, 86, 95, 98, 99]。

该索引支持选择 test_scores 字段的查询。例如,以下查询返回 test_scores 数组中至少有一个元素大于 85 的文档:
db.students.find({
  test_scores: { $elemMatch: { $gt: 85 } }
})
输出结果如下:
{ _id: ObjectId("6322...."), name: 'king', test_scores: [ 86, 95 ] }
{ _id: ObjectId("6322...."), name: 'cici', test_scores: [ 99, 98 ] }

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

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

想要创建索引,可以使用 db.collection.createIndex() 方法,具体代码如下:
db.<collection>.createIndex( { <field>: <sortOrder> } )
在下面的应用示例中,创建了一个 inventory 集合,其中包含以下文档:
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 字段上创建升序多键索引,具体代码如下:
db.inventory.createIndex( { "stock.quantity": 1 } )
由于 stock 包含数组值,因此 MongoDB 会将此索引存储为多键索引。该索引包含 stock.quantity 字段中显示的每个单独值的键。索引为升序,即按此顺序存储键值:[1, 4, 6, 7, 8, 10]。

该索引支持选择 stock.quantity 字段的查询。例如,以下查询会返回 stock 数组中至少有一个元素的 quantity 少于 5 的文档:
db.inventory.find({ "stock.quantity": { $lt: 5 } })
输出结果如下:
{ _id: ObjectId("6368..."), item: 'vest', stock: [ { size: 'small', quantity: 6 }, { size: 'large', quantity: 1 } ] }
{ _id: ObjectId("6368...."), item: 'sweater', stock: [ { size: 'small', quantity: 4 }, { size: 'large', quantity: 7 } ] }

该索引还支持对 stock.quantity 字段进行排序操作,请看下面的查询代码:
db.inventory.find().sort({ "stock.quantity": -1 })
输出结果如下:
{ _id: ObjectId("6368...."), item: 't-shirt', stock: [ { size: 'small', quantity: 8 }, { size: 'large', quantity: 10 } ] }
{ _id: ObjectId("6368...."), item: 'sweater', stock: [ { size: 'small', quantity: 4 }, { size: 'large', quantity: 7 } ] }
{ _id: ObjectId("6368...."), item: 'vest', stock: [ { size: 'small', quantity: 6 }, { size: 'large', quantity: 1 } ] }
当设计人员对对象数组进行降序排序时,MongoDB 会首先根据拥有最大值元素的字段进行排序。

多键索引边界

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

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

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

1) 填充样本集合并创建 students 集合,其中包含具有字段 name 和数组字段 grades 的文档:
db.students.insertMany([
  { _id: 1, name: "king", grades: [ 85, 90 ] },
  { _id: 2, name: "cici", grades: [ 95, 99 ] }
])

2) 在 grades 数组上创建多键索引:
db.students.createIndex({ grades: 1 })

3) 运行以下代码进行集合查询:
db.students.find({
  grades: {
    $elemMatch: {
      $gte: 91,
      $lte: 100
    }
  }
})
上面的查询使用 $elemMatch 操作符返回文档,其中 grades 数组至少包含一个与两个指定条件匹配的元素,具体说明如下:
由于查询使用 $elemMatch 连接这些关键词,因此 MongoDB 与边界相交:ratings: [[91, 100]]。

如果查询未使用 $elemMatch 连接数组字段上的条件,则 MongoDB 无法与多键索引边界相交。那么可以考虑以下查询代码:
db.students.find({ grades: { $gte: 91, $lte: 100 } })
上述查询代码在 grades 数组中搜索:
同一元素可以同时满足这两个条件。

由于前面的查询未使用 $elemMatch 操作符,因此 MongoDB 不会与边界相交。如果使用 $elemMatch 操作符,则 MongoDB 会使用以下任一边界:
MongoDB 数据库不保证它会选择这两个边界中的哪一个。

多键索引的复合边界

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

在下面的代码示例中,考虑具有以下边界的复合索引 { temperature: 1, humidity: 1 },具体说明如下:
对边界进行复合会导致使用两个边界:
{ temperature: [ [ 80, Infinity ] ], humidity: [ [ -Infinity, 20 ] ] }
如果 MongoDB 无法组合这两个边界,那么 MongoDB 将按前导字段上的边界限制索引扫描。在上面的示例中,前导字段为 temperature,因此约束条件为 temperature: [[ 80, Infinity ]]。

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

首先,填充样本集合并创建 survey 集合,其中包含具有字段 item 和数组字段 ratings 的文档:
db.survey.insertMany([
  { _id: 1, item: "ABC", ratings: [1, 9] },
  { _id: 2, item: "XYZ", ratings: [6, 3] }
])

然后,在 item 和 ratings 字段上创建复合多键索引:
db.survey.createIndex({ item: 1, ratings: 1 })

最后,运行以下代码进行集合查询:
db.survey.find( { item: "XYZ", ratings: { $gte: 3 } } )

上面的查询在索引的两个键(item和ratings)上指定了一个条件,分别采用以下关键词:
MongoDB 数据库使用以下各项的组合边界:
{ item: [ [ "XYZ", "XYZ" ] ], ratings: [ [ 3, Infinity ] ] }

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

首先,填充样本集合并创建 survey2 集合,其中包含具有字段 item 和数组字段 ratings 的文档:
db.survey2.insertMany([
  {
    _id: 1,
    item: "ABC",
    ratings: [
      { score: 2, by: "mn" },
      { score: 9, by: "anon" }
    ]
  },
  {
    _id: 2,
    item: "XYZ",
    ratings: [
      { score: 5, by: "anon" },
      { score: 7, by: "wv" }
    ]
  }
])

然后,创建复合多键索引,在 item 和 ratings 字段上创建复合多键索引:
db.survey2.createIndex({
  "item": 1,
  "ratings.score": 1,
  "ratings.by": 1
})

在上面的码证中,描述了如下内容:
最后,运行以下代码进行集合查询:
db.survey2.find({
  item: "XYZ",
  "ratings.score": { $lte: 5 },
  "ratings.by": "anon"
})

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

MongoDB 通过以下方式之一完成查询:
1) MongoDB 将 item 边界与 ratings.score 边界相结合:
{
  "item": [["XYZ", "XYZ"]],
  "ratings.score": [[-Infinity, 5]],
  "ratings.by": [[MinKey, MaxKey]]
}

2) MongoDB 将 item 边界与 ratings.by 边界相结合:
{
  "item": [["XYZ", "XYZ"]],
  "ratings.score": [[MinKey, MaxKey]],
  "ratings.by": [["anon", "anon"]]
}
在上面的代码中,想要将 ratings.score 的边界与 ratings.by 的边界复合,查询必须使用 $elemMatch 操作符。

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

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

首先,在 ratings.score 字段和 ratings.by 字段上创建复合索引:
db.survey2.createIndex( { "ratings.score": 1, "ratings.by": 1 } )
其中,字段 ratings.score 和 ratings.by 共享字段路径 ratings。

然后,运行以下代码进行集合查询:
db.survey2.find({ ratings: { $elemMatch: { score: { $lte: 5 }, by: "anon" } } })
上面的查询在 ratings 字段上使用 $elemMatch 操作符,以要求数组至少包含一个同时匹配这两个条件的单个元素,分别采用如下的关键词:
MongoDB 数据库将两个边界复合为以下边界:
{ "ratings.score" : [ [ -Infinity, 5 ] ], "ratings.by" : [ [ "anon", "anon" ] ] }
如果查询在偏离公共路径的字段上指定 $elemMatch 操作符,那么 MongoDB 无法复合来自同一数组的索引键的边界。

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

首先,填充样本集合并创建 collectionsurvey3 集合,其中包含具有字符串字段 item 和数组字段 ratings 的文档。
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 字段上创建复合索引:
db.survey3.createIndex({ "ratings.scores.q1": 1, "ratings.scores.q2": 1 })
在上面的代码中,字段 ratings.scores.q1 和 ratings.scores.q2 共享字段路径 ratings.scores。为了复合索引边界,查询必须在公共字段路径(Field Path)上使用 $elemMatch 操作符。

最后,以下查询使用不在所需路径上的 $elemMatch 操作符:
db.survey3.find({ ratings: { $elemMatch: { 'scores.q1': 2, 'scores.q2': 8 } } })
MongoDB 无法复合索引边界,并且 ratings.scores.q2 字段在索引扫描期间不受约束。想要复合边界,查询必须在公共路径 $elemMatch 操作符上使用 ratings.scores:
db.survey3.find({ 'ratings.scores': { $elemMatch: { 'q1': 2, 'q2': 8 } } })

相关文章