为什么 mongo 选择了错误的索引/执行计划?

Sy1*_*100 5 mongodb

在生产 mongod 日志文件中,我们可以看到很多慢查询。他们中的大多数没有使用最好的索引,因此没有使用最好的执行计划。但是,当我在 mongo shell 中自己运行相同的查询时,会使用正确的索引。那么为什么对于相同的查询,我们没有相同的执行计划?

Mongodb 版本:4.0.2(单机版)

mongod 日志文件的摘录:

2018-12-28T13:55:28.282+0100 I COMMAND [conn1032115] command mydb.products command: find { find: "products", filter: { origins_tags: "italia" }, sort: { last_modified_t: -1 }, skip: 320, limit: 20, singleBatch: false, maxTimeMS: 0, tailable: false, noCursorTimeout: false, awaitData: false, allowPartialResults: false, $readPreference: { mode: "secondaryPreferred" }, $db: "mydb" } planSummary: IXSCAN { last_modified_t: -1 } keysExamined:721542 docsExamined:721542 cursorExhausted:1 numYields:5637 nreturned:3 reslen:23886 locks:{ Global: { acquireCount: { r: 5638 } }, Database: { acquireCount: { r: 5638 } }, Collection: { acquireCount: { r: 5638 } } } protocol:op_query 5166ms
Run Code Online (Sandbox Code Playgroud)

我们可以看到以下信息:

  • 计划摘要:IXSCAN { last_modified_t: -1 }
  • keysExamined:721542 docsExamined:721542(基本上,检查整个集合)
  • 返回:3
  • 持续时间:5166ms

但是,这些索引存在于 collection 中:

db.products.createIndex({"origins_tags": 1,"sortkey": -1}, { background: true })
db.products.createIndex({"last_modified_t": -1}, { background: true })
Run Code Online (Sandbox Code Playgroud)

这是优化的执行计划(预期的):

  • planSummary: IXSCAN {"origins_tags": 1,"sortkey": -1}
  • 键已检查:331 文档已检查:331
  • 返回:3
  • 持续时间:11ms

所以我们可以看到一个巨大的差异!

Ste*_*nie 11

这些索引存在于集合中:

db.products.createIndex({"origins_tags": 1,"sortkey": -1}, { background: true })
db.products.createIndex({"last_modified_t": -1}, { background: true })
Run Code Online (Sandbox Code Playgroud)

MongoDB 查询优化器根据查询形状(查询谓词、排序和投影的组合)和候选索引选择最有效的查询计划。如果给定查询形状有多个候选计划,则将运行试验评估以确定哪个索引返回初始批次的结果(101 个文档),并且测量的“工作”量最少。如果查询性能或其他情况发生变化(例如,添加或删除索引),获胜计划将被缓存并定期重新评估

由于这两个索引都不是您的查询的理想选择,因此索引选择可能会有所不同,具体取决于哪个索引在计划评估期间更快地返回初始批次的结果:

  • 第一个索引origins_tags作为过滤条件更具选择性,但它需要文档获取和内存中排序(具有32MB 限制的阻塞查询阶段)才能返回按 排序的结果last_modified。对给定查询使用此索引所需的工作将取决于匹配文档的总数:必须提取每个匹配文档进行排序。最坏的情况是查询需要对超过 32MB 的数据执行内存排序并导致异常。
  • 第二个索引以所需的排序顺序返回结果,但需要集合扫描来过滤origins_tags。使用此索引所需的工作取决于找到匹配项的速度:此索引可以在找到匹配项时流式传输匹配项,并在匹配项足够多时立即停止。最坏的情况是进行完整的集合扫描以确认没有进一步匹配的文档。

如果评估两个计划的结果是平局(两者似乎都执行相同的工作以返回给定查询形状的初始结果),则不需要内存中排序的计划将获胜。

您可以通过使用mode:解释查询来查看计划评估的详细信息。allPlansExecutiondb.products.find({...}).explain('allPlansExecution')

keysExamined:721542 docsExamined:721542(基本上,检查整个集合)

这是第二个索引的最坏情况:721,542 个文档中有 323 个匹配项,并且您要求结果 320-340(通过skip: 20, limit: 20)。

我不明白第二个索引如何在计划评估期间提供更快的结果

计划评估只考虑哪个候选计划以较少的总体工作返回初始批次的结果(101 个文档)。查询计划器不会在评估期间将计划运行到完成,也不会保留有关键值分布的任何指标。缓存查询计划将基于比较,其中第二个索引不必执行集合扫描。

当 mongo 选择一个糟糕的计划时,它应该能够看到执行时间不好并且它不应该为接下来的查询选择相同的计划。

根据 MongoDB 文档中的查询计划流程图,如果性能下降,将重新评估缓存计划。但是,如果您有多个没有确定性获胜者的候选计划,则可能会选择一个查询计划,该计划对于初始结果(或具有不同值的相同查询形状)更快,但对于以后的结果却不是最佳选择。

我怎样才能解决这个问题?

要解决此问题,您应该添加一个支持过滤器和排序条件的复合索引:

db.products.createIndex({"origins_tags": 1,"last_modified_t": -1}, { background: true })
Run Code Online (Sandbox Code Playgroud)

或者(不太理想)您可以提供索引提示以强制查询使用第一个索引。请注意,提示将忽略任何可能更理想的未来添加的索引,并且如果内存中排序需要处理超过 32MB 的数据,也会失败并出现异常。

添加建议的复合索引将导致检查的键和文档与返回的结果数量的最有效比率。