查询计划器 - Amazon DocumentDB

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

查询计划器

Amazon DocumentDB 的新查询规划器(计划器版本 2.0)具有高级查询优化功能和改进的性能。使用带有索引的update运算符时,适用于 Amazon DocumentDB 5.0 的 Planner 版本 2.0 find 与之前的版本相比,性能提高了多达 10 倍。性能改进主要来自于使用更优化的索引计划以及为否定运算符 ($neq$nin) 和嵌套$elementMatch等运算符启用索引扫描支持。通过更好的成本估算技术、优化的算法和增强的稳定性,Planner 2.0 版查询可以更快地运行。Planner 版本 2.0 还支持计划缓 APIs 存过滤器,这增强了计划器的稳定性。借助此功能,Amazon DocumentDB 5.0 现在可以从不同版本的查询计划器中进行选择。

先决条件

以下先决条件适用于计划器版本 2.0:

  • Planner 版本 2.0 适用于所有提供引擎版本 5.0 的区域。

  • 要选择使用 2.0 版作为默认查询计划器,您的集群需要使用引擎补丁版本 3.0.15902 或更高版本的 Amazon DocumentDB 5.0 版本。有关更新到最新引擎版本补丁的步骤,请参阅对集群的引擎版本执行补丁更新

  • 要将规划器版本 2.0 设置为默认查询规划器,您需要具有 IAM 权限才能更新集群参数组。

选择计划器版本 2.0 作为默认查询计划器

使用以下步骤从控制台或 CLI 中选择 2.0 作为默认查询计划器:

  • 按照中的步骤修改 Amazon DocumentDB 集群参数修改集群的参数组。

  • 对于名为 “PlannerVersion” 的参数,将值更改为 2.0,表示计划器版本 2.0。

  • 选择 “立即应用”(选择 “重启时应用” 将使该选择在下次重启集群之前失效)。

最佳实践

要获得预期结果,请在应用 Planner 版本 2.0 时使用以下最佳实践:

  • 在全局群集中,在两个区域的群集参数组中选择相同的plannerVersion值(1.0 或 2.0)。请注意,在主要和次要区域中选择不同的计划器版本可能会导致查询行为和性能不一致。

  • 在定期维护时段或流量减少期间更新到计划器版本 2.0 的干扰最小,因为如果在工作负载活跃运行时更改计划器版本,则错误率可能会增加。

  • Planner 版本 2.0 与 MongoDB 外壳版本 5.0 配合使用效果最佳。

限制

以下限制适用于计划器版本 2.0:

  • 弹性集群不支持 Planner 版本 2.0,它将退回到计划器版本 1.0。

  • Planner 版本 2.0 不支持聚合和不同命令,这将回退到计划器版本 1.0。

  • 计划程序版本 2.0 $expr 中的计划缓存过滤器不支持包含正则表达式、文本搜索、地理空间、jsonschema 或过滤器的查询。

对 and Update 运算符Find的改进

Planner 版本 2.0 优化了基本操作find,包括updatedelete、和find-and-modify命令。以下选项卡式部分显示了 Planner 版本 2.0 中增强的索引功能以及查询性能的改进:

Enhanced index support
  • Planner 版本 2.0 增加了对否定运算符的索引支持,包括$nin$ne$not {eq}$not {in}、、和,以及$type$elemMatch

    Sample Document: { "x": 10, "y": [1, 2, 3] } db.foo.createIndex({ "x": 1, "y": 1 }) db.foo.find({ "x": {$nin: [20, 30] }}) db.foo.find({"x":{ $type: "string" }}) db.foo.createIndex({"x.y": 1}) db.foo.find({"x":{$elemMatch:{"y":{$elemMatch:{"$gt": 3 }}}}})
  • Planner 版本 2.0 使用稀疏或部分索引,即使查询$exists表达式中不存在也是如此。

    Sample Document: {"name": "Bob", "email": "example@fake.com" } Using Planner Version 1.0, you can specify the command as shown below: db.foo.find({email: "example@fake.com", email: {$exists: true}}) Using Planner Version 2.0, you can specify command without $exists: db.foo.find({ email: "example@fake.com" })
  • 即使查询条件与部分索引筛选表达式不完全匹配,Planner 版本 2.0 也将使用部分索引。

    Sample Document: {"name": "Bob", "age": 34} db.foo.createIndex({"age":1},{partialFilterExpression:{"age":{$lt:50}}}) With Planner Version 1.0, index is used only when the query condition meets the partial index filter criterion: db.foo.find({"age":{$lt:50}}) With Planner Version 2.0, index is used even when the query condition doesn’t meet the index criterion: db.foo.find({"age":{$lt:30}})
  • Planner 版本 2.0 使用部分索引扫描和 $elemMatch 查询。

    Sample Document: {"name": "Bob", "age": [34,35,36]} db.foo.createIndex({"age":1},{partialFilterExpression:{"age":{$lt:50,$gt:20}}}) db.foo.find({age:{$elemMatch:{$lt:50,$gt:20}}})
  • Planner 版本 2.0 包括对索引扫描的支持$regex,无需$hint在应用程序代码中提供。 $regex仅支持前缀搜索上的索引。

    Sample Document: { "x": [1, 2, 3], "y": "apple" } db.foo.createIndex({ "x": 1, "y": 1 }) db.foo.find({"y":{ $regex: "^a" }})
  • Planner 版本 2.0 提高了涉及多键索引的查询的性能,多键字段具有相等条件。

    Sample Document: {"x": [1, 2, 3], "y": 5} db.foo.createIndex({"x": 1, "y":1}) db.foo.find({"x": 2, "y": {$gt: 1}}).limit(1)
  • Planner 版本 2.0 提高了涉及多个筛选器的查询的性能,尤其是在文档大于 8 KB 的集合中。

    Sample Document: {"x": 2, "y": 4, "z": 9, "t": 99} db.foo.find({$and: [{"x": {$gt : 1}, "y": {$gt : 3}, "z": {$lt : 10}, "t":{$lt : 100}}]})
  • Planner 版本 2.0 通过消除排序阶段,提高了使用带有复合索引的$in运算符时的查询性能。

    Sample Document: {"x": 2, "y": 4, "z": 9, "t": 99} db.foo.createIndex({"x":1, "y":1}) db.foo.find({"x":2, "y":$in:[1,2,3,4]}).sort({x:1,y:1})

    它还提高了使用带有$in元素的多键索引的查询的性能。

    Sample Document: {"x": [1, 2, 3]} db.foo.createIndex({"x": 1}) db.foo.find("x":{$in:[>100 elements]})
Query performance improvements
  • Planner 版本 2.0 提高了涉及多个筛选器的查询的性能,尤其是在文档大于 8 KB 的集合中。

    Sample Document: {"x": 2, "y": 4, "z": 9, "t": 99} db.foo.find({$and: [{"x": {$gt : 1}, "y": {$gt : 3}, "z": {$lt : 10}, "t":{$lt : 100}}]})
  • Planner 版本 2.0 通过消除排序阶段,提高了使用带有复合索引的$in运算符时的查询性能。

    Sample Document: {"x": 2, "y": 4, "z": 9, "t": 99} db.foo.createIndex({"x":1, "y":1}) db.foo.find({"x":2, "y":$in:[1,2,3,4]}).sort({x:1,y:1})

    它还提高了使用带有$in元素的多键索引的查询的性能。

    Sample Document: {"x": [1, 2, 3]} db.foo.createIndex({"x": 1}) db.foo.find("x":{$in:[>100 elements]})

计划缓存过滤器 API

注意

计划缓存筛选器不支持文本索引。

  • Planner 版本 2.0 增加了对索引筛选功能的支持,该功能允许您指定特定查询形状可以使用的索引列表。此功能可通过 API 访问,也可从服务器端进行控制。如果您遇到查询回归问题,此功能可为您提供更快、更灵活的选项,无需修改应用程序代码即可缓解问题。

    db.runCommand({ planCacheSetFilter: <collection>, query: <query>, sort: <sort>, // optional, indexes: [ <index1>, <index2>, ...], comment: <any> // optional})

    要列出集合中的所有过滤器,请使用以下命令:

    db.runCommand( { planCacheListFilters: <collection> } )

    此命令显示集合上的所有索引过滤器。输出示例:

    { "filters" : [ { "query" : {a: "@", b: "@"}, "sort" : {a: 1}, "indexes" : [ <index1>, ... ] }, ... ], "ok": 1 }
  • 您可以使用explain命令输出中的两个新字段来分析 Planner 版本 2.0 的索引筛选:indexFilterSetindexFilterAppliedindexFilterSet如果集合上设置了与查询形状相匹配的索引筛选器,则设置为 “true”。 indexFilterApplied当且仅当查询应用索引筛选器并使用筛选器列表中的索引选择计划时,才会设置为 “true”。

    您可以使用以下命令清除索引过滤器:

    db.runCommand( { planCacheClearFilters: <collection>> query: <query pattern>, // optional sort: <sort specification>, // optional comment: <any>. //optional } )

    要清除集合 “foo” 上的所有过滤器,请使用以下命令:

    db.runCommand({planCacheClearFilters: "foo"})

    要清除任何排序的特定查询形状,可以从以下输出中复制并粘贴查询形状planCacheListFilters

    db.runCommand({planCacheClearFilters: "foo", query: {a: @}})

    要清除具有指定字段作为排序依据的特定查询形状,可以从以下输出中复制并粘贴查询形状planCacheListFilters

    db.runCommand({planCacheClearFilters: "foo", query: {a: @},sort: {a: 1}})

计划器版本 1.0、2.0 和 MongoDB 之间的潜在行为差异

在某些边缘情况下,计划器版本 2.0 可能产生的结果与 MongoDB 中的结果略有不同。本节将介绍这些可能性的一些示例。

$(update) and $(projection)
  • 在某些情况下,MongoDB 中的$(projection)运算符的行为可能$(update)与亚马逊 DocumentDB 的计划器版本 1.0 不同。以下是一些示例:

    db.students_list.insertMany( [ { _id: 5, student_ids: [ 100, 200 ], grades: [ 95, 100 ], grad_year: [ 2024, 2023 ] } ] )
    db.students_list.updateOne({ student_ids: 100, grades: 100, grad_year: 2024 }, { $set: { “grad_year.$”: 2025 } }
    • 规划师版本 1.0 — 更新字段 2022

    • MongoDB — 2022 年更新字段

    • 规划师版本 2.0 — 2021 年更新字段

  • db.col.insert({x:[1,2,3]}) db.col.update({$and:[{x:1},{x:3}]},{$set:{"x.$":500}})
    • Planner 版本 1.0 — 随机更新第一个匹配的元素

    • MongoDB — 随机更新第一个匹配的元素

    • 规划器版本 2.0-不进行更新

  • db.col.insert({x:[1,2,3]}) db.col.find()
    • Planner 版本 1.0-随机选择匹配的元素

    • MongoDB — 随机选择匹配的元素

    • Planner 版本 2.0-不做出选择

  • db.col.insert({x:100}) db.col.update({x:100},{x:100})
    • Planner 版本 1.0 — n修改了计数更改

    • MongoDB — 未修改的计数变化

    • Planner 版本 2.0 — nModified 计数在更新为相同值时不会改变。

  • $(update)运算符与一起使用时$setOnInsert,计划器版本 1.0 和 MongoDB 会抛出错误,但计划器版本 2.0 不会抛出错误。

  • 在计划器版本 2.0 中将不存在的字段重命名为会$field引发错误,而在计划器版本 1.0 和 MongoDB 中不会生成更新。

Index behavior
  • Planner 版本 2.0 在使用不合适的索引时会$hint引发错误,而规划器版本 1.0 和 MongoDB 则不会。

    // Insert db.col.insert({x:1}) db.col.insert({x:2}) db.col.insert({x:3}) // Create index on x with partialFilter Expression {x:{$gt:2}} db.col.createIndex({x:1},{partialFilterExpression:{x:{$gt:2}}}) // Mongodb allows hint on the following queries db.col.find({x:1}).hint("x_1") // result is no documents returned because {x:1} is not indexed by the partial index // Without $hint mongo should return {x:1}, thus the difference in result between COLSCAN and IXSCAN DocumentDB will error out when $hint is applied on such cases. db.col.find({x:1}).hint("x_1") Error: error: { "ok" : 0, "operationTime" : Timestamp(1746473021, 1), "code" : 2, "errmsg" : "Cannot use Hint for this Query. Index is multi key index , partial index or sparse index and query is not optimized to use this index." } rs0:PRIMARY> db.runCommand({"planCacheSetFilter": "col", "query": { location: {$nearSphere: {$geometry: {type: "Point", coordinates: [1, 1]}}}}, "indexes": ["name_1"]}) { "ok" : 0, "operationTime" : Timestamp(1750815778, 1), "code" : 303, "errmsg" : "Unsupported query shape for index filter $nearSphere" }
  • $near无法$hint({“$natural”:1})在计划器版本 2.0 中使用。

    // indexes present are index on x and geo index rs0:PRIMARY> db.usarestaurants.getIndexes() [ { "v" : 4, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.usarestaurants" }, { "v" : 4, "key" : { "location" : "2dsphere" }, "name" : "location_2dsphere", "ns" : "test.usarestaurants", "2dsphereIndexVersion" : 1 } ] // Planner Version 2.0 will throw an error when $hint is applied with index "x_1" rs0:PRIMARY> db.usarestaurants.find({ "location":{ "$nearSphere":{ "$geometry":{ "type":"Point", "coordinates":[ -122.3516, 47.6156 ] }, "$minDistance":1, "$maxDistance":2000 } } }, { "name":1 }).hint({"$natural": 1}) Error: error: { "ok" : 0, "operationTime" : Timestamp(1746475524, 1), "code" : 291, "errmsg" : "unable to find index for $geoNear query" } // Planner Version 1.0 and MongoDB will not throw an error db.usarestaurants.find({ "location":{ "$nearSphere":{ "$geometry":{ "type":"Point", "coordinates":[ -122.3516, 47.6156 ] }, "$minDistance":1, "$maxDistance":2000 } } }, { "name":1 }).hint({"$natural": 1}) { "_id" : ObjectId("681918e087dadfd99b7f0172"), "name" : "Noodle House" }
  • 虽然 MongoDB 支持完整的正则表达式索引扫描,但 Planner 版本 2.0 仅支持对前缀字段进行正则表达式索引扫描。

    // index on x db.col.createIndex({x:1}) // index scan is used only for prefix regexes rs0:PRIMARY> db.col.find({x: /^x/}).explain() { "queryPlanner" : { "plannerVersion" : 2, "namespace" : "test.col", "winningPlan" : { "stage" : "IXSCAN", "indexName" : "x_1", "direction" : "forward", "indexCond" : { "$and" : [ { "x" : { "$regex" : /^x/ } } ] }, "filter" : { "x" : { "$regex" : /^x/ } } } }, "indexFilterSet" : false, "indexFilterApplied" : false, "ok" : 1, "operationTime" : Timestamp(1746474527, 1) } // COLSCAN is used for non-prefix regexes rs0:PRIMARY> db.col.find({x: /x$/}).explain() { "queryPlanner" : { "plannerVersion" : 2, "namespace" : "test.col", "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "x" : { "$regex" : /x$/ } } } }, "indexFilterSet" : false, "indexFilterApplied" : false, "ok" : 1, "operationTime" : Timestamp(1746474575, 1)
  • 与 MongoDB 相比,在计划程序版本 2.0 中使用计划缓存筛选器有一些固有的区别。虽然规划器版本 2.0 不支持使用计划缓存过滤器指定 “投影” 和 “排序规则”,但 MongoDB 支持。但是,MongoDB 索引过滤器仅在内存中,重启后会丢失。Planner 版本 2.0 通过重新启动和补丁来保留索引过滤器。

Others
  • 使用计划器版本 2.0 时,DML 审核日志的格式与计划器版本 1.0 的格式略有不同。

    command - db.col.find({x:1}) ************** Audit logs generated ****************** // v1 format for dml audit logs {"atype":"authCheck","ts":1746473479983,"timestamp_utc":"2025-05-05 19:31:19.983","remote_ip":"127.0.0.1:47022","users":[{"user":"serviceadmin","db":"test"}],"param":{"command":"find","ns":"test.col","args":{"batchSize":101,"filter":{"x":1},"find":"col","limit":18446744073709551615,"lsid":{"id":{"$binary":"P6RCGz9ZS4iWBSSHWXW15A==","$type":"4"},"uid":{"$binary":"6Jo8PisnEi3dte03+pJFjdCyn/5cGQL8V2KqaoWsnk8=","$type":"0"}},"maxScan":18446744073709551615,"singleBatch":false,"skip":0,"startTransaction":false},"result":0}} // v2 formal for dml audit logs {"atype":"authCheck","ts":1746473583711,"timestamp_utc":"2025-05-05 19:33:03.711","remote_ip":"127.0.0.1:37754","users":[{"user":"serviceadmin","db":"test"}],"param":{"command":"find","ns":"test.col","args":{"find":"col","filter":{"x":1},"lsid":{"id":{"$binary":"nJ88TGCSSd+BeD2+ZtrhQg==","$type":"4"}},"$db":"test"},"result":0}}
  • 作为解释计划一部分的索引条件:

    rs0:PRIMARY> db.col.createIndex({index1:1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1, "operationTime" : Timestamp(1761149251, 1) }

    Planner 版本 2.0 解释了显示索引条件和筛选器的计划输出:

    rs0:PRIMARY> db.col.find({$and:[{price:{$eq:300}},{item:{$eq:"apples"}}]}).explain() { "queryPlanner" : { "plannerVersion" : 2, "namespace" : "test.col", "winningPlan" : { "stage" : "IXSCAN", "indexName" : "price_1", "direction" : "forward", "indexCond" : { "$and" : [ { "price" : { "$eq" : 300 } } ] }, "filter" : { "$and" : [ { "item" : { "$eq" : "apples" } } ] } } }, "indexFilterSet" : false, "indexFilterApplied" : false, "ok" : 1, "operationTime" : Timestamp(1761149497, 1) }

    Planner 版本 1.0 解释计划输出:

    rs0:PRIMARY> db.col.find({$and:[{price:{$eq:300}},{item:{$eq:"apples"}}]}).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "test.col", "winningPlan" : { "stage" : "IXSCAN", "indexName" : "price_1", "direction" : "forward" } }, "ok" : 1, "operationTime" : Timestamp(1761149533, 1) }

Planner 版本 2.0 弥合了与 MongoDB 的行为差距

计划器版本 2.0 在某些方面弥补了 MongoDB 的行为差距:

  • Planner 版本 2.0 允许在扁平化数组上进行数字索引查找:$elemMatch

    doc: {"x" : [ [ { "y" : 1 } ] ] } // Planner Version 2 and mongo > db.bar.find({"x.0": {$elemMatch: {y: 1}}}) { "_id" : ObjectId("68192947945e5846634c455a"), "x" : [ [ { "y" : 1 } ] ] } > db.bar.find({"x": {$elemMatch: {"0.y": 1}}}) { "_id" : ObjectId("68192947945e5846634c455a"), "x" : [ [ { "y" : 1 } ] ] } //Whereas Planner Version 1 wouldn't return any results. > db.bar.find({"x.0": {$elemMatch: {y: 1}}}) > db.bar.find({"x": {$elemMatch: {"0.y": 1}}})
  • 虽然规划器版本 1.0 在投影中排除了字符串,但规划器版本 2.0 的行为与 MongoDB 一致,并将它们视为文字值”

    // Planner V2/ MongoDB > db.col.find() { "_id" : ObjectId("681537738aa101903ed2fe05"), "x" : 1, "y" : 1 } > db.col.find({},{x:"string"}) { "_id" : ObjectId("681537738aa101903ed2fe05"), "x" : "string" } // Planner V1 treats strings as exclude in projection rs0:PRIMARY> db.col.find() { "_id" : ObjectId("68153744d42969f11d5cca72"), "x" : 1, "y" : 1 } rs0:PRIMARY> db.col.find({},{x:"string"}) { "_id" : ObjectId("68153744d42969f11d5cca72"), "y" : 1 }
  • Planner 版本 2.0 与 MongoDB 一样,不允许在相同的字段 “x” 和 “x.a” 上进行投影:

    // Planner version 2/MongoDB will error out > db.col.find() { "_id" : ObjectId("68153da2012265816bc9ba23"), "x" : [ { "a" : 1 }, 3 ] } db.col.find({},{"x.a":1,"x":1}) // error // Planner Version 1 does not error out db.col.find() { "_id" : ObjectId("68153da2012265816bc9ba23"), "x" : [ { "a" : 1 }, 3 ] } db.col.find({},{"x.a":1,"x":1}) { "_id" : ObjectId("68153d60143af947c720d099"), "x" : [ { "a" : 1 }, 3 ] }
  • Planner 版本 2.0 与 MongoDB 一样,允许对子文档进行投影:

    // Planner Version2/MongoDB supports projections on subdocuments db.col.find() { "_id" : ObjectId("681542d8f35ace71f0a50004"), "x" : [ { "y" : 100 } ] } > db.col.find({},{"x":{"y":1}}) { "_id" : ObjectId("681542b7a22d548e4ac9ddea"), "x" : [ { "y" : 100 } ] } // Planner V1 throws error if projection is subdocument db.col.find() { "_id" : ObjectId("681542d8f35ace71f0a50004"), "x" : [ { "y" : 100 } ] } rs0:PRIMARY> db.col.find({},{"x":{"y":1}}) Error: error: { "ok" : 0, "operationTime" : Timestamp(1746223914, 1), "code" : 2, "errmsg" : "Unknown projection operator y" }
  • 在 Planner 2.0 版本中,像 MongoDB 一样,投影不支持运算符之后的字段:$

    // Mongo and Planner Version 2 will error out db.col.find() { "_id" : ObjectId("68155fa812f843439b593f3f"), "x" : [ { "a" : 100 } ] } db.col.find({"x.a":100},{"x.$.a":1}) - // error // v1 will not error out db.col.find() { "_id" : ObjectId("68155fa812f843439b593f3f"), "x" : [ { "a" : 100 } ] } db.col.find({"x.a":100},{"x.$.a":1}) { "_id" : ObjectId("68155dee13b051d58239cd0a"), "x" : [ { "a" : 100 } ] }
  • 与 MongoDB 一样,Planner 版本 2.0 允许使用:$hint

    // v1 will error out on $hint if there are no filters db.col.find({}).hint("x_1") Error: error: { "ok" : 0, "operationTime" : Timestamp(1746466616, 1), "code" : 2, "errmsg" : "Cannot use Hint for this Query. Index is multi key index , partial index or sparse index and query is not optimized to use this index." } // Mongo and Planner Version 2 will allow $hint usage db.col.find({}).hint("x_1") { "_id" : ObjectId("6818f790d5ba9359d68169cf"), "x" : 1 }