查询计划器 v2 - Amazon DocumentDB

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

查询计划器 v2

Amazon DocumentDB 的新查询计划程序(计划程序版本 2.0)具有高级查询优化功能和提升的性能。将 findupdate 运算符与索引结合使用后,适用于 Amazon DocumentDB 5.0 的计划程序版本 2.0 的性能较前一版本提升高达 10 倍。性能提升主要源于使用更优化的索引计划,以及为否定运算符($neq$nin)和嵌套的 $elementMatch 等运算符启用索引扫描支持。通过更优的成本估算技术、优化的算法和增强的稳定性,计划程序版本 2.0 查询可以更快速地运行。Planner 版本 2.0 还支持计划缓 APIs 存过滤器,这增强了计划器的稳定性。借助此功能,Amazon DocumentDB 5.0 现在可以从不同版本的查询计划程序中进行选择。

先决条件

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

  • 计划程序版本 2.0 已在所有提供引擎版本 5.0 的区域中推出。

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

  • 要将计划程序版本 2.0 设置为默认查询计划程序,您需要具有更新集群参数组的 IAM 权限。

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

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

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

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

  • 选择立即应用(若选择重启时应用,则在集群下次重启后才会生效)。

最佳实践

要获得预期结果,请在应用计划程序版本 2.0 时遵循以下最佳实践:

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

  • 在定期维护时段或流量减少期间更新到计划程序版本 2.0 可最大限度地减少中断,因为如果在工作负载活跃运行时更改计划程序版本,可能会导致错误率增加。

  • 计划程序版本 2.0 与 MongoDB Shell 版本 5.0 配合使用效果最佳。

限制

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

  • 弹性集群不支持计划程序版本 2.0,将回退至计划程序版本 1.0。

  • 聚合和不同命令不支持计划程序版本 2.0,将回退至计划程序版本 1.0。

  • 计划程序版本 2.0 中的计划缓存筛选条件不支持包含以下内容的查询:正则表达式、文本搜索、地理空间、jsonschema 或筛选条件中的 $expr

改进了 FindUpdate 运算符

计划程序版本 2.0 优化了基本操作,包括 findupdatedeletefind-and-modify 命令。以下选项卡式部分显示了计划程序版本 2.0 中增强的索引功能以及查询性能提升:

Enhanced index support
  • 计划程序版本 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 }}}}})
  • 即使查询表达式中没有 $exists,计划程序版本 2.0 版本也会使用稀疏索引或部分索引。

    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" })
  • 即使查询条件与部分索引筛选条件表达式不完全匹配,计划程序版本 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}})
  • 计划程序版本 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}}})
  • 计划程序版本 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" }})
  • 计划程序版本 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)
  • 计划程序版本 2.0 提升了涉及多个筛选条件的查询性能,尤其针对文档大于 8KB 的集合。

    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}}]})
  • 计划程序版本 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
  • 计划程序版本 2.0 提升了涉及多个筛选条件的查询性能,尤其针对文档大于 8KB 的集合。

    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}}]})
  • 计划程序版本 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

注意

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

  • 计划程序版本 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 命令输出中的两个新字段来分析计划程序版本 2.0 的索引筛选:indexFilterSetindexFilterApplied。如果集合上设置了与查询形状相匹配的索引筛选条件,则将 indexFilterSet 设置为“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 中 $(update)$(projection) 运算符的行为可能与 Amazon 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}})
    • 计划程序版本 1.0 – 随机更新首个匹配元素

    • MongoDB – 随机更新首个匹配元素

    • 计划程序版本 2.0 – 不进行更新

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

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

    • 计划程序版本 2.0 – 不做选择

  • db.col.insert({x:100}) db.col.update({x:100},{x:100})
    • 计划程序版本 1.0 – nModified 计数更改

    • MongoDB – nModified 计数更改

    • 计划程序版本 2.0 – nModified 计数在更新为相同值时不会更改。

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

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

Index behavior
  • 当使用不合适的索引应用 $hint 时,计划程序版本 2.0 会抛出错误,而计划程序版本 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 无法在计划程序版本 2.0 中使用 $hint({“$natural”:1})

    // 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 支持完整的正则表达式索引扫描,但计划程序版本 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)
  • 在使用计划缓存筛选条件时,计划程序版本 2.0 与 MongoDB 存在一些固有差异。虽然计划程序版本 2.0 不支持使用计划缓存筛选条件指定“投影”和“排序规则”,但 MongoDB 支持。但 MongoDB 索引筛选条件仅在内存中,重新启动后会丢失。计划程序版本 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) }

    计划程序版本 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) }

    计划程序版本 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) }

计划程序版本 2.0 弥合了与 MongoDB 的行为差距

计划程序版本 2.0 在以下方面弥补了与 MongoDB 的行为差距:

  • 计划程序版本 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 }
  • 计划程序版本 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 ] }
  • 计划程序版本 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" }
  • 计划程序版本 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 } ] }
  • 计划程序版本 2.0 与 MongoDB 一样,允许使用 $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 }