合理使用索引6、查询缓存原理6.1、工作流程6.2、案例6.3、内部原理6.3.1、查询优化器如何选择最优计划6.3.2、如何保证已缓存计划的效率6.3.3、如何清理计划缓存7、强制命中7.1、使用hint方法7.2、使用IndexFilter方法8、索引正交8.1、索引正交8.2、一些限制9、使用MongoDB Compass10、优化原则10.1、在适当的时机使用索引10.2、使用前缀匹配10.3、避免低效的操作符10.4、使用覆盖索引优化10.5、高基数优先原则10.6、控制索引的数量10.7、避免设计过长的数组索引10.8、避免创建重复的索引10.9、删除无用的索引10.10、避免深度分页10.11、避免一次性返回大量结果集10.12、谨防内存排序10.13、避免大量的扫描10.14、为索引预留足够的内存6、查询缓存原理MongoDB通过查询计划query plan来描述一个查询语句的执行过程。通常情况下一个查询操作可能对应多个不同的查询计划例如对于{x100y100}这个条件既可以选择{x1}索引也可以选择{y1}索引甚至是全表扫描计划。但无论是哪一种方式都会先经过内部的评分机制进行评估最终选出一个最优的执行方案。那么查询计划的评估必然会产生一定的计算开销。如果不进行缓存数据库就无法应对高吞吐、高性能的场景。为此MongoDB提供了PlanCache用以实现查询计划的缓存能力可以避免在一定条件内对同一个查询模型进行重复性的分析、评估工作。而且如果对于某个查询已经有了确定性的选择查询优化器会直接做出选择此时缓存并不会启用。例如对于已经存在{a1}索引的集合在执行find{a100}这样的查询时很可能并不会产生查询计划缓存。6.1、工作流程查询计划缓存的具体工作流程如图所示。说明查询开始执行判断PlanCache中是否有对应的缓存。如果没有缓存则进入计划生成阶段执行下列步骤。分析查询语句与全部索引产生候选计划。评估查询计划包括对计划的并发执行、采样。选择最优的计划。将计划存入缓存。如果存在缓存则触发replanning机制评估查询性能此时有以下两种结果。查询性能不达标淘汰缓存重新生成计划。查询性能达标采纳计划。执行最终计划返回结果。查询模型查询模型query shape是对于当前查询场景的唯一性结构描述。查询优化器会首先将查询请求解析为某个查询模型根据这个模型再进行计划缓存的查询。查询模型的组成包括以下3个部分。query描述查询条件的结构该结构由条件的字段、操作符谓词以及条件的嵌套关系组成并不包含查询条件的具体值。projection描述即将返回哪些字段。sort描述排序的规则。6.2、案例为了呈现查询计划缓存我们为集合创建了两个索引db.foo.createIndex({x:1})db.foo.createIndex({y:1})接着执行如下的查询db.foo.find({x:{$lt:10},y:{$gt:54}},{x:1,z:1}).sort({z:-1})不难判断无论是使用索引{x1}还是{y1}都无法完美地匹配这次查询。因此MongoDB会为当前的查询评选出一个最优的计划并写入PlanCache。记住没有确定性选择是产生计划缓存的关键。对于update操作中的查询同样可能触发查询计划的评估MongoDB对此会一视同仁。如果你执行的是explain命令那么缓存则一定不会被使用。使用PlanQuery的listQueryShapes方法可以展示当前已经缓存的查询模型代码如下db.foo.getPlanCache().list()返回的结果是一个数组包含了刚刚的查询模型而且query、sort、projection这几个字段也正如我们预期的一样。queryHash是由查询模型计算得出的稳定哈希值该字段也会在explain的结果中出现。需要注意的是尽管query字段呈现了查询条件中具体的值但在计算模型结构时这些条件值都会被忽略。也就是说query{x{$lt10}y{$gt55}}和query{x{$lt100}y{$gt0}}在结构上仍然是相同的最终的queryHash值也是一致的。使用PlanQuery的getPlansByQuery方法可以查看针对某个查询模型所产生的计划缓存代码如下返回结果如下上述结果中的plans为一个数组其中包含了当前查询可能采取的多个计划。reason.score是指得分得分越高的计划被采纳的概率越大reason.stats则是对这个计划进行评估时的一些执行过程信息和explain命令的输出基本一致。6.3、内部原理6.3.1、查询优化器如何选择最优计划最初查询优化器需要根据现有的上下文产生一些候选计划。如果同时存在多个候选计划那么需要根据一种评分机制从这些计划中选出一个最优计划这就涉及计划的评优evaluate过程如图所示。具体的机制如下。首先让所有计划都同时执行一定量的扫描任务扫描任务在满足以下条件时停止扫描次数达到numWorks次numWorksMath.max100000.3×collection.count。返回结果达到numResults个numResultsMath.min101query.getN()query.getLimit()其中query.getN()来自getMore命令query.getLimit()则只有限制了limit条件才会出现。这两个参数只有存在时才会参与比较否则numResults默认就是101。然后为每个计划的执行情况打分计算分数的因子来自下面几点isEOF是否为true如果出现isEOE则说明扫描的指针已经到达了末尾。如果计划提前结束了则扫描会获得最大的机会。advance/workUnits%如果返回的结果数占扫描数的比例越大则代表扫描效率越高。是否存在以下低效率的阶段PROJECTIONFETCH非覆盖索引查询、SORT内存排序、AND_HASH|STAGE_AND_SORTED索引正交阶段。任意一种低效阶段的存在都会导致候选计划被扣分。最后根据所得分数进行排序得分最高的计划被评选为最优计划并写入缓存。6.3.2、如何保证已缓存计划的效率事实上对于已经缓存的计划MongoDB仍会采用一种replaining的机制来保证其高效性。在返回缓存的计划之前首先对该计划进行扫描采样这次的采样数相比之前的numWorks会扩大10倍。扫描过程中如果返回了numResults个条目或者到达了EOF则达到通过pass条件此时仍然选用此计划。如果超过采样数之后仍未达到通过条件则转为失败fail状态触发replain过程此时将重新评选计划。如果扫描过程中出错则同样会变成失败状态此时也会触发replain过程。6.3.3、如何清理计划缓存MongoDB在发生如下一些情况时可实现计划缓存的清除。执行了PlanCache.clear方法。创建、删除索引或者执行了集合的drop操作。将MongoDB进程重启。查询缓存功能属于MongoDB的内部机制官方文档并未对此提供特别详细且公开的描述。7、强制命中7.1、使用hint方法在某些极少情况下某些查询产生的最终计划可能并不是你想要的。事实上想实现一个完美的查询计划是非常困难的MongoDB在查询优化器上做了很多合理性方面的努力也提供了一些干预的手段。hint方法实现了对查询计划机制的干预在查询计划中使用hint语句可以让MongoDB忽略查询优化器的结果从而直接使用指定的索引。比如下面的做法db.test.find().hint({name:}).explain()该语句将被强制使用{name1}这个索引。如果没有指定则会使用全表扫描的方式。hint方法的参数可以传入索引的定义对象也可以是索引的名称代码如下db.test.find().hint(name_1)如果所传入的索引并不存在则将会得到错误提示。7.2、使用IndexFilter方法IndexFilter方法也可以于预查询索引代码如下db.runCommand({planCacheSetFilter:test,query:{a:3,b:4},indexes:[{b:1}]})执行planCacheSetFilter命令会在test集合中增加一个IndexFilter对象该对象将会自动关联到同时包含a字段与b字段的等值查询并引导查询优化器使用{b1}这个索引。执行planCacheListFilters命令可以查看它的定义代码如下db.runCommand({planCacheListFilters:test}){filters:[{query:{a:3,b:4},sort:{},projection:{},indexes:[{b:1}]}],ok:1}其中查询模型query shape会忽略查询条件中具体的数值因此下面的查询是适用的db.test.find({a:99,b:-1})db.test.find({a:3,b:10})对于关联了IndexFilter的查询在explain结果中的indexFilterSet字段为true代码如下在上述例子中如果使用了非等值查询如ge、lt或者排序sort、投射projection发生了变化就会导致匹配失效。IndexFilter的优先级高于hint方法如果查询优化器发现了关联的IndexFilter则一定会忽略hint语句。但是IndexFilter并不能保证查询优化器最终一定会选择对应的索引事实上优化器会将这些索引与全表扫描方式一并进行评估再抉择出最终的结果。在最坏的情况下如果我们指定了不存在的索引就会导致全表扫描。IndexFilter是内存态的如果重启了MongoDB则会自动失效。另外也可以使用planCacheClearFilters进行擦除代码如下db.runCommand({planCacheClearFilters:test})注意MongoDB的查询优化器已经足够强大无论是hint还是IndexFilter都会改变默认的查询优化行为。除非迫不得已否则应该尽量少用。8、索引正交8.1、索引正交在符合“前缀匹配”的原则下组合索引可以满足一些不同的查询模式。对于如下的索引{x:1,y:1}可以很好地满足以下查询db.test.find({x:100})db.test.find({x:100,y:9})db.test.find({x:{$gt:10},y:{$lt:3}})但是对于以下的查询却无能为力db.test.find({y:5})db.test.find().sort({y:1})索引正交是一种特殊的检索方式它会利用多个索引来满足当前的查询。如果将上面的组合索引拆为如下两个{x:1}{y:1}此时利用索引正交的特点就可以同时满足上述这些查询。另一个问题是如果查询中存在排序会是什么情形呢假定有以下索引{x:1,y:1}{z:1}{y:1}对于下面的查询仍然不适用db.test.find({z:3}).sort({y:1})但是如果稍微做一下调整就可以使用索引正交db.test.find({z:3,x:10}).sort({y:1})这里的关键点在于需要先存在一个索引能同时覆盖查询和排序字段上面的这个查询会先以{x1y1}为基础结合索引{z1}进行正交计算。如果查询使用了索引正交则其explain结果会表示为AND_SORT 或者AND_HASH类型。8.2、一些限制在索引正交的实现上MongoDB规定最多只能使用两个索引。在大多数情况下查询优化器很可能并不会选择索引正交作为最优计划。这其中的原因来自一些可变的因素。例如假设大部分的文档数据都可以通过内存获取那么查询优化器很容易会认为比起索引正交来说直接在内存中获取文档进行过滤计算的开销要小得多。9、使用MongoDB Compass对于查询分析优化除了使用explain方法还可以使用一些GUI工具。MongoDB Compass是MongoDB官方提供的GUI工具可以用于数据管理、查询优化等如图所示。MongoDB Compass分为社区版、企业版等多个不同分支不同的版本具备的功能也有些差别。总的来说MongoDB Compass主要提供了以下功能数据管理如一般的CRUD操作。表结构分析可查看各字段的取值分布需要企业版才支持。实时的服务器监控需要企业版才支持。帮助创建管道pipeline化的聚合操作。使explain结果可视化辅助查询分析优化。使用方法下载安装Compass社区版本。打开Compass选择集合并切换到Explain Plan视图。将输入框中的OPTIONS展开在FILTER处输入{x{$in[1234]}}在SORT处输入{y1}。单击“EXPLAIN”按钮可看到详细的图形化的执行计划如图所示。10、优化原则10.1、在适当的时机使用索引记住一点索引的作用在于提高查询效率。当返回结果集只是全部文档的一小部分时增加合适的索引才能获得不错的性价比。当然你可以自己设定结果集的比例例如15%20%。10.2、使用前缀匹配对于模糊匹配的查询尽量使用前缀匹配。比如搜索名称时使用/^keyword/要明显优于/keyword/。后者很可能需要对整个索引树进行遍历。10.3、避免低效的操作符nin、ne、not不利于索引命中。相反这些操作符会导致大范围扫描建议避免这些操作。除此之外where、exist也无法利用索引优化同样需要避免。10.4、使用覆盖索引优化建议只返回需要的字段同时利用覆盖索引来提升性能。10.5、高基数优先原则基数cardinality是指某个字段拥有的唯一值的数量。以用户信息为例基数高的字段为用户身份证号、手机号等基数低的字段为性别等。一个字段的基数越高越有利于快速检索。这是因为高基数的索引能迅速将检索范围圈定到一个比较小的结果集上如果基数太小则会产生许多排除性的工作。比如想要查询“1990年10月13日出生的女性”如果使用性别gender这个索引进行查找将只能将结果集范围缩小50%左右。一般来说唯一索引的数量是最高的基数等同于文档的数量建议优先为基数高的字段建立索引另外组合索引上也应尽量将基数高的字段放在前面。10.6、控制索引的数量索引并不是越多越好相反过多的索引会带来一些问题多余索引会抢占内存空间导致常用的索引被挤兑影响查询性能。写操作性能降低一次文档写入会触发多个索引的更新增加了系统I/O开销。需要谨慎地对整个文档进行保存。如果使用了SpringDataMongo这样的ORM框架开发者往往会使用save命令保存整个文档此时将会触发集合中所有索引的更新。如果能在update操作中明确指定需要更新的字段那么影响则会减少一些。也就是说只有当索引中的字段被update指定时才会触发更新。不利于查询计划优化太多的索引会增加查询计划评估的难度在极端情况下可能会产生“索引跳变”。10.7、避免设计过长的数组索引数组索引是多值的在存储时需要使用更多的空间。如果索引的数组长度特别长或者数组的增长不受控制则可能导致索引空间急剧膨胀。10.8、避免创建重复的索引组合索引具有“前缀覆盖”的特点应避免存在已经被覆盖的索引。例如db.test.createIndex({x:1,y:1,z:1})db.test.createIndex({x:1,y:1})db.test.createIndex({x:1}){x1y1z1}同时具备{x1}索引和{x1y1}索引的功能因此后面两个索引可以去掉。如果使用了哈希分片则可能会获得该分片键上的一个哈希索引。如果应用上只需要对该字段进行单个查询那么该索引已经完全可以满足。不要对分片键创建重复的索引。10.9、删除无用的索引对环境中的数据库例行检查及时删除不使用的索引。10.10、避免深度分页避免使用skip命令实现超大集合的分页该命令会消耗CPU资源当跳转到深度页面时响应会非常缓慢。10.11、避免一次性返回大量结果集对于较大的集合应避免直接使用find命令进行全表查询当返回的结果集很大时使用limit方法限制合理的返回条目数比如一次批量查询不超过1000条。这个约束还要考虑结合当前系统所能承受的性能压力一般在高并发、高实时性的场景下这个值通常要设置得更小。无论何时都应该对结果集的大小保持警惕。如果对此不加控制则很可能导致应用程序出现OOM错误。10.12、谨防内存排序对于存在排序需求的查询在缺失索引或者组合索引的排序方向不匹配的情况下会产生内存排序。内存排序需要更多的临时内存影响资源占用。此外MongoDB对于排序的内存使用有32MB的限制超过该阈值会产生失败。10.13、避免大量的扫描避免全表扫描或大范围的记录扫描通过explain结果中的totalKeysExamined、totalDocsExamined可获知当前的扫描次数。避免涉及大范围扫描的计算操作。10.14、为索引预留足够的内存为了保证性能最好保证索引能被装入内存中。当内存不足时索引需要从磁盘中加载这会导致昂贵的I/O操作。一种例外的情况是递增式的索引即索引的值呈现出递增的特点而且应用上也只关心最近产生的一部分数据。例如系统日志可以按照日期建立索引而通常我们也只关心最近几天产生的日志此时内存中则可以仅保留“被关心”的部分数据。
MongoDB 4.x——合理使用索引(二)
合理使用索引6、查询缓存原理6.1、工作流程6.2、案例6.3、内部原理6.3.1、查询优化器如何选择最优计划6.3.2、如何保证已缓存计划的效率6.3.3、如何清理计划缓存7、强制命中7.1、使用hint方法7.2、使用IndexFilter方法8、索引正交8.1、索引正交8.2、一些限制9、使用MongoDB Compass10、优化原则10.1、在适当的时机使用索引10.2、使用前缀匹配10.3、避免低效的操作符10.4、使用覆盖索引优化10.5、高基数优先原则10.6、控制索引的数量10.7、避免设计过长的数组索引10.8、避免创建重复的索引10.9、删除无用的索引10.10、避免深度分页10.11、避免一次性返回大量结果集10.12、谨防内存排序10.13、避免大量的扫描10.14、为索引预留足够的内存6、查询缓存原理MongoDB通过查询计划query plan来描述一个查询语句的执行过程。通常情况下一个查询操作可能对应多个不同的查询计划例如对于{x100y100}这个条件既可以选择{x1}索引也可以选择{y1}索引甚至是全表扫描计划。但无论是哪一种方式都会先经过内部的评分机制进行评估最终选出一个最优的执行方案。那么查询计划的评估必然会产生一定的计算开销。如果不进行缓存数据库就无法应对高吞吐、高性能的场景。为此MongoDB提供了PlanCache用以实现查询计划的缓存能力可以避免在一定条件内对同一个查询模型进行重复性的分析、评估工作。而且如果对于某个查询已经有了确定性的选择查询优化器会直接做出选择此时缓存并不会启用。例如对于已经存在{a1}索引的集合在执行find{a100}这样的查询时很可能并不会产生查询计划缓存。6.1、工作流程查询计划缓存的具体工作流程如图所示。说明查询开始执行判断PlanCache中是否有对应的缓存。如果没有缓存则进入计划生成阶段执行下列步骤。分析查询语句与全部索引产生候选计划。评估查询计划包括对计划的并发执行、采样。选择最优的计划。将计划存入缓存。如果存在缓存则触发replanning机制评估查询性能此时有以下两种结果。查询性能不达标淘汰缓存重新生成计划。查询性能达标采纳计划。执行最终计划返回结果。查询模型查询模型query shape是对于当前查询场景的唯一性结构描述。查询优化器会首先将查询请求解析为某个查询模型根据这个模型再进行计划缓存的查询。查询模型的组成包括以下3个部分。query描述查询条件的结构该结构由条件的字段、操作符谓词以及条件的嵌套关系组成并不包含查询条件的具体值。projection描述即将返回哪些字段。sort描述排序的规则。6.2、案例为了呈现查询计划缓存我们为集合创建了两个索引db.foo.createIndex({x:1})db.foo.createIndex({y:1})接着执行如下的查询db.foo.find({x:{$lt:10},y:{$gt:54}},{x:1,z:1}).sort({z:-1})不难判断无论是使用索引{x1}还是{y1}都无法完美地匹配这次查询。因此MongoDB会为当前的查询评选出一个最优的计划并写入PlanCache。记住没有确定性选择是产生计划缓存的关键。对于update操作中的查询同样可能触发查询计划的评估MongoDB对此会一视同仁。如果你执行的是explain命令那么缓存则一定不会被使用。使用PlanQuery的listQueryShapes方法可以展示当前已经缓存的查询模型代码如下db.foo.getPlanCache().list()返回的结果是一个数组包含了刚刚的查询模型而且query、sort、projection这几个字段也正如我们预期的一样。queryHash是由查询模型计算得出的稳定哈希值该字段也会在explain的结果中出现。需要注意的是尽管query字段呈现了查询条件中具体的值但在计算模型结构时这些条件值都会被忽略。也就是说query{x{$lt10}y{$gt55}}和query{x{$lt100}y{$gt0}}在结构上仍然是相同的最终的queryHash值也是一致的。使用PlanQuery的getPlansByQuery方法可以查看针对某个查询模型所产生的计划缓存代码如下返回结果如下上述结果中的plans为一个数组其中包含了当前查询可能采取的多个计划。reason.score是指得分得分越高的计划被采纳的概率越大reason.stats则是对这个计划进行评估时的一些执行过程信息和explain命令的输出基本一致。6.3、内部原理6.3.1、查询优化器如何选择最优计划最初查询优化器需要根据现有的上下文产生一些候选计划。如果同时存在多个候选计划那么需要根据一种评分机制从这些计划中选出一个最优计划这就涉及计划的评优evaluate过程如图所示。具体的机制如下。首先让所有计划都同时执行一定量的扫描任务扫描任务在满足以下条件时停止扫描次数达到numWorks次numWorksMath.max100000.3×collection.count。返回结果达到numResults个numResultsMath.min101query.getN()query.getLimit()其中query.getN()来自getMore命令query.getLimit()则只有限制了limit条件才会出现。这两个参数只有存在时才会参与比较否则numResults默认就是101。然后为每个计划的执行情况打分计算分数的因子来自下面几点isEOF是否为true如果出现isEOE则说明扫描的指针已经到达了末尾。如果计划提前结束了则扫描会获得最大的机会。advance/workUnits%如果返回的结果数占扫描数的比例越大则代表扫描效率越高。是否存在以下低效率的阶段PROJECTIONFETCH非覆盖索引查询、SORT内存排序、AND_HASH|STAGE_AND_SORTED索引正交阶段。任意一种低效阶段的存在都会导致候选计划被扣分。最后根据所得分数进行排序得分最高的计划被评选为最优计划并写入缓存。6.3.2、如何保证已缓存计划的效率事实上对于已经缓存的计划MongoDB仍会采用一种replaining的机制来保证其高效性。在返回缓存的计划之前首先对该计划进行扫描采样这次的采样数相比之前的numWorks会扩大10倍。扫描过程中如果返回了numResults个条目或者到达了EOF则达到通过pass条件此时仍然选用此计划。如果超过采样数之后仍未达到通过条件则转为失败fail状态触发replain过程此时将重新评选计划。如果扫描过程中出错则同样会变成失败状态此时也会触发replain过程。6.3.3、如何清理计划缓存MongoDB在发生如下一些情况时可实现计划缓存的清除。执行了PlanCache.clear方法。创建、删除索引或者执行了集合的drop操作。将MongoDB进程重启。查询缓存功能属于MongoDB的内部机制官方文档并未对此提供特别详细且公开的描述。7、强制命中7.1、使用hint方法在某些极少情况下某些查询产生的最终计划可能并不是你想要的。事实上想实现一个完美的查询计划是非常困难的MongoDB在查询优化器上做了很多合理性方面的努力也提供了一些干预的手段。hint方法实现了对查询计划机制的干预在查询计划中使用hint语句可以让MongoDB忽略查询优化器的结果从而直接使用指定的索引。比如下面的做法db.test.find().hint({name:}).explain()该语句将被强制使用{name1}这个索引。如果没有指定则会使用全表扫描的方式。hint方法的参数可以传入索引的定义对象也可以是索引的名称代码如下db.test.find().hint(name_1)如果所传入的索引并不存在则将会得到错误提示。7.2、使用IndexFilter方法IndexFilter方法也可以于预查询索引代码如下db.runCommand({planCacheSetFilter:test,query:{a:3,b:4},indexes:[{b:1}]})执行planCacheSetFilter命令会在test集合中增加一个IndexFilter对象该对象将会自动关联到同时包含a字段与b字段的等值查询并引导查询优化器使用{b1}这个索引。执行planCacheListFilters命令可以查看它的定义代码如下db.runCommand({planCacheListFilters:test}){filters:[{query:{a:3,b:4},sort:{},projection:{},indexes:[{b:1}]}],ok:1}其中查询模型query shape会忽略查询条件中具体的数值因此下面的查询是适用的db.test.find({a:99,b:-1})db.test.find({a:3,b:10})对于关联了IndexFilter的查询在explain结果中的indexFilterSet字段为true代码如下在上述例子中如果使用了非等值查询如ge、lt或者排序sort、投射projection发生了变化就会导致匹配失效。IndexFilter的优先级高于hint方法如果查询优化器发现了关联的IndexFilter则一定会忽略hint语句。但是IndexFilter并不能保证查询优化器最终一定会选择对应的索引事实上优化器会将这些索引与全表扫描方式一并进行评估再抉择出最终的结果。在最坏的情况下如果我们指定了不存在的索引就会导致全表扫描。IndexFilter是内存态的如果重启了MongoDB则会自动失效。另外也可以使用planCacheClearFilters进行擦除代码如下db.runCommand({planCacheClearFilters:test})注意MongoDB的查询优化器已经足够强大无论是hint还是IndexFilter都会改变默认的查询优化行为。除非迫不得已否则应该尽量少用。8、索引正交8.1、索引正交在符合“前缀匹配”的原则下组合索引可以满足一些不同的查询模式。对于如下的索引{x:1,y:1}可以很好地满足以下查询db.test.find({x:100})db.test.find({x:100,y:9})db.test.find({x:{$gt:10},y:{$lt:3}})但是对于以下的查询却无能为力db.test.find({y:5})db.test.find().sort({y:1})索引正交是一种特殊的检索方式它会利用多个索引来满足当前的查询。如果将上面的组合索引拆为如下两个{x:1}{y:1}此时利用索引正交的特点就可以同时满足上述这些查询。另一个问题是如果查询中存在排序会是什么情形呢假定有以下索引{x:1,y:1}{z:1}{y:1}对于下面的查询仍然不适用db.test.find({z:3}).sort({y:1})但是如果稍微做一下调整就可以使用索引正交db.test.find({z:3,x:10}).sort({y:1})这里的关键点在于需要先存在一个索引能同时覆盖查询和排序字段上面的这个查询会先以{x1y1}为基础结合索引{z1}进行正交计算。如果查询使用了索引正交则其explain结果会表示为AND_SORT 或者AND_HASH类型。8.2、一些限制在索引正交的实现上MongoDB规定最多只能使用两个索引。在大多数情况下查询优化器很可能并不会选择索引正交作为最优计划。这其中的原因来自一些可变的因素。例如假设大部分的文档数据都可以通过内存获取那么查询优化器很容易会认为比起索引正交来说直接在内存中获取文档进行过滤计算的开销要小得多。9、使用MongoDB Compass对于查询分析优化除了使用explain方法还可以使用一些GUI工具。MongoDB Compass是MongoDB官方提供的GUI工具可以用于数据管理、查询优化等如图所示。MongoDB Compass分为社区版、企业版等多个不同分支不同的版本具备的功能也有些差别。总的来说MongoDB Compass主要提供了以下功能数据管理如一般的CRUD操作。表结构分析可查看各字段的取值分布需要企业版才支持。实时的服务器监控需要企业版才支持。帮助创建管道pipeline化的聚合操作。使explain结果可视化辅助查询分析优化。使用方法下载安装Compass社区版本。打开Compass选择集合并切换到Explain Plan视图。将输入框中的OPTIONS展开在FILTER处输入{x{$in[1234]}}在SORT处输入{y1}。单击“EXPLAIN”按钮可看到详细的图形化的执行计划如图所示。10、优化原则10.1、在适当的时机使用索引记住一点索引的作用在于提高查询效率。当返回结果集只是全部文档的一小部分时增加合适的索引才能获得不错的性价比。当然你可以自己设定结果集的比例例如15%20%。10.2、使用前缀匹配对于模糊匹配的查询尽量使用前缀匹配。比如搜索名称时使用/^keyword/要明显优于/keyword/。后者很可能需要对整个索引树进行遍历。10.3、避免低效的操作符nin、ne、not不利于索引命中。相反这些操作符会导致大范围扫描建议避免这些操作。除此之外where、exist也无法利用索引优化同样需要避免。10.4、使用覆盖索引优化建议只返回需要的字段同时利用覆盖索引来提升性能。10.5、高基数优先原则基数cardinality是指某个字段拥有的唯一值的数量。以用户信息为例基数高的字段为用户身份证号、手机号等基数低的字段为性别等。一个字段的基数越高越有利于快速检索。这是因为高基数的索引能迅速将检索范围圈定到一个比较小的结果集上如果基数太小则会产生许多排除性的工作。比如想要查询“1990年10月13日出生的女性”如果使用性别gender这个索引进行查找将只能将结果集范围缩小50%左右。一般来说唯一索引的数量是最高的基数等同于文档的数量建议优先为基数高的字段建立索引另外组合索引上也应尽量将基数高的字段放在前面。10.6、控制索引的数量索引并不是越多越好相反过多的索引会带来一些问题多余索引会抢占内存空间导致常用的索引被挤兑影响查询性能。写操作性能降低一次文档写入会触发多个索引的更新增加了系统I/O开销。需要谨慎地对整个文档进行保存。如果使用了SpringDataMongo这样的ORM框架开发者往往会使用save命令保存整个文档此时将会触发集合中所有索引的更新。如果能在update操作中明确指定需要更新的字段那么影响则会减少一些。也就是说只有当索引中的字段被update指定时才会触发更新。不利于查询计划优化太多的索引会增加查询计划评估的难度在极端情况下可能会产生“索引跳变”。10.7、避免设计过长的数组索引数组索引是多值的在存储时需要使用更多的空间。如果索引的数组长度特别长或者数组的增长不受控制则可能导致索引空间急剧膨胀。10.8、避免创建重复的索引组合索引具有“前缀覆盖”的特点应避免存在已经被覆盖的索引。例如db.test.createIndex({x:1,y:1,z:1})db.test.createIndex({x:1,y:1})db.test.createIndex({x:1}){x1y1z1}同时具备{x1}索引和{x1y1}索引的功能因此后面两个索引可以去掉。如果使用了哈希分片则可能会获得该分片键上的一个哈希索引。如果应用上只需要对该字段进行单个查询那么该索引已经完全可以满足。不要对分片键创建重复的索引。10.9、删除无用的索引对环境中的数据库例行检查及时删除不使用的索引。10.10、避免深度分页避免使用skip命令实现超大集合的分页该命令会消耗CPU资源当跳转到深度页面时响应会非常缓慢。10.11、避免一次性返回大量结果集对于较大的集合应避免直接使用find命令进行全表查询当返回的结果集很大时使用limit方法限制合理的返回条目数比如一次批量查询不超过1000条。这个约束还要考虑结合当前系统所能承受的性能压力一般在高并发、高实时性的场景下这个值通常要设置得更小。无论何时都应该对结果集的大小保持警惕。如果对此不加控制则很可能导致应用程序出现OOM错误。10.12、谨防内存排序对于存在排序需求的查询在缺失索引或者组合索引的排序方向不匹配的情况下会产生内存排序。内存排序需要更多的临时内存影响资源占用。此外MongoDB对于排序的内存使用有32MB的限制超过该阈值会产生失败。10.13、避免大量的扫描避免全表扫描或大范围的记录扫描通过explain结果中的totalKeysExamined、totalDocsExamined可获知当前的扫描次数。避免涉及大范围扫描的计算操作。10.14、为索引预留足够的内存为了保证性能最好保证索引能被装入内存中。当内存不足时索引需要从磁盘中加载这会导致昂贵的I/O操作。一种例外的情况是递增式的索引即索引的值呈现出递增的特点而且应用上也只关心最近产生的一部分数据。例如系统日志可以按照日期建立索引而通常我们也只关心最近几天产生的日志此时内存中则可以仅保留“被关心”的部分数据。