2

技术干货| 如何运用 MongoDB 部分索引优化性能问题?

 2 years ago
source link: https://mongoing.com/archives/82603
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

技术干货| 如何运用 MongoDB 部分索引优化性能问题?

背 景

最近研发提交业务需求,大概逻辑就是先统计总数,然后分页进行导出。SQL 查询条件很简单。根据时间范围以及 productTags 字段必须存在作为条件。目前每天大约 5000 万数据量,数据保留 6 个月满足条件数据不多。但在没有索引的情况下,前端导出是卡死的。本次只讨论count性能问题,分页导数同样需要优化。具体SQL如下:

db.xiaoxu20220704.count({ "productTags" : { "$exists" : true } ,"deliveryTime" : { "$gte" : { "$numberLong" : "1656864000000" } }, "$lt" : { "$numberLong" : "1657814400000" } } )

目前是基于 4.4 版本的分片集群。下班后创建索引语句跑起来,第二天上班创建成功,一共执行了 8 小时。通知研发可以进行验证。悲催的事情,执行 count 同样卡死。创建索引语句( 4.2 开始不区分前后台创建引),以下是分析过程。

db.xiaoxu20220704.createIndex({deliveryTime:1,productTags:1})

 

分析过程

分析执行计划

explain() 查看执行计划发现 “productTags” : { “$exists” : true } 没有用上索引,而是回表后进行过滤。

IXSCAN+FETCH 执行计划,而不是 COUNT_SCAN 执行计划。

explain(“executionStats”) 执行一个小时都没有出来,初步猜测在于 5000 万  fetch+filter 导致的慢。需要找研发了解数据情况。

db.xiaoxu20220704.explain().count({ "productTags" :{ "$exists" :
 true } ,  "deliveryTime" : { "$gte" : NumberLong("1656864000000") ,
  "$lt" : NumberLong("1657814400000")  }} )
"winningPlan" : {
"stage" : "COUNT",
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"productTags" : {
"$exists" : true
}
,
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"deliveryTime" : 1,
"productTags" : 1
},
"indexName" : "deliveryTime_1_productTags_1",
"isMultiKey" : true,
"multiKeyPaths" : {
"deliveryTime" : [ ],
"productTags" : [
"productTags"
]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"deliveryTime" : [
"[1656864000000, 1657814400000)"
],
"productTags" : [
"[MinKey, MaxKey]"
]

沟通业务逻辑

经了解,导数据通常是按天的,但也会存在按周、按月的需求,为什么会存在按周、按月,业务不想自己去合并表格,每天满足条件数据在 10万 左右,同时这个只有10万记录存在这个 productTags 字段,其他将近 5000 万都不存在这个字段。

有没有办法只把满足 “productTags” :{ “$exists” : true } 这个条件的记录索引?

如果能实现,这样查询每天的数据大约在 10 万次,此时如果 FETCH+FILTER 只有 10 万,相比之前 5000 万次,减少了 99.8% 次数。如果能实现查询覆盖,count 效率会更高。

MongoDB 中确实有这样功能,稀疏索引与部分索引都可以实现这个功能。部分索引功能是稀疏索引的超集同时提供更多的表达式,所以推荐使用部分索引。

优化索引–创建部分索引

db.xiaoxu2022070.createIndex({deliveryTime:1,productTags:1},{partialFilterExpression:{ "productTags" : { "$exists" : true }}})

partialFilterExpression: 支持如下表达式,$exists: true等价稀疏索引 (sparse:1)

  • equality expressions (i.e. field: value or using the $eq operator),
  • $exists: true expression,
  • $gt, $gte, $lt, $lte expressions,
  • $type expressions,
  • $and operator at the top-level only

查看最新执行计划

这个分片表执行计划只显示一个 shard,其他 shard 都类似,一共 8 个 shard 。总共加起来10万。执行计划本身没有改变,只是总的 totalKeysExamined 以及 totalDocsExamined 减少 99% ,所以速度很快。

为什么不能使用覆盖查询?

正常说只要统计出 deliveryTime 个数就知道知道总 count ,因为 productTags 都是满足 “$exists” : true 。

注意:

分片集合与非分片集合的查询覆盖有区别:分片集合想要使用覆盖查询必须包括分片键( readConcern 不是 avaiable 即可),使用非分片集合时,同样无法使用覆盖索引。在目前版本以及包括 5.0 版本使用 $exists:true 时都无法覆盖索引,部分索引能否使用覆盖查询,答案是肯定。

目前在不改代码逻辑的情况下,索引已经是最优了。

db.xiaoxu20220704.explain("executionStats").count({"productTags" : { "$exists" : true },  "deliveryTime" : { "$gte" : NumberLong("1656864000000") ,"$lt" : NumberLong("1656950400000")  }} )
"executionStats" : {
                "nReturned" : 0,
                "executionTimeMillis" : 3155,
                "totalKeysExamined" : 116013,
                "totalDocsExamined" : 107597,
                "executionStages" : {
                        "stage" : "SHARD_MERGE",
                        "nReturned" : 0,
                        "executionTimeMillis" : 3155,
                        "totalKeysExamined" : 116013,
                        "totalDocsExamined" : 107597,
                        "totalChildMillis" : NumberLong(4836),
                        "shards" : [
                                {
                                        "shardName" : "shard7",
                                        "executionSuccess" : true,
                                        "nReturned" : 0,
                                        "executionTimeMillis" : 113,
                                        "totalKeysExamined" : 7716,
                                        "totalDocsExamined" : 7193,
                                        "executionStages" : {
                                                "stage" : "COUNT",
                                                "nReturned" : 0,
                                                "executionTimeMillisEstimate" : 113,
                                                "works" : 7717,
                                                "advanced" : 0,
                                                "needTime" : 7716,
                                                "needYield" : 0,
                                                "saveState" : 10,
                                                "restoreState" : 10,
                                                "isEOF" : 1,
                                                "nCounted" : 7193,
                                                "nSkipped" : 0,
                                                "inputStage" : {
                                                        "stage" : "SHARDING_FILTER",
                                                        "nReturned" : 7193,
                                                        "executionTimeMillisEstimate" : 113,
                                                        "works" : 7717,
               "advanced" : 7193,
               "needTime" : 523,
                "needYield" : 0,
                  "saveState" : 10,
                   "restoreState" : 10,
                    "isEOF" : 1,
                  "chunkSkips" : 0,
                 "inputStage" : {
                           "stage" : "FETCH",
                           "filter" : {
                                   "productTags" : {
                                           "$exists" : true
                                   }
                           },
                           "nReturned" : 7193,
                           "executionTimeMillisEstimate" : 86,
                           "works" : 7717,
                           "advanced" : 7193,
                           "needTime" : 523,
                           "needYield" : 0,
                           "saveState" : 10,
                           "restoreState" : 10,
                           "isEOF" : 1,
                           "docsExamined" : 7193,
                           "alreadyHasObj" : 0,
                           "inputStage" : {
                           "stage" : "IXSCAN",
                           "nReturned" : 7193,
                           "executionTimeMillisEstimate" : 15,
                           "works" : 7717,
                           "advanced" : 7193,
                           "needTime" : 523,
                           "needYield" : 0,
                           "saveState" : 10,
                           "restoreState" : 10,
                           "isEOF" : 1,
                           "keyPattern" : {
                                   "deliveryTime" : 1,
                                   "productTags" : 1
                           },
                           "indexName" : "deliveryTime_1_productTags_1",
                           "isMultiKey" : true,
                           "multiKeyPaths" : {
                                   "deliveryTime" : [ ],
                                   "productTags" : [
                                           "productTags"
                                   ]
                           },
                           "isUnique" : false,
                           "isSparse" : false,
                           "isPartial" : true,
                           "indexVersion" : 2,
                           "direction" : "forward",
                           "indexBounds" : {
                                   "deliveryTime" : [
                                           "[1656864000000, 1656950400000)"
                                   ],
                                   "productTags" : [
                                           "[MinKey, MaxKey]"
                                   ]
                           },
                           "keysExamined" : 7716,
                           "seeks" : 1,
                           "dupsTested" : 7716,
                           "dupsDropped" : 523
                                                               }
                                                        }
                                                }
                                        }
                                }

 

部分索引知识

部分索引特点与优势

部分索引只是对满足过滤表达式的记录进行索引,而不是所有记录,所以才称为部分索引。

部分索引可以减少索引大小,加快查询效率以减少磁盘空间,同时部分索引不是针对所有查询都生效。查询条件必须包括过滤表达式。优化器会自动判断是否使用部分索引,对于排序或者查询可能会导致数据不全的情况,优化器会拒绝使用。

partialFilterExpression 支持类型

  • equality expressions (i.e. field: value or using the $eq operator),
  • $exists: true expression,
  • $gt, $gte, $lt, $lte expressions,
  • $type expressions,
  • $and operator at the top-level only

部分索引与稀疏索引

  • 部分索引能够更好控制那些记录被索引,稀疏根据索引字段是否存在来索引,而部分索引支持很多种表达式。
  • 部分索引相当于稀疏索引的超集功能。例部分索引的 $exists:true 等价稀疏索引,但也存在区别,部分索引的过滤表达式可以是索引定义也可以不是索引定义(只是用来过滤记录),稀疏索引则都属于索引的定义。这个部分索引如何定义会影响查询覆盖。这个问题与我遇到问题的很接近,接下来我们围绕这个来分析下。

部分索引与查询覆盖

在文章开头提到遇到的案例中查询条件是 $exists:true 作为查询条件,经过优化后创建过滤条件为 $exists:true 的部分索引,解决 count 性能问题,但如果过滤的记录增加 N 个数量级,还是会存在性能问题。导致性能问题是完全满足查询覆盖,但优化器却没有使用。而是回表进行过滤,相比在索引是过滤效率高(查询覆盖),如果是需要回表返回完整记录,那么不存在效率问题。因为索引中记录都是满足条件的直接回表过滤也都是满足条件的。

经过验证目前存在 $exists:true 查询时,不管是部分索引还是普通索引,都无法使用查询覆盖(截止目前最新 5.0 版本都还没有解决,期待未来版本能够优化这个问题),对于部分索引中过滤为 $exists:true 时,满足覆盖查询时,使用具体值而不是 $exists:true 时可以使用查询覆盖。

分片集合支持查询覆盖,相比非分片集合,索引中需要带分片键。如果开启读写分离时,读备库 readConcern 默认是 avaiable ,此时与非分片集合一样,不需要包括分片键就可以查询覆盖。此时导致读取孤儿文档。

需要注意此时可以调整 readConcern 为 local 。

 

案 例

构造数据

mongos> db.xiaoxu20220718.find();

{ "_id" : ObjectId("62d4fbf69dadbc915955a94b"), "name" : "xiaoxu", 
"age" : 18, "addr" : "shanghai" }
{ "_id" : ObjectId("62d4fbf69dadbc915955a94c"), "name" : "xiaojing", 
"age" : 20, "addr" : "beijing" }
{ "_id" : ObjectId("62d4fbf69dadbc915955a94d"), "name" : "xiaobao", 
"age" : 1 }
{ "_id" : ObjectId("62d4fbf69dadbc915955a94e"), "name" : "xiaoxing", 
"age" : 18 }

创建部分索引:索引定义不包括过滤字段

db.xiaoxu20220718.createIndex({name:1},{partialFilterExpression:{addr:{$exists:true}}})

查询 count 总数

条件中带 $exists:true

执行计划:IXSCAN+FETCH+COUNT ,而不是我们期望 COUNT_SCAN

db.xiaoxu20220718.explain("executionStats").count({"name" : "xiaoxu",
"addr":{$exists:true}})
"winningPlan" : {
"stage" : "COUNT",
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"addr" : {
"$exists" : true
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : 1
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : true,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"xiaoxu\", \"xiaoxu\"]"

条件中字段使用实际值而非 $exists:true

执行计划:IXSCAN+FETCH+COUNT,而不是我们期望 COUNT_SCAN

db.xiaoxu20220718.explain("executionStats").count({"name" : "xiaoxu",
"addr":"shanghai"})
"winningPlan" : {
"stage" : "COUNT",
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"addr" : {
"$eq" : "shanghai"
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : 1
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : true,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"xiaoxu\", \"xiaoxu\"]"
]
}
}
}
},
"rejectedPlans" : [ ]
}
]

创建部分索引定义中包括过滤字段

db.xiaoxu20220718.createIndex({name:1,addr:1},{partialFilterExpression:{addr:{$exists:true}}})

查询 count 总数

条件中带 $exists:true

执行计划:IXSCAN+FETCH+COUNT,而不是我们期望COUNT_SCAN,还是选择单列索引

db.xiaoxu20220718.explain("executionStats").count({"name" : "xiaoxu",
"addr":{$exists:true}})
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
"addr" : {
"$exists" : true
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : 1
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : true,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"xiaoxu\", \"xiaoxu\"]"
]
}
}
},
"rejectedPlans" : [
{
"stage" : "FETCH",
"filter" : {
"addr" : {
"$exists" : true
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : 1,
"addr" : 1
},
"indexName" : "name_1_addr_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ],
"addr" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : true,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"xiaoxu\", \"xiaoxu\"]"
],
"addr" : [
"[MinKey, MaxKey]"
]

条件中字段使用实际值而非$exists:true

执行计划:符合我们期望 COUNT_SCAN

mongos> db.xiaoxu20220718.explain("executionStats").count({"name" : 
"xiaoxu","addr":"shanghai"})
"winningPlan" : {
"stage" : "COUNT",
"inputStage" : {
"stage" : "COUNT_SCAN",
"keyPattern" : {
"name" : 1,
"addr" : 1
},
"indexName" : "name_1_addr_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ],
"addr" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : true,
"indexVersion" : 2,
"indexBounds" : {
"startKey" : {
"name" : "xiaoxu",
"addr" : "shanghai"
},
"startKeyInclusive" : true,
"endKey" : {
"name" : "xiaoxu",
"addr" : "shanghai"
},
"endKeyInclusive" : true
}
}
},
"rejectedPlans" : [ ]
}
]

 

总 结

  • 本次通过部分索引来进行性能优化,同时对部分索引知识简单介绍,需要注意点是查询覆盖在 $exists:true 条件无法生效,期待后续版本改进这个点。
  • 注意部分索引只适合特定场景以及查询覆盖注意事项。

关于作者:徐靖

MongoDB 中文社区成员,数据库工程师,具有丰富的数据库运维经验,精通数据库性能优化及故障诊断,目前专注于 MongoDB 数据库运维与技术支持,同时也是公众号《 DB 说》维护者,喜欢研究与分享数据库相关技术。希望能够为社区贡献一份力量。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK