前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MongoDB中null性能问题以及如何应对

MongoDB中null性能问题以及如何应对

作者头像
徐靖
发布2022-12-19 19:58:43
2.5K0
发布2022-12-19 19:58:43
举报
文章被收录于专栏:DB说

【背景】

在使用ORACLE、MYSQL以及MongoDB数据库时,其中查询时经常遇到NULL的性能问题,例如Oracle的索引中不记录全是NULL的记录,MongoDB中默认索引中会记录全是null的文档,MongoDB查询等于null时,表示索引字段对应值是null同时还包括字段不存在的文档.因为MongoDB是动态模式,允许每一行的字段都不一样,例如记录1中包括包括字段A等于1,记录2包括字段A等于null,记录3不包括字段A,那么索引中不仅会包括A等于null的文档,同时也记录不包括A字段的文档,同样会赋予null值(空数组属于特殊的).正是由于这些设计规则不同,难免在使用过程中就会遇到各种性能问题.常见查询包括统计null总数以及对应明细数据.其中以汇总统计为例.

  • db.xiaoxu.count({fld4:null})
  • db.xiaoxu.count({fld4:{$ne:null})
  • db.xiaoxu.count({fld4:{$in:[1,2,null]}})

疑问:对于以上三个查询语句,大家可以先思考下能不能用上索引、如能用上索引会是什么样执行计划,例如IXSCAN+FETCH.是否有优化空间?接下来我们会重点分析这些问题点,也欢迎大家提出自己看法以及实际环境遇到相关性能问题.

备注:当前是基于4.4 版本来验证与测试.同时在集合xiaoxu上fld4字段存在正常索引{a:1},包括用到5.0以及6.0版本来解决性能问题(安装与升级不在本次中).另外索引不是multikey.

【性能问题之查询单个null值总数】

1、具体语句以及执行效率

代码语言:javascript
复制
db.xiaoxu.count()
 54528512
db.xiaoxu.count({fld4:null})
  550000
 db.xiaoxu.explain("executionStats").count({fld4:null}).
 executionStats.executionTimeMillis
 900ms

从以上查询结果来看,null占比1%,整个集合5400万,如集合总数以及null呈现N倍数量级上升.例如null到500万,预计查询时间至少在10s.

2、分析具体的执行计划

其中IXSCAN+FETCH+FILTER+COUNT,消耗时间主要消耗在FETCH+FILTER,IXSCAN预估消耗才58ms.占比不到10%.说明MongoDB中查询null能够用上索引,需要关注效率问题

关键点:回表并没有过滤掉什么记录,都是满足记录,为什么不能使用覆盖查询来进行统计.如果是索引覆盖查询,直接采用COUNT_SCAN即可.如果把null换成其他常量是否可以?

3、查询等值字符串总数

代码语言:javascript
复制
   db.xiaoxu.explain("executionStats").count({fld4:"sit"})

经过验证:

查询非空等值汇总时,执行计划走的是覆盖查询,直接COUNT_SCAN,并没有出现回表FETCH以及FILTER操作.符合预期行为.而且有114万满足条件只需要445ms.比查询55万null值还快500ms.

4、问题思考

1、查询等于null为什么不能使用覆盖查询,需进行FETCH+FILTER,对于存在少量满足null情况的过滤对性能影响小,如随着集合总数以及null呈现N倍数据量上升,此时进行FETCH+FILTER对性能影响非常大.

2、对于查询null,能否给这些字段赋予默认值,不用null,使用其他默认值来替代,避免去检查字段值等于null或者字段不存在的情况--这种虽然可行,需要提前设计就需要参考考虑进去,另外本身就是动态模式,这样限制它的灵活性.特定场景下是可以使用,例如模式是固定的.或者从关系型数据库改造到MongoDB.

5、性能优化思路

针对上述优化方案,对于第二条虽然可以,需要前期参与以及牺牲一定灵活性,所以重点考虑问题点1.

1、是否可以采用部分索引--这个跟exists:true不同,fld4:null可以直接定位到数据,已经实现与部分索引相同的作用.其中都需要进行回表进行过滤.无法实现覆盖查询.

主要性能在于回表过滤,理论上都满足覆盖查询条件,经过检索mongodb jira发现,这个是由于老的索引格式造成.从4.9版本开始,重新设计索引格式,只要索引是非multikey,

查询等于null可以使用覆盖查询.对于multikey索引,至少6.0还是不行.

2、 升级到5.0版本验证下能否实现覆盖查询

备注:升级5.0后使用覆盖查询,性能明细提升,响应时间从900ms下降到384ms.性能明显提升.第一个性能问题通过升级完美解决,至于赋予默认值方式也可以解决.

代码语言:javascript
复制
db.xiaoxu.explain("executionStats").count({fld4:null}).executionStats
{
 "executionSuccess" : true,
 "nReturned" : 0,
 "executionTimeMillis" : 384,
  "totalKeysExamined" : 550002,
 "totalDocsExamined" : 0,
 "executionStages" : {
         "stage" : "COUNT",
         "nReturned" : 0,
         "executionTimeMillisEstimate" : 59,
         "works" : 550002,
         "advanced" : 0,
         "needTime" : 550001,
         "needYield" : 0,
         "saveState" : 550,
         "restoreState" : 550,
         "isEOF" : 1,
         "nCounted" : 550000,
         "nSkipped" : 0,
         "inputStage" : {
                 "stage" : "OR",
                 "nReturned" : 550000,
                 "executionTimeMillisEstimate" : 59,
                 "works" : 550002,
                 "advanced" : 550000,
                 "needTime" : 1,
                 "needYield" : 0,
                 "saveState" : 550,
                 "restoreState" : 550,
                 "isEOF" : 1,
                 "dupsTested" : 550000,
                 "dupsDropped" : 0,
                 "inputStages" : [
                         {
           "stage" : "COUNT_SCAN",
           "nReturned" : 0,
           "executionTimeMillisEstimate" : 0,
           "works" : 1,
           "advanced" : 0,
           "needTime" : 0,
           "needYield" : 0,
           "saveState" : 550,
           "restoreState" : 550,
           "isEOF" : 1,
           "keysExamined" : 1,
           "keyPattern" : {
                   "fld4" : 1
           },
           "indexName" : "fld4_1",
           "isMultiKey" : false,
           "multiKeyPaths" : {
                   "fld4" : [ ]
           },
           "isUnique" : false,
           "isSparse" : false,
           "isPartial" : false,
           "indexVersion" : 2,
           "indexBounds" : {
                   "startKey" : {
                           "fld4" : undefined
                   },
                   "startKeyInclusive" : true,
                   "endKey" : {
                           "fld4" : undefined
                   },
                   "endKeyInclusive" : true
           }
   },
   {
           "stage" : "COUNT_SCAN",
           "nReturned" : 550000,
           "executionTimeMillisEstimate" : 19,
           "works" : 550001,
           "advanced" : 550000,
           "needTime" : 0,
           "needYield" : 0,
           "saveState" : 550,
           "restoreState" : 550,
           "isEOF" : 1,
           "keysExamined" : 550001,
           "keyPattern" : {
                   "fld4" : 1
           },
           "indexName" : "fld4_1",
           "isMultiKey" : false,
           "multiKeyPaths" : {
                   "fld4" : [ ]
           },
           "isUnique" : false,
           "isSparse" : false,
           "isPartial" : false,
           "indexVersion" : 2,
           "indexBounds" : {
                   "startKey" : {
                                  "fld4" : null
 },
"startKeyInclusive" : true,
"endKey" : {
"fld4" : null
},
"endKeyInclusive" : true

【性能问题之查询单个不等于null值总数】

1、具体SQL

代码语言:javascript
复制
db.xiaoxu.count({fld4:{$ne:null})

备注:对于查询不等于null的情况,从4.2版本就支持覆盖查询,通常情况下,不等于null数据非常大,此时查询速度无法保证.大部分场景下性能都存在瓶颈.这个例子中5300万耗时是38s.这个相对简单些,需要MongoDB 4.2版本才支持索引覆盖查询.

场景:适合不等于值少的.否则虽能使用覆盖查询,但对于大集合还是消耗时间.

【性能问题之查询组合null与其他等值总数】

1、查询语句以及问题

代码语言:javascript
复制
db.xiaoxu.count({fld4:{$in:[1,2,null]}})
550003

备注:4.4版本执行计划--没有走覆盖索引,根据第一个案例中提到这个属于正常.

问题来了

5.0版本执行计划--居然还没有走覆盖索引,根据第一个案例中提到升级5.0可以走覆盖查询,组合查询失效.

2、问题思考

1、5.0版本为什么查询单个null值或者其他非null等值组合查询时,可以使用覆盖查询,与null值组合到一起后不能使用覆盖查询?

2、5.0版本中所有值都进行回表过滤,执行计划与4.4版本单个等值null相同.5.0版本优化是对null进行拆分多个or然后合并?当null与非null组合出现,拆分成多个or场景并没有出现?--这个是我们的机会.

3、如何进行优化

如遇到上面的性能问题,5.0也无法解决,考虑如下2个思路:

1、能否继续升级到6.0版本--对于生产环境需要从多个角度进行考虑.这里只是验证能否解决性能问题.

2、如果已经是5.0版本,能否手动改写SQL来调优.搞MongoDB这么久,第一次尝试SQL改写来进行优化.

4、方案1-升级到6.0来验证

代码语言:javascript
复制
db.serverStatus().version;
6.0.0-rc8

重点:升级到6.0版本发现组合查询使用覆盖查询,查询时间是300ms.从850ms下降到300ms,提升性能明显.这个只是作为技术验证方案,是否升级需要看实际情况,如果新选型,通常建议选择新版本带来的红利.同时也要忍受一定BUG.

5、方案2--5.0中改写SQL进行优化

改写原因:

1、为什么会想到SQL改写,主要受到5.0中null优化思路影响,在5.0中把null查询拆分成2个OR,一个是查询null,一个是查询undefined,最终合并.

2、在使用ORACLE、MYSQL时由于优化器不足或者设计问题,导致在当前版本需要手动改写SQL来进行性能优化或者升级新版本来解决(升级版本已尝到甜头).

改写要点:

1、用到unionWith聚合管道,相当于关系型数据库中union all,注意不是union.unionWith是4.4版本新功能.在改写过程中遇到一个诡异事情.--主要研究这个如何改写.

2、在应用端进行拆分,然后应用端进行汇总(这里需要在应用端修改实现--这里不讨论),因为5.0中单个null已提升性能.

使用unionWith进行改写:

【原始SQL】

代码语言:javascript
复制
db.xiaoxu.count({fld4:{$in:[1,2,null]}})
550003

【改写后SQL】

代码语言:javascript
复制
db.xiaoxu.aggregate([
{$match: {fld4: null}}, 
{$group: {_id: '$fld4',total: {$sum: 1}}}, 
{
$unionWith: {coll: 'xiaoxu',
pipeline: [
{$match: {fld4: {$in: [1,2]}}},
{$group: {_id: '$fld4',total: {$sum: 1}}}]}},
{$group: {_id: null,total: {$sum: '$total'}}}])
 { "_id" : null, "total" : 550003 }

问题:发现改写后,依然存在FETCH阶段,并没有达到预期覆盖查询,问题点在哪里?

区别:前后2者区别在于FETCH+PROJECTTION_SIMPLE以及PROJECTION_COVERED.那么如果null这部分也能实现PROJECTION_COVERED,那么问题迎刃而解.问题来了:投影是怎么产生的?

[投影是如何产生]

拆解下:match+group2个管道组成,那么产生投影就是group作用.对应SQL:group:{_id:"fld4",total:{in:[1,2]可以投影覆盖,而null不能投影覆盖(有朋友知道可以告知下).--具体原因不得而知,如果按照语义来修改成group:{_id:null,total:{

按照语义来改写语句

代码语言:javascript
复制
db.xiaoxu.aggregate([
{$match: {fld4: null}}, 
{$group: {_id: null,total: {$sum: 1}}}, 
{
$unionWith: {coll: 'xiaoxu',
pipeline: [
{$match: {fld4: {$in: [1,2]}}},
{$group: {_id: null,total: {$sum: 1}}}]}},
{$group: {_id: null,total: {$sum: '$total'}}}])
[ { _id: null, total: 550003 } ]

经过改写后,执行时间从1326ms下降到432ms,性能提升70%.相比6.0版本,执行效率要稍微差一点.新版本红利.

【总结】

1、对于查询汇总单个null值总数,4.9版本开始(5.0版本)索引可以实现覆盖查询来解决FETCH+FILTER造成性能问题,表越大以及null越多效果越明显.4.9之前版本没有太好办法,只能在程序设计考虑使用默认值来替代null.

2、对于查询汇总组合null与其他等值总数,6.0版本可以完美使用索引实现覆盖查询来解决FETCH+FILTER造成性能问题,5.0版本需要使用unionWith改成或者在应用端拆分多个count来累加也可以.其实这个改写上一在MongoDB尝试过一次失败了,主要是单纯count与分组聚合count上语义理解偏差导致.这次也是偶然发现.

欢迎大家一起来交流与分享.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DB说 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 SQL Server
腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档