精通MongoDB-索引与查询优化

     索引是非常重要的东西,有了正确的索引,MongoDB才能高效地使用硬件,为应用程序提供快速的查询。错误的索引则会导致相反的结果:慢查询、无法充分利用硬件。显而易见,想要高效使用MongoDB的人都要理解索引。MongoDB索引的基础-B树数据结构。在下面的实践中,我们会讨论唯一性索引、稀疏索引和多键索引,为索引管理做些说明,随后,我们会深入研究查询优化,描述如何使用explain()和查询优化器。

1. 索引理论

     用户通常认为一个查询里要查询两个字段,可以针对它们分离索引。有一个现成的算法:查找每个索引里匹配项的页码,针对同时匹配两个索引的食谱扫描它们页码的并集。会有不少匹配不上的页码,但还是能减少扫描的总数。一些数据库实现了这个算法,但是MonggoDB中没有。就算它实现了,使用复合索引来查找两个字段总是会比刚才描述的算法更高效。记住,每个查询中数据库只会使用一个索引,如果要对多个字段进行查询,请确保有这些字段的复合索引。

    使用多个键的索引成为复合索引(compound index).索引能显著减少获取文档所需的工作量。没有合适的索引,实现查询的唯一途径就是线性扫描整个文档,知道满足查询条件为止,这通常是扫描整个集合。解析查询时只会使用一个单键索引。对于包含多个键的查询,包含这些键的复合索引能更好地解析查询。如果有ingredient-cuisine(食材-菜肴)索引,可以去掉ingredient索引。也就是说,如果有一个a-b复合索引,就可以去除a的单键索引。复合索引里键的顺序是很重要的。

1.1 核心索引概念

1.1.1 单键索引:单键索引的每一项都对应了被索引文档里的一个值。默认的_id索引就是一个很好的例子,由于这个字段上有索引,可以根据它快速地获取文档。

1.1.2 复合键索引:到目前为止,MongoDB中每个查询就使用一个索引。但是你经常需要对多个属性进行查询,希望这些查询都能高效一点。复合索引就是每一项都由多个键组合而成的索引。结合使用索引和查询时,有两件事要注意。第一,在索引里键的顺序很重要。第二就是明白为什么选择这样的顺序。一般来说,一个查询里有一项要精确匹配,另一项指定了一个范围。在使用复合索引时,范围匹配的那个键放在第二个位置上。

1.1.3 索引效率:索引对良好的查询性能涞水是必不可少的,但每个新索引都会带来一些小的维护成本。其原因是显而易见的,每当向集合添加文档时,都必须修改集合的所有索引,以加入新的文档。因此,如果一个集合上有10个索引,每次插入的时候都要做10次独立的结构修改。对于所有的写操作都是如此,无论是删除文档还是更新指定文档的索引键。

       对于读密集型应用而言,索引的成本一般都是合理的,你只要认识到索引还是会引入一定开销,必须谨慎选择即可。这意味着所有索引都被用到,没有一个索引时多余的。此处还有一个问题需要考虑:就算拥有正确的索引,但是还是有可能得不到快速的查询,索引和数据集无法全部放入内存时就会发生这种情况。

     MongoDB使用map()系统调用告诉操作系统将所有数据文件映射到内存里。基于这点,操作系统会按照名为页(page)的4KB块将数据文件换入换出内存,包含所有文档、集合和索引。在请求指定页的数据时,操作系统必须保证该页在内存里。如果不在,会抛出页错误异常,告诉内存管理器从磁盘上把页加载到内存里。有了充足的内存,所有的使用中的数据文件最终都会被加载到内存里。当那块内存发生变化时,比如执行写操作是,那些改变会被操作系统异步地刷到磁盘上,而写操作很快,是直接发生在内存里的。数据完全装入内存时最为理想的状态。因为磁盘访问的数量会降到最低程度。但如果使用中的数据集无法全部装入内存,就会出现页错误,也就是说操作系统会频繁访问磁盘,大大减缓读写操作。最坏的情况下,数据大小远远大于可用内存,这时任何读写操作的数据都必须到磁盘上做页交换。这种情况称为颠簸(thrashing),会导致性能严重下滑。

    还好这种情况相对容易避免,最起码要保证索引都能放入内存;对于为何避免创建无用索引如此重要,这就是原因之一。拥有额外的索引,就会要求更多的内存来维护那些索引。同样道理,每个索引应该只包含它需要的键:有时会用到三键复合索引,但请注意它要比简单的单键索引占用更多的空间。

    理想情况下,索引和使用中的数据集都能放入内存。但评估部署时需要多少内存并非易事,你可以通过查看stats命令的结果来了解总的索引大小。但要找打工作集(working set)大小却没有这么容易,因为每个应用程序都不一样。工作集通常是查询与更新的全部数据的子集。

1.2 B树

    MongoDB内部使用B树来表示索引。B树无处不在,至少从20世纪70年代后期就流行于数据库记录和索引中。如果你使用过其他数据库系统,那么可能已经熟知各种B树的情况。B树有两个最显著的特点,并因此成为了数据库索引的理想选择。第一,它们能用于多种查询,包括精确匹配、范围条件、排序、前缀匹配和仅用索引的查询。第二,在添加和删除键的时候,它们仍能保持平衡。

2. 索引实战

2.1 索引类型

    MongoDB中所有索引底层都使用相同的数据结构,但可以有很多不同的属性。尤其是唯一性索引、稀疏索引和多键索引,它们都很常用。

2.1.1 唯一性索引

    要创建唯一性索引,设置unique选项即可:

db.user.ensureIndex({username:1},{unique:true})
唯一性索引保证了集合中所有索引项的唯一性。如果要向本书示例应用程序的用户集合users插入一个文档,其中的用户名已经被索引过了,那么插入失败,抛出如下异常:

E11000 duplicate key error index:

  gardening.users.$username_1 dup key:{:"kbanker"} 

   如果使用驱动,那么只有在使用驱动的安全模式执行插入时才能捕获该异常。如果集合上需要唯一性索引,通常在插入数据前先创建索引比较好。提前创建索引,能在一开始就保证唯一性约束。在已经包含数据的集合上创建唯一性索引时,会有失败的风险,因为集合里可能已经存在重复的键了。存在重复键时,创建索引会失败。

   如果真有需要在一个已经建好的集合上创建唯一性索引,你有几个选择。首先是不停地重复创建唯一性索引,根据失败消息手工删除包含重复键的文档。如果数据不需要,还可以通过dropDups选项告诉数据库自动删除包含重复键的文档。举例来说,如果用户集合users里已经有数据了,而且你并不介意删除包含重复键的文档,可以像下面这样发起索引创建命令。

db.users.ensureIndex({username:1},{unique:true,dropDups:true})
请注意,要保留哪个重复键的文档时不确定的,因此在使用时要特别小心。

2.1.2 稀疏索引

     索引默认都是密集型的。也就是说,在一个有索引的集合里,每个文档都会有对应的索引项,哪怕文档中没有被索引键也是如此。例如,回想一下电子商务数据模型里的产品集合,假设你在产品属性category_ids上构建一个索引。现在假设这些产品没有分配给任何分类,对于每个无分类的产品,category_ids索引仍然会存在像这样的一个null项。可以这样查询null值:

db.products.find({category_ids:null})
在查询缺少分类的所有产品时,查询优化器仍然能使用category_ids上的索引定位对应产品。但是有两种情况使用密集型索引会不太方便。 一种是希望在并非出现在集合所有文档内的字段上增加唯一性索引。举例来说,你明确希望在每个产品的sku字段上增加唯一性索引。但是出于某些原因假设产品在还未分配sku时就加入系统了。如果sku字段上有唯一性索引,而你希望插入多个没有sku的产品,那么第一次插入会成功,但后续插入都会失败,因为索引里已经存在一个sku为null的项了。这种情况下密集型索引并不适合,需要使用稀疏索引(sparse index)

     在稀疏索引里,只会出现被索引键有值的文档。如果想创建稀疏索引,指定sparse:true就可以了。例如,可以像下面这样在sku上创建一个唯一性稀疏索引。

db.products.ensureIndex({sku:1},{unique:true,sparse:true})
     另一种情况适用稀疏索引的情况:集合中大量文档都不包含被索引键。例如,假设允许对电子商务网站进行匿名评论。这种情况下,半数评论都可能缺少user_id字段,假如那个字段上有索引,那么该索引中一般的项都会是null。出于两个原因,这种情况的效率会很差。第一,这会增加索引的大小。第二,在添加或删除带null值的user_id字段的文档时也要求更新索引。创建索引,也可以使用createIndex指令。

      如果很少(或不会)对匿名评论进行查询,那么可以选择在user_id上构建一个稀疏索引。这是sparse选项同非常简单:

db.reviews.ensureIndex({user_id:1},{saprse:true})
现在就只有那些通过user_id字段关联了用户的评论才会被索引。

2.1.3 多键索引

       在之前的几章中,我们遇到过好多索引字段的值是数组的例子。这是多键索引(multikey index)的东西让这些成为可能,它允许索引中多个条目指向相同的文档。举例说明,假设一个产品文档,包含几个标签

{
name:"Wheelbarrow",
tags:["tools","gardening","soil"]
}
如果在tags上创建索引,标签数组里的每个值都会出现在索引里。也就是说,对数组中任意值的查询都能用索引来定位文档。多键索引背后的理念是这样的:多个索引或键最终指向同一个文档。MongoDB中的多键索引总是处于激活状态。被索引字段只要包含数组,每个数组值都会在索引里有自己的位置。合理使用索引是正确设置MongoDB Schema时必不可少的一环。

2.2 索引管理

     在MongoDB中管理索引,现有的知识还显得稍有不足。本节我们简化介绍索引的创建和删除,并讨论与压缩(compaction)和备份相关的问题。

2.2.1 索引的创建与删除

     查询索引说明,使用db.user.getIndexSpec()方法

    创建索引,使用ensureIndex或者createIndex两种方法;

    要删除索引,可以使用数据库命令deleteIndexes删除索引,和创建索引一样,删除索引也有辅助方法可用,如果希望直接运行该方法,也没有问题。该命令接受一个文档作为参数,其中包含集合名称。要删除的索引名称或者用*来删除所有索引。要手工删除索引。

db.runCommand({deleteIndexes:"user",index:"email_1"})
同样也可以使用dropIndex()方法删除索引。请注意,必须提供上述定义的索引名称

db.user.dropIndex("email_1")
2.2.2 索引的构建

     大多数时候,你会在把应用程序正式投入使用之前添加索引,这允许随着数据的插入增量地构建索引。但在两种情况下,你可能会选择相反的过程。第一种情况是在切换到生产环境之前需要导入大量数据。举例来说,你想将应用程序迁移到MongoDB,需要从数据仓库导入用户信息。你可以事先在用户数据上创建索引,但在数据导入之后再创建索引能从一开始就保证理想的平衡性和密集的索引,也能将构建索引的净时间降到最低。

     第二种情况发生在为新查询进行优化的时候。无论为什么要创建新索引,这个过程都很难让人愉快起来。对于大数据集,构建索引可能要花好几个小时,甚至好几天,但你可以从MongoDB的日志里监控索引的构建过程,来看一个例子,先声明要构建的索引:

db.values.ensureIndex({open:1,close:1})
索引的构建分为两步。第一步,对要索引的值排序,经过排序的数据集在插入到B树时会更高效。注意,排序的进度会以已排序文档数和总文档数的比率来进行展示:

第二步,排序后的值被插入索引中,进度显示方式与第一步相同,完成之后,完成索引构建所用的时间会显示出来,作为最终耗时。
除了查询MongoDB的日志,还可以通过db.currentOp()方法检查构建索引的进度。msg字段描述了构建进度,还要注意lockType,它说明索引构建用了写锁,也就是说其他客户端此时无法读写数据库。如发生在生产环境,这无疑是很糟糕的,这也是长时间索引构建让人抓狂的原因。我们接下来会看到两个可行的解决方案。

后台索引:如果是生产环境,经不住这样暂停数据库访问的情况,可以指定在后台构建索引。虽然索引仍然会占用写锁,但构建任务会停下来允许其他写操作访问数据库。如果应用程序大量使用MongoDB,后台索引会降低性能,但在某些情况下这是可接受的。构建后台索引,申明索引时指定background:true

db.values.ensureIndex({open:1,close:1},{background:true})
离线索引:如果生产数据集太大,无法在几个小时内完成索引,这是就要其他方案了。通常这会涉及让一个副本节点下线,在该节点上构建索引,随后让其上的数据与主节点同步。一旦完成数据同步,将该节点提升为主节点,再让另一个从节点下线,构建它自己的索引。该策略假设你的肤质oplog足够大,能避免离线节点的数据在索引构建过程中变得过旧。

2.2.3 备份

       因为索引很难构建,所以你可能会希望为他们备份,可惜并非所有备份方法都包含索引。举例来说,如果你想用mongodump和mongorestore,但这些工具仅保存了集合和索引声明。也就是说,当运行mongorestore时,所备份的所有集合上声明的索引都会被重新创建一遍。如果数据集很大,那么构建索引所消耗的时间也是无法忍受的。因此,如果想要在备份中包含索引,需要直接备份MongoDB的数据文件。

2.2.4 压紧

     如果应用程序会大量更新现有数据,或者执行很多大规模删除,其结果就是索引的碎片化程度很高。虽说B树会自己合并,但这并非总能抵消大量删除的影响。碎片过多的索引大小远超你对指定数据集大小的预期,也会让索引使用更多内存。这些情况下,你可能希望重建一个或多个索引:可以删除并重新创建单个索引,或者运行reIndex命令(它会重建指定集合上的所有索引):

db.values.reIndex()
在重建索引时要小心:在重建过程中该命令会占用写锁,让你的MongoDB实例暂时无法使用。重建最好是在线下完成,就像之前提到的节点上构建索引一样。

2.2.5 获取集合中的索引

db.values.getIndexes().用于查询values集合中当前存在的索引。




     

你可能感兴趣的:(MongDB)