写点什么

MongoDB find getmore 操作慢问题排查

  • 2022 年 1 月 12 日
  • 本文字数:10817 字

    阅读完需:约 35 分钟

MongoDB find getmore操作慢问题排查

本文来自获得《2021MongoDB 技术实践与应用案例征集活动》入围案例奖作品

作者:张家侨

 

问题描述

本文介绍一次帮助业务排查线上查询操作慢的问题的详细过程以及解决方法。

业务在使用 find().limit()命令查询数据时遇到卡顿,一次操作卡最多 1 分钟,问题描述如下:

db.xxx_collection.find({timetag: { $gt: 1636513802167 }, $or: [ { nto: "zhang" }, { nfrom:"zhang" } ], nid: 50, status: 0 }).sort({ timetag: 1 }).limit(200)业务反馈:出问题的 MongoDB 版本为 4.2.12。这个查询从慢日志来看要四五十秒,但是直接跑的话第一批返回数据很快,getmore 的时候就慢了,你看下为啥会慢在 getmore

从问题描述来看,直观上是 getmore 造成了查询卡顿,可能原因有如下:

  • getmore 操作内核出现卡顿 -- 内核问题可能性较低

  • 查询计划与被查询数据不匹配 -- 可能性较高

下面问题排查将从这两个主要问题点入手。


问题重现

将数据恢复到一个备用集群,然后使用业务的查询进行操作:

db.xxx_collection.find({timetag: { $gt: 1636513802167 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).limit(200)

第一次查询立刻返回,当获取数据条数到 102 条时,出现卡顿。操作产生慢日志,发现是触发了一次 getmore 操作。日志输出如下:

2021-12-15T10:21:55.007+0800 I COMMAND [conn857] command xxx_db.xxx_collection appName: "MongoDB Shell" command: getMore { getMore: 1244654399487918694, collection: "xxx_collection", ****** planSummary: IXSCAN { timetag: 1 } cursorid:1244654399487918694 keysExamined:11338296 docsExamined:11338296cursorExhausted:1 numYields:88583 nreturned:99 reslen:100170 locks:{ReplicationStateTransition: { acquireCount: { w: 88584 } }, Global: { acquireCount: { r: 88584 } }, Database: { acquireCount: { r: 88584 } }, Collection: { acquireCount: { r: 88584 } }, Mutex: { acquireCount: { r: 1 } } } storage:{ data: { bytesRead: 15442700982, timeReadingMicros: 40865619 },timeWaitingMicros: { cache: 33773 } }protocol:op_msg 65270ms


问题排查

确认是否是 getmore 的问题

在 MongoDB 中,其查询返回结果批大小默认为 101。也就是说,MongoDB 一次查询会找到 101 个结果然后一次性返回,当需要更多结果时再查找 101 个结果,通过 getmore 返回。

我们是可以通过 batchSize 操作来设置 MongoDB 一次返回的数据个数的,包括 getmore 操作。

如果是 getmore 的问题,理论上调整其 batchSize 并不会影响问题的出现,所以我们进行了如下调整。

将 batchSize 设置为 150:

db.xxx_collection.find({timetag: { $gt: 1636513802167 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).batchSize(150).limit(200)

第一次查询立刻返回结果,超过 batchSize 数据后,getmore 又卡顿,问题依旧:

2021-12-15T10:25:54.781+0800 I COMMAND [conn859] command xxx_db.xxx_collection appName: "MongoDB Shell" command: getMore { getMore: 8826588127480667691, collection: "xxx_collection", batchSize: 150, ******planSummary: IXSCAN { timetag: 1 }cursorid:8826588127480667691 keysExamined:11338234 docsExamined:11338234cursorExhausted:1 numYields:88582 nreturned:50 reslen:50818 locks:{ReplicationStateTransition: { acquireCount: { w: 88583 } }, Global: { acquireCount: { r: 88583 } }, Database: { acquireCount: { r: 88583 } }, Collection: { acquireCount: { r: 88583 } }, Mutex: { acquireCount: { r: 1 } } } storage:{ data: { bytesRead: 16610295032, timeReadingMicros: 30201139 },timeWaitingMicros: { cache: 17084 } }protocol:op_msg 53826ms

调整为 199 后效果也类似,但是调整为 200 后,问题变为第一次查询就出现卡顿:

db.xxx_collection.find({timetag: { $gt: 1636513802167 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).batchSize(200).limit(200)

相应慢日志如下:

2021-12-15T10:27:23.729+0800 I COMMAND [conn859] command xxx_db.xxx_collection appName: "MongoDB Shell" command: find { find: "xxx_collection", filter: { timetag: { $gt: 1636513802167.0 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50.0, status: 0.0 }, projection: {$sortKey: { $meta: "sortKey" } }, sort: { timetag: 1.0 }, limit: 200, batchSize: 200, ******planSummary: IXSCAN { timetag: 1 }keysExamined:11338445 docsExamined:11338445 cursorExhausted:1 numYields:88582nreturned:200 queryHash:ECA82717 planCacheKey:AC7EC9E3 reslen:202045 locks:{ReplicationStateTransition: { acquireCount: { w: 88583 } }, Global: { acquireCount: { r: 88583 } }, Database: { acquireCount: { r: 88583 } }, Collection: { acquireCount: { r: 88583 } }, Mutex: { acquireCount: { r: 2 } } } storage:{ data: { bytesRead: 17688667726, timeReadingMicros: 14907251 },timeWaitingMicros: { cache: 11784 } }protocol:op_msg 36654ms

所以我们可以基本排除是 getmore 操作本身的问题。从慢操作日志我们可以看出,查询计划使用 timetag 索引来进行数据获取和过滤,一共遍历了 1 千万的数据。问题应该和数据查询计划以及数据分布有关,具体在查询进行第 199~200 个结果获取时发生了卡顿,且遍历 timetag 索引不够快。

所以我们的分析方向转为查询计划以及查询的数据上,确认是否存在问题,即查询计划不适合目标数据。


分析查询的数据分布

首先,我们需要了解业务数据的分布格式以及查询的目的,业务数据关键字段如下:

{ "nto" : , "nfrom" : , "nid" : , "timetag" : , "status" : }

从库表名以及数据格式来看,查询数据为某种消息的传递记录。

目标查询如下:

db.xxx_collection.find({timetag: { $gt: 1636513802167 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).limit(200)

而查询所要做的就是要找到某人在某个时间点后,接收到或者发送的消息记录,且 nid = 50 & status = 0。所以整个查询比较重要的条件就是时间 timetag 以及用户名。而大于某个时间点这种查询可能存在目标数据量特别大的情形,下面进行确认。


数据在 timetag 上的分布

mongos>db.xxx_collection.count()538058824

由上我们可以看出,整个目标集合数据量为 5 亿多,数据量较大。下面针对性取出造成卡顿的第 199 个和第 200 个查询结果的 timetag。

第 199 个结果 timetag:

{ "_id" : ObjectId("618b380a82010a586b4265e6"), "timetag" : NumberLong("1636513802759") }

第 200 个结果 timetag:

{ "_id" : ObjectId("618ce26682010a586be027f9"), "timetag" : NumberLong("1636622950801") }

以查询条件的时间点 1636513802167 为 T0,第 199 个结果的时间点 1636513802759 为 T1,第 200 个结果的时间点 1636622950801 为 T2。下面以这三个时间为节点,对数据进行分段分析。


查询数据的分布

T0 后总的数据量:

mongos>db.xxx_collection.find({ timetag: { $gt: 1636513802167 }}).count()191829505

处于目标时间段的数据总量为 1.9 亿多,数据量比较大

T0~T1 的数据量:

mongos>db.xxx_collection.find({ $and: [{timetag: {$gt: 1636513802167}}, {timetag: {$lte: 1636513802759}}]}).count()287

T1~T2 之间的数据量:

mongos>db.xxx_collection.find({ $and: [{timetag: {$gt: 1636513802759}}, {timetag: {$lt: 1636622950801}}]}).count()11338157

T2 后的数据量:

mongos>db.xxx_collection.find({timetag: {$gte: 1636622950801}}).count()180491061


查询结果的分布

总查询结果:

mongos>db.xxx_collection.find({ timetag: { $gt: 1636513802167 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).count()428

T0~T1:

mongos>db.xxx_collection.find({ $and:[{timetag: { $gt: 1636513802167 }}, {timetag: {$lte: 1636513802759}}], $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).count()199

T1~T2:

mongos>db.xxx_collection.find({ $and:[{timetag: { $gt: 1636513802759 }}, {timetag: {$lt: 1636622950801}}], $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).count()0

T2 后:

mongos>db.xxx_collection.find({ timetag: { $gte: 1636622950801 }, $or: [ { nto:"zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0}).sort({ timetag: 1 }).count()229

从数据以及相应结果数量的分布可以看出,查询结果主要分布在 T0~T1 和 T2 后的时间段内。T1~T2 时间段内不存在符合条件的结果,有 1 千万不符合条件的数据存在。总结来说,结果分布在 timetag 字段上呈现两头分布,中间存在巨大空洞。


分析执行的查询计划

原查询计划

db.xxx_collection.find({timetag: { $gt: 1636513802167 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).limit(200).explain("executionStats")

得到的查询计划:

"parsedQuery" : {"$and" : [{"$or" : [{"nfrom" : {"$eq" : "zhang"}},{"nto" : {"$eq" : "zhang"}}]},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}},{"timetag" : {"$gt" : 1636513802167}}]},"winningPlan" : {"stage" : "LIMIT","limitAmount" : 200,"inputStage" : {"stage" : "FETCH","filter" : {"$and" : [{"$or" : [{"nfrom" : {"$eq" : "zhang"}},{"nto" : {"$eq" : "zhang"}}]},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}}]},"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"timetag" : 1},"indexName" : "timetag_1","isMultiKey" : false,"multiKeyPaths" : {"timetag" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"timetag" : ["(1636513802167.0, inf.0]"]}}}}

由上面的结果我们可以看出,原查询的查询计划为利用 timetag 的索引进行扫描,然后逐个过滤出符合 timetag 条件的记录。在数据分布分析中我们知道,符合目标 timetag 的数据有 1.9 亿,而扫描这些数据将会非常慢,即使使用索引。因为 getmore 操作使用的 cursor 是原查询计划产生的,同一个查询内只会使用同一个查询计划。下面通过分段执行原查询计划来佐证扫描 timetag 慢。


T0~T1 数据段执行原计划

使用上述查询计划查询 T0~T1 的数据:

"parsedQuery" : {"$and" : [{"$or" : [{"nfrom" : {"$eq" : "zhang"}},{"nto" : {"$eq" : "zhang"}}]},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}},{"timetag" : {"$lte" : 1636513802759}},{"timetag" : {"$gt" : 1636513802167}}]},"winningPlan" : {"stage" : "LIMIT","limitAmount" : 200,"inputStage" : {"stage" : "FETCH","filter" : {"$and" : [{"$or" : [{"nfrom" : {"$eq" : "zhang"}},{"nto" : {"$eq" : "zhang"}}]},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}}]},"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"timetag" : 1},"indexName" : "timetag_1","isMultiKey" : false,"multiKeyPaths" : {"timetag" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"timetag" : ["(1636513802167.0, 1636513802759.0]"]}}}},

结果立刻得到,因为总的需要扫描的数据量只有 287。


T1~T2 数据段执行原计划

使用上述查询计划查询 T1~T2 的数据:

"parsedQuery" : {"$and" : [{"$or" : [{"nfrom" : {"$eq" : "zhang"}},{"nto" : {"$eq" : "zhang"}}]},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}},{"timetag" : {"$lt" : 1636622950801}},{"timetag" : {"$gt" : 1636513802759}}]},"winningPlan" : {"stage" : "SORT","sortPattern" : {"timetag" : 1},"limitAmount" : 200,"inputStage" : {"stage" : "SORT_KEY_GENERATOR","inputStage" : {"stage" : "FETCH","filter" : {"$and" : [{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}}]},"inputStage" : {"stage" : "OR","inputStages" : [{"stage" : "IXSCAN","keyPattern" : {"nto" : 1,"validflag" : 1,"timetag" : 1},"indexName" : "nto_1_validflag_1_timetag_1","isMultiKey" : false,"multiKeyPaths" : {"nto" : [ ],"validflag" : [ ],"timetag" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"nto" : ["[\"zhang\", \"zhang\"]"],"validflag" : ["[MinKey, MaxKey]"],"timetag" : ["(1636513802759.0, 1636622950801.0)"]}},{"stage" : "IXSCAN","keyPattern" : {"nfrom" : 1,"timetag" : 1},"indexName" : "nfrom_1_timetag_1","isMultiKey" : false,"multiKeyPaths" : {"nfrom" : [ ],"timetag" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"nfrom" : ["[\"zhang\", \"zhang\"]"],"timetag" : ["(1636513802759.0, 1636622950801.0)"]}}]}}}},

查询计划变了,强制使用 timetag_1 索引来使用原查询计划:

mongos>db.xxx_collection.find({ $and:[{timetag: { $gt: 1636513802759 }}, {timetag:{$lt: 1636622950801}}], $or: [ { nto: "zhang" }, { nfrom:"zhang" } ], nid: 50, status: 0 }).sort({ timetag: 1}).limit(200).hint("timetag_1").explain("executionStats")

查询计划:

"parsedQuery" : {"$and" : [{"$or" : [{"nfrom" : {"$eq" : "zhang"}},{"nto" : {"$eq" : "zhang"}}]},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}},{"timetag" : {"$lt" : 1636622950801}},{"timetag" : {"$gt" : 1636513802759}}]},"winningPlan" : {"stage" : "LIMIT","limitAmount" : 200,"inputStage" : {"stage" : "FETCH","filter" : {"$and" : [{"$or" : [{"nfrom" : {"$eq" : "zhang"}},{"nto" : {"$eq" : "zhang"}}]},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}}]},"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"timetag" : 1},"indexName" : "timetag_1","isMultiKey" : false,"multiKeyPaths" : {"timetag" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"timetag" : ["(1636513802759.0, 1636622950801.0)"]}}}},

查询耗时:

2021-12-15T11:18:43.650+0800 I COMMAND [conn913] command xxx_db.xxx_collection appName: "MongoDB Shell" command: find { find: "xxx_collection", filter: { $and: [ { timetag: { $gt: 1636513802759.0 } }, { timetag: { $lt: 1636622950801.0 } } ], $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50.0, status: 0.0 }, projection: {$sortKey: { $meta: "sortKey" } }, sort: { timetag: 1.0 }, hint: { $hint: "timetag_1" }, limit: 200, runtimeConstants: { localNow: new Date(1639538294423), clusterTime:Timestamp(1639538291, 1) },shardVersion: [ Timestamp(0, 0), ObjectId('000000000000000000000000') ], ****** planSummary: IXSCAN {timetag: 1 } keysExamined:11338157 docsExamined:11338157 cursorExhausted:1numYields:88579 nreturned:0 reslen:386 locks:{ ReplicationStateTransition: {acquireCount: { w: 88580 } }, Global: {acquireCount: { r: 88580 } }, Database:{ acquireCount: { r: 88580 } },Collection: { acquireCount: { r: 88580 } }, Mutex: {acquireCount: { r: 2 } } } storage:{data: { bytesRead: 16223299833,timeReadingMicros: 9431804 },timeWaitingMicros: { cache: 14082 } }protocol:op_msg 29226ms

我们发现,查询 T1~T2 的空洞区域非常慢,耗时 29 秒,因为需要扫描 1 千万多的数据。


T2 后数据段执行原计划

mongos> db.xxx_collection.find({timetag: { $gt: 1636622950801 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).limit(200).explain("executionStats")

查询计划:

"parsedQuery" : {"$and" : [{"$or" : [{"nfrom" : {"$eq" : "zhang"}},{"nto" : {"$eq" : "zhang"}}]},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}},{"timetag" : {"$gt" : 1636622950801}}]},"winningPlan" : {"stage" : "SORT","sortPattern" : {"timetag" : 1},"limitAmount" : 200,"inputStage" : {"stage" : "SORT_KEY_GENERATOR","inputStage" : {"stage" : "FETCH","filter" : {"$and" : [{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}}]},"inputStage" : {"stage" : "OR","inputStages" : [{"stage" : "IXSCAN","keyPattern" : {"nto" : 1,"validflag" : 1,"timetag" : 1},"indexName" : "nto_1_validflag_1_timetag_1","isMultiKey" : false,"multiKeyPaths" : {"nto" : [ ],"validflag" : [ ],"timetag" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"nto" : ["[\"zhang\", \"zhang\"]"],"validflag" : ["[MinKey, MaxKey]"],"timetag" : ["(1636622950801.0, inf.0]"]}},{"stage" : "IXSCAN","keyPattern" : {"nfrom" : 1,"timetag" : 1},"indexName" : "nfrom_1_timetag_1","isMultiKey" : false,"multiKeyPaths" : {"nfrom" : [ ],"timetag" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"nfrom" : ["[\"zhang\", \"zhang\"]"],"timetag" : ["(1636622950801.0, inf.0]"]}}]}}}},

查询计划变了,强制使用 timetag_1 索引来使用原查询计划:

mongos>db.xxx_collection.find({ timetag: { $gt: 1636622950801 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).limit(200).hint("timetag_1").explain("executionStats")

查询计划:

"parsedQuery" : {"$and" : [{"$or" : [{"nfrom" : {"$eq" : "zhang"}},{"nto" : {"$eq" : "zhang"}}]},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}},{"timetag" : {"$gt" : 1636622950801}}]},"winningPlan" : {"stage" : "LIMIT","limitAmount" : 200,"inputStage" : {"stage" : "FETCH","filter" : {"$and" : [{"$or" : [{"nfrom" : {"$eq" : "zhang"}},{"nto" : {"$eq" : "zhang"}}]},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}}]},"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"timetag" : 1},"indexName" : "timetag_1","isMultiKey" : false,"multiKeyPaths" : {"timetag" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"timetag" : ["(1636622950801.0, inf.0]"]}}}},

日志时间输出:

2021-12-15T11:36:34.388+0800 I COMMAND [conn918] command xxx_db.xxx_collection appName: "MongoDB Shell" command: explain { explain: { find: "xxx_collection", filter: { timetag: { $gt: 1636622950801.0 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50.0, status: 0.0 }, sort: { timetag: 1.0 }, hint: { $hint: "timetag_1" }, limit: 200, ****** $db: "xxx_db" } numYields:1109015 reslen:2691 locks:{ReplicationStateTransition: { acquireCount: { w: 1109016 } }, Global: { acquireCount: { r: 1109016 } }, Database: { acquireCount: { r: 1109016 } }, Collection: { acquireCount: { r: 1109016 } }, Mutex: { acquireCount: { r: 2 } } } storage:{ data: { bytesRead: 195293544507, timeReadingMicros: 518472952 },timeWaitingMicros: { cache: 272870 } }protocol:op_msg 801697ms

由上面可以看出,查询 T2 后的数据时,需要 800 秒,因为需要扫描 1.8 亿的数据。因为原查询使用 limit(200)限制了查询的总结果数,且只有一个结果在这个时间段内,所以查询找到一个结果即可返回,不需要扫描整个时间段。

问题原因总结

总结来说,问题出现原因是 MongoDB 给出的查询计划不符合目标数据的查询。

原查询计划为使用 timetag 索引来进行扫描,获取结果后再根据其他条件进行过滤。

我们发现,查询结果在 timetag 上分段分布,分布在 timetag 的两头,中间存在一大段无效数据。第 1~199 的结果分布在 T0~T1 时间段内,第 200 后的结果分布在 T2 时间段后。

如果使用原查询计划,即只使用 timetag 索引来扫描全表,在查询第 199 个后的结果时将会非常慢,因为这些结果分布在 1.9 亿的数据里。这也解释了第一次查询获取 101 个结果快,因为只需查扫描 T0~T1 时间段内 400 多个数据,第二次查询需要获取第 102~202 个数据则慢,因为需要先扫描 1 千多万的无效数据然后才能返回结果。原查询计划不够高效,导致扫描比较慢。

问题解决方案

只使用 timetag 索引进行数据遍历较低效,使用聚合索引来在遍历数据时进行过滤,减少遍历的数据量。所以预期要使用 timetag 和 nfrom 以及 timetag 和 nto 的联合索引来进行并行查询,最后将两个查询结果进行合并。由于 MongoDB 的查询优化器不能将原有的查询转化为上面预期的查询计划,所以我们需要改写查询计划,便于 MongoDB 的查询优化器进行识别。

将原查询:

db.xxx_collection.find({timetag: { $gt: 1636513802167 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).limit(200)

转化为等价的优化查询:

db.xxx_collection.find({$or: [ {$and: [{nto: "zhang"}, {timetag: {$gt: 1636513802167}}, { nid:50}, {status: 0}]}, {$and: [{nfrom: "zhang"}, {timetag: {$gt: 1636513802167}}, { nid:50}, {status: 0}]} ] }).sort({timetag:1}).limit(200)

并在 nto 和 timetag 以及 nfrom 和 timetag(备注:原库已经有 nfrom 和 timetag 的联合索引)上建立联合索引

{"v" : 2,"key" : {"nfrom" : 1,"timetag" : 1},"name" : "nfrom_1_timetag_1","ns" : "xxx_db.xxx_collection"},{"v" : 2,"key" : {"nto" : 1,"timetag" : 1},"name" : "nto_1_timetag_1","ns" : "xxx_db.xxx_collection"},

得到的查询计划:

"parsedQuery" : {"$or" : [{"$and" : [{"nfrom" : {"$eq" : "zhang"}},{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}},{"timetag" : {"$gt" : 1636513802167}}]},{"$and" : [{"nid" : {"$eq" : 50}},{"nto" : {"$eq" : "zhang"}},{"status" : {"$eq" : 0}},{"timetag" : {"$gt" : 1636513802167}}]}]},"winningPlan" : {"stage" : "SUBPLAN","inputStage" : {"stage" : "LIMIT","limitAmount" : 200,"inputStage" : {"stage" : "PROJECTION_SIMPLE","transformBy" : {"timetag" : 1},"inputStage" : {"stage" : "SORT_MERGE","sortPattern" : {"timetag" : 1},"inputStages" : [{"stage" : "FETCH","filter" : {"$and" : [{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}}]},"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"nfrom" : 1,"timetag" : 1},"indexName" : "nfrom_1_timetag_1","isMultiKey" : false,"multiKeyPaths" : {"nfrom" : [ ],"timetag" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"nfrom" : ["[\"zhang\", \"zhang\"]"],"timetag" : ["(1636513802167.0, inf.0]"]}}},{"stage" : "FETCH","filter" : {"$and" : [{"nid" : {"$eq" : 50}},{"status" : {"$eq" : 0}}]},"inputStage" : {"stage" : "IXSCAN","keyPattern" : {"nto" : 1,"timetag" : 1},"indexName" : "nto_1_timetag_1","isMultiKey" : false,"multiKeyPaths" : {"nto" : [ ],"timetag" : [ ]},"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 2,"direction" : "forward","indexBounds" : {"nto" : ["[\"zhang\", \"zhang\"]"],"timetag" : ["(1636513802167.0, inf.0]"]}}}]}}}},

这个查询计划符合我们的预期。


查询等价证明

上述优化的查询计划可以通过布尔等价运算得到。

原查询为:

db.xxx_collection.find({timetag: { $gt: 1636513802167 }, $or: [ { nto: "zhang" }, { nfrom: "zhang" } ], nid: 50, status: 0 }).sort({ timetag:1 }).limit(200)

我们将 find 的查询条件定义抽象如下:

  • a:timetag > 1636513802167

  • b:nto = "zhang"

  • c:nfrom = "zhang"

  • d:nid = 50

  • e:status = 0

所以原查询条件相当于一个合取范式:

$$a \bigwedge (b \bigvee c) \bigwedge d \bigwedge e$$

通过逻辑运算,可以转化为一个析取范式:

$$(a \bigwedge b \bigwedge d \bigwedge e) \bigvee (a \bigwedge c \bigwedge d\bigwedge e)$$

对应如下查询:

db.xxx_collection.find({$or: [ {$and: [{nto: "zhang"}, {timetag: {$gt: 1636513802167}}, { nid:50}, {status: 0}]}, {$and: [{nfrom: "zhang"}, {timetag: {$gt: 1636513802167}}, { nid:50}, {status: 0}]} ] }).sort({timetag:1}).limit(200)


查询计划等价得证

 

关于作者:网易游戏 张家侨

用户头像

还未添加个人签名 2019.07.27 加入

还未添加个人简介

评论

发布
暂无评论
MongoDB find getmore操作慢问题排查