MongoDB索引的创建之道----快速查询的利器

为什么要写这篇文章呢?原因来自于一次生产MongoDB CPU利用率100%问题的排查与解决。事情是这样的:崩溃日志的数据可视化功能上线后打开页面发现页面的数据展现很慢,数据请求需要花费很长时间;同时数据请求使用的MongoDB数据库cpu利用率100%持续报警。经过一系列查找,最终定位是MongoDB数据库慢查询导致的。在解决问题过程中发现,这些慢查询的优化基本上跟Mongodb索引有关。因此,有了这篇关于MongoDB索引使用的文章,同时借此做以总结。

一.什么是索引

索引能支持MongoDB的高效查询。如果没有索引,MongoDB查询时,为了选择、查找查询语句匹配的文档,MongoDB数据库需要执行集合扫描,即,扫描集合中的每一个文档。如果查询建立了合适的索引,MongoDB查询时,Mongodb库可以使用索引,减少扫描文档的数量。也就说,索引是通过减少查询时扫描集合文档的数量来提高查询效率的。

索引是储集合数据集的一小部分,这部存储数据有易于遍历的特性。为了易于遍历,索引是按字段的值排序存储这些字段或一组字段的值的。索引条目的排序支持有效的相等匹配和基于范围的查询操作。此外,MongoDB还可以使用索引中的排序返回排序后的结果。

下图显示了使用索引选择和排序匹配文档的查询:

从根本上说,MongoDB中的索引与其他数据库系统中的索引相似。MongoDB在集合级别定义索引,并支持MongoDB集合中文档的任何字段或子字段的索引

二、索引的管理

1.创建索引

1
db.collection.ensureIndex( keys,[,options] )
  • keys,要建立索引的参数列表。如:{KEY:1},其中key表示字段名,1表示升序排序,也可使用使用数字-1降序。
  • options,可选参数,表示建立索引的设置。可选值如下:
    • background,Boolean,在后台建立索引,以便建立索引时不阻止其他数据库活动。默认值 false。
    • unique,Boolean,创建唯一索引。默认值 false。
    • name,String,指定索引的名称。如果未指定,MongoDB会生成一个索引字段的名称和排序顺序串联。
    • dropDups,Boolean,创建唯一索引时,如果出现重复删除后续出现的相同索引,只保留第一个。
    • sparse,Boolean,对文档中不存在的字段数据不启用索引。默认值是 false。
    • v,index version,索引的版本号。
    • weights,document,索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。

注意:1.8版本之前创建索引使用createIndex(),1.8版本之后已移除该方法
2.查询索引

1
2
db.collection.getIndexes()

3.删除索引

1
2
3
4
5
//删除某条索引
db.collection.dropIndex("INDEX-NAME")
//删除所有的索引
db.collection.dropIndexes()

三、索引的创建之道

索引支持查询

1.如果所有查询都使用相同的单键,则创建一个单键索引

1
db.example.ensureIndex( { "a": 1 } )

2.为满足不同键的查询,可以创建复合索引

如果您有时只查询一个键,而有时查询该键与另一个键的组合,那么创建复合索引比创建单键索引更有效。MongoDB将为两个查询使用复合索引。

1
db.example.ensureIndex( { "a": 1, "b": 1 } )

这允许您两个选项。您可以只查询a,还可以查a和b的组合。

此外,多个字段上的一个复合索引可以支持搜索这些字段的“前缀”子集的所有查询

清单3.1 例子如下:

索引

1
{x:1,y:1,z:1}

可以支持下列索引的查询

1
2
{ x: 1 }
{ x: 1, y: 1 }

在某些情况下,前缀索引可能提供更好的查询性能:例如,如果z是一个大数组

{x: 1, y: 1, z: 1}索引还可以支持许多与以下索引相同的查询:

1
{ x: 1, z: 1 }

索引支持查询结果排序

在MongoDB中,排序操作可以通过基于索引中的排序检索文档来获得排序顺序。如果查询规划器无法从索引中获得排序顺序,它将在内存中对结果进行排序。使用索引的排序操作通常比不使用索引的操作具有更好的性能。此外,不使用索引的排序操作在使用32兆内存时会中止。

1.使用单个字段的排序

如果升序索引或降序索引位于单个字段上,则字段上的排序操作可以朝任何方向进行

清单3.2 例子如下:

例如,在字段a上为创建升序索引:

1
db.exampe.ensureIndex( { a: 1 } )

这个索引可以支持a上的升序排序:

1
db.exampe.find().sort( { a: 1 } )

该索引还可以通过反向遍历索引来支持a上的以下降序排序:

1
db.exampe.find().sort( { a: -1 } )
  1. 多个字段的排序

创建一个复合索引以支持对多个字段进行排序。

可以在索引的所有键或子集上指定排序;但是,排序键必须按照索引中出现的顺序列出。例如,索引键模式{a: 1, b: 1}可以支持{a: 1, b: 1}上的排序,但不支持{b: 1, a: 1}上的排序。

对于使用复合索引进行排序的查询 .sort()文档中所有键的指定排序方向必须与索引键模式匹配或与索引键模式相反。例如,一个索引键模式{a: 1, b: -1}可以支持{a: 1, b: -1}和{a: -1, b: 1}上的排序,但不能支持{a: -1, b: -1}或{a: 1, b: 1}上的排序。

  • 排序与索引前缀

如果排序键对应于索引键或索引前缀,MongoDB可以使用索引对查询结果进行排序。复合索引的前缀是由索引键模式开头的一个或多个键组成的子集

清单3.3 例子如下:

1
db.example.ensureIndex( { a:1, b: 1, c: 1, d: 1 } )

则上述索引的前缀为:

1
2
3
{ a: 1 }
{ a: 1, b: 1 }
{ a: 1, b: 1, c: 1 }

下面的查询和排序操作使用索引前缀对结果进行排序。这些操作不需要在内存中对结果集进行排序

例子 索引前缀
db.example.find().sort( { a: 1 } ) { a: 1 }
db.example.find().sort( { a: -1 } ) { a: 1 }
db.example.find().sort( { a: 1, b: 1 } ) { a: 1, b: 1 }
db.example.find().sort( { a: -1, b: -1 } ) { a: 1, b: 1 }
db.example.find().sort( { a: 1, b: 1, c: 1 } ) { a: 1, b: 1, c: 1 }
db.example.find( { a: { $gt: 4 } } ).sort( { a: 1, b: 1 } ) { a: 1, b: 1 }

考虑以下示例,其中索引的前缀键同时出现在查询谓词和排序中:

1
2
db.example.find( { a: { $gt: 4 } } ).sort( { a: 1, b: 1 } )

在这种情况下,MongoDB可以使用索引按照排序指定的顺序检索文档。如示例所示,查询谓词中的索引前缀可以与排序中的前缀不同。

  • 索引的排序和非前缀子集

索引可以支持索引键模式的非前缀子集上的排序操作。为此,查询必须在排序键之前的所有前缀键上包含相等条件。

清单3.4 例子如下:

1
2
//索引
{ a: 1, b: 1, c: 1, d: 1 }

以下操作可以使用索引获得排序顺序:

例子 索引前缀
db.example.find( { a: 5 } ).sort( { b: 1, c: 1 } ) { a: 1 , b: 1, c: 1 }
db.example.find( { b: 3, a: 4 } ).sort( { c: 1 } ) { a: 1, b: 1, c: 1 }
db.example.find( { a: 5, b: { $lt: 3} } ).sort( { b: 1 } ) { a: 1, b: 1 }

如最后一个操作所示,只有排序子集前面的索引字段必须具有查询文档中的相等条件;其他索引字段可以指定其他条件。

如果查询没有在排序规范前面或重叠的索引前缀上指定相等条件,则操作将无法有效地使用索引。例如,以下操作指定了{c: 1}的一类文档,但是查询文档不包含前面索引字段a和b上的相等匹配:

1
2
db.example.find( { a: { $gt: 2 } } ).sort( { c: 1 } )
db.example.find( { c: 5 } ).sort( { c: 1 } )

这些操作不能有效地使用索引{a: 1, b: 1, c: 1, d: 1},甚至不能使用索引检索文档。

三、分析MongoDB的慢请求,优化索引

MongoDB 支持 profiling 功能,将请求的执行情况记录到同DB下的 system.profile 集合里,profiling 有3种模式:

  • 关闭 profiling
  • 针对所有请求开启 profiling,将所有请求的执行都记录到 system.profile 集合
  • 针对慢请求 profiling,将超过一定阈值的请求,记录到system.profile 集合
  • 默认请求下,MongoDB 的 profiling 功能是关闭,生产环境建议开启,慢请求阈值可根据需要定制,如不确定,直接使用默认值100ms。

    关于profiling功能说明,参考文档。默认请求下,MongoDB 的 profiling 功能是关闭,生产环境建议开启,慢请求阈值可根据需要定制,如不确定,直接使用默认值100ms。

1
2
3
operationProfiling:
mode: slowOp
slowOpThresholdMs: 100

基于上述配置,MongoDB 会将超过 100ms 的请求记录到对应DB 的 system.profile 集合里,system.profile 默认是一个最多占用 1MB 空间的 capped collection。

1
2
查看最近3条 慢请求,{$natrual: -1} 代表按插入数序逆序
db.system.profile.find().sort({$natrual: -1}).limit(3)

情况1:全盘扫描

全集合(表)扫描 COLLSCAN,当一个查询(或更新、删除)请求需要全表扫描时,是非常耗CPU资源的,所以当你在 system.profile 集合 或者日志文件发现 COLLSCAN 关键字时,就得注意了,很可能就是这些查询吃掉了你的 CPU 资源;确认一下,如果这种请求比较频繁,最好是针对查询的字段建立索引来优化。

一个查询扫描了多少文档,可查看 system.profile 里的 docsExamined 的值,该值越大,请求CPU开销越大。关键字:COLLSCAN、 docsExamined。

情况2:索引未添加或不合理

一个走索引的查询,扫描了多少条索引,可查看 system.profile 里的 keysExamined 字段,该值越大,CPU 开销越大。关键字:IXSCAN、keysExamined。

对于Mongodb数据库的慢请求分析与MongoDB CPU 利用率高分可参考:

《MongoDB CPU 利用率高,怎么破?》