MongoDB——单键索引

106 阅读3分钟

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

介绍

单键索引时MongoDB必须提供的最简单的索引,它是后续所有索引的基础。

在某一个特定的字段建立的索引,主要具有以下特征:

  • 只有一个字段
  • 可以精确查找某一个值。例:按照id查询
  • 可以范围查找。例:按照时间范围查询
  • 可以使用点符号(.)来索引内嵌文档。例如:内嵌文档的某一个字段也是可以创建索引的
  • 可以通过单个查询找到不同的值。例如:$in操作

创建单键索引命令

db.<collection>.createIndex({ <field>: <direction>})

应用

导入数据

首先,我们先在mongo中导入一个 person.json文件

mongoimport -h 127.0.0.1:27017 -u admin -p admin --authenticationDatabase=admin -d test -c people people.json
connected to: mongodb://127.0.0.1:27017/
50474 document(s) imported successfully. 0 document(s) failed to import.

我们一共导入了5万多条记录。我们打开shell执行以下查询命令,并加上explain。

db.people.find({ "ssn" : "720-38-5636" }).explain("executionStats")
{
  explainVersion: '1',
  queryPlanner: {
    namespace: 'test.people',
    indexFilterSet: false,
    parsedQuery: { ssn: { '$eq': '720-38-5636' } },
    queryHash: 'EFBD2442',
    planCacheKey: 'EFBD2442',
    maxIndexedOrSolutionsReached: false,
    maxIndexedAndSolutionsReached: false,
    maxScansToExplodeReached: false,
    winningPlan: {
      //全表扫描
      stage: 'COLLSCAN',
      //筛选条件
      filter: { ssn: { '$eq': '720-38-5636' } },
      direction: 'forward'
    },
    rejectedPlans: []
  },
  executionStats: {
    //执行成功
    executionSuccess: true,
    //实际返回条数
    nReturned: 1,
    //耗费时间:毫秒
    executionTimeMillis: 31,
    //扫描的索引key数
    totalKeysExamined: 0,
    //扫描的文档数
    totalDocsExamined: 50474,
    executionStages: {
      stage: 'COLLSCAN',
      filter: { ssn: { '$eq': '720-38-5636' } },
      nReturned: 1,
      executionTimeMillisEstimate: 3,
      works: 50476,
      advanced: 1,
      needTime: 50474,
      needYield: 0,
      saveState: 50,
      restoreState: 50,
      isEOF: 1,
      direction: 'forward',
      docsExamined: 50474
    }
  },
  command: { find: 'people', filter: { ssn: '720-38-5636' }, '$db': 'test' },
  serverInfo: {
    host: '65fac2418820',
    port: 27017,
    version: '6.0.1',
    gitVersion: '32f0f9c88dc44a2c8073a5bd47cf779d4bfdee6b'
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600
  },
  ok: 1
}

通过queryPlanner.winningPlan 我们可以查看出我们执行全表扫描(COLLSCAN),这说明我们执行的过程中扫描了每个document

executionStats可以看到扫描了5万多条,因为我们的集合就有5万条,最终只返回了一条。这样效率非常低下,当随着数据量的增多,我们要扫描的数据会越来越多。

添加索引

现在我们在ssn字段上添加一个升序索引

db.people.createIndex( { ssn : 1 } )

通过这样做,mongodb 必须查看集合中每一个文档,拿出ssn字段,如果文档中不存在该字段,则该键是一个空值,我们再次执行查询语句。

//创建一个people集合的explain对象,后续可以重复使用
exp = db.people.explain("executionStats")
exp.find( { "ssn" : "720-38-5636" } )
{
  queryPlanner: {
    winningPlan: {
      stage: 'FETCH',
      inputStage: {
        stage: 'IXSCAN',
      }
    }
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 1,
    executionTimeMillis: 3,
    totalKeysExamined: 1,
    totalDocsExamined: 1,
    executionStages: {
      stage: 'FETCH',
      nReturned: 1,
      executionTimeMillisEstimate: 0,
      works: 2,
      advanced: 1,
      inputStage: {
        stage: 'IXSCAN',
        nReturned: 1,
        executionTimeMillisEstimate: 0,
        works: 2,
      }
    }
  }
}

这次我们再查看就会发现是索引扫描(IXSCAN),只扫描了一个索引,就返回了一条记录,而不是5万条记录。

这种情况下,我们只需要一个索引键,然后该键指向了我们需要的文档。效率比全表扫描高了很多。

如果查询的字段不是索引键?

现在我们不利用索引键来查询

exp.find( { last_name : "Acevedo" } )
{
  queryPlanner: {
    winningPlan: {
      stage: 'COLLSCAN'
    },
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 10,
    executionTimeMillis: 28,
    totalKeysExamined: 0,
    totalDocsExamined: 50474,
  }
}

如果我们不使用索引的字段来查询,是不会使用索引的。所以还是全表扫描

内嵌文档索引

现在我们插入2条数据包含内嵌文档的数据。

db.examples.insertOne( { _id : 0, subdoc : { indexedField: "value", otherField : "value" } } )
db.examples.insertOne( { _id : 1, subdoc : { indexedField : "wrongValue", otherField : "value" } } )

现在内嵌文档的indexedField加上索引

db.examples.createIndex( { "subdoc.indexedField" : 1 } )

执行查询

db.examples.explain("executionStats").find( { "subdoc.indexedField" : "value" } )
{
  queryPlanner: {
    winningPlan: {
      stage: 'FETCH',
      inputStage: {
        stage: 'IXSCAN'
      }
    }
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 1,
    executionTimeMillis: 1,
    totalKeysExamined: 1,
    totalDocsExamined: 1,
    executionStages: {
      stage: 'FETCH',
      nReturned: 1,
      inputStage: {
        stage: 'IXSCAN',
      }
    }
  }
}

注意:

永远不应该在内嵌文档的字段上建立索引,因为这样做必须查询整个内嵌文档。