MongoDB——复合索引

425 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

介绍

复合索引是多个字段组合而成的索引,可以支持在多个字段傻姑娘匹配的查询,其性质和单字段索引类似。但不同的是,复合索引中字段的顺序、字段的升降序对查询性能有直接的影响,因此在设计复合索引时则需要考虑不同的查询场景。

注意:

  • 任意一个复合索引字段不能超过32个
  • MongoDB 4.2之前复合索引不包含哈希索引,MongoDB 4.4开始复合索引可能包含单个哈希索引字段

创建方式:

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

结构

复合索引的结构依然是基于B+树的结构,我们可能会认为索引是二维的,但事实并非如此。实际上是一维的,可以看作是一个有序列表。

例如:我们对last_namefirst_name来进行索引

image.png 如果我们想找到Adam Bailey这条记录,则通过BaileyAdam索引键,然后指向我们需要的记录。我们扫描到这个索引,则就会返回指向的文档。

如果我们想找到Chris Bailey,则先去找last_name = Bailey的,然后再找出first_name = Chris的,最后找到该条文档。如果我们想跟据last_name先找那将非常容易。

但是,我们如果想要先找first_name = James,我们必须遍历每一个键或者每一个文档才能找到。

正如我们所看到的,首先出现的一个或多个字段在某些方面比后面的字段更重要。

应用

继续使用 person.json的数据。

我们先查询以下语句

exp = db.people.explain("executionStats")
exp.find( { "last_name" : "Frazier", "first_name": "Jasmine" } )
{
  executionStats: {
    executionSuccess: true,
    nReturned: 1,
    executionTimeMillis: 27,
    totalKeysExamined: 0,
    totalDocsExamined: 50474,
    executionStages: {
      stage: 'COLLSCAN',
      filter: {
        '$and': [
          { first_name: { '$eq': 'Jasmine' } },
          { last_name: { '$eq': 'Frazier' } }
        ]
      },
    }
 }

我们可以看到找到这条文档花费了27ms,使用了全表扫描,意味着我们没有使用索引。我们必须通过扫描5万条记录才能查询到一条记录。

然后我们给last_name建立一个单键索引,然后再次查询

db.people.createIndex({"last_name": 1})
exp.find( { "last_name" : "Frazier", "first_name": "Jasmine" } )
{
  executionStats: {
    executionSuccess: true,
    nReturned: 1,
    executionTimeMillis: 2,
    totalKeysExamined: 31,
    totalDocsExamined: 31,
    executionStages: {
      stage: 'FETCH',
      filter: { first_name: { '$eq': 'Jasmine' } }
      inputStage: {
        stage: 'IXSCAN'
      }
    }
}

我们可以看到扫描了31个索引键并找到一条文档,相比较全表要快很多。查询时间只用了2ms,然后再通过FETCH扫描31条文档才找到first_name = Jasmine这条记录。

假如说我们将first_name做成复合索引,应该是什么情况?要注意字段之间的顺序以及排序是很重要的。

db.people.createIndex({"last_name": 1, "first_name": 1})
exp.find( { "last_name" : "Frazier", "first_name": "Jasmine" } )
{
 explainVersion: '1',
  queryPlanner: {
    rejectedPlans: [
      {
        stage: 'FETCH',
        filter: { first_name: { '$eq': 'Jasmine' } },
        inputStage: {
          stage: 'IXSCAN',
          keyPattern: { last_name: 1 },
          indexName: 'last_name_1',
        }
      }
    ]
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 1,
    executionTimeMillis: 1,
    totalKeysExamined: 1,
    totalDocsExamined: 1,
    executionStages: {
      stage: 'FETCH',
      nReturned: 1,
      inputStage: {
        stage: 'IXSCAN',
        nReturned: 1,
        keyPattern: { last_name: 1, first_name: 1 },
        indexName: 'last_name_1_first_name_1'
      }
    }
  }
}

这次我们可以看到只花费了1毫秒,只扫描了一个索引键,还扫描了一个文档,可以说是非常快,比例也是最佳的比例。我们还可以看到将我们前面的单键索引,服务器给拒绝了。

让我们在稍微修改一下查询

db.people.createIndex({"last_name": 1, "first_name": 1})
exp.find( { "last_name" : "Frazier", "first_name": {$gte: "L"} } )
{
  executionStats: {
    executionSuccess: true,
    nReturned: 16,
    executionTimeMillis: 0,
    totalKeysExamined: 16,
    totalDocsExamined: 16,
  }
}

我们可以发现扫描了16个索引键,16个文档,返回了16个文档,这个比例依然是最佳的。还不必去查看其他多余的文档。因此,我们可以通过复合索引查询多字段。并且会按照我们索引键的排序方式给我们返回。