Compartilhar via

排查使用 Azure Cosmos DB for MongoDB 时的查询问题

适用对象: Mongodb

重要

你是否正在寻找一种数据库解决方案,以应对需要高扩展性、99.999% 可用性服务级别协议(SLA)、即时自动扩展和跨多个区域的自动故障转移的场景? 请考虑使用 Azure Cosmos DB for NoSQL

本文逐步讲解一般建议的方法,用于排查 Azure Cosmos DB 中的查询问题。 虽然不应将本文中所述的步骤视为针对潜在查询问题的完全防御方法,但我们在其中包含了最常见的性能提示。 本文可作为排查 Azure Cosmos DB 的 MongoDB API 查询速度缓慢或成本高昂问题的参考起点。 如果使用 Azure Cosmos DB for NoSQL,请参阅 API for NoSQL 查询故障排除指南一文。

Azure Cosmos DB 中的查询优化被广泛分类,如下所示:

  • 可降低查询请求单位 (RU) 费用的优化
  • 仅降低延迟的优化

如果降低查询的 RU 费用,通常还会降低延迟。

注释

本文假设你使用的是 Azure Cosmos DB 的 MongoDB 帐户 API,版本为 3.6 及更高。 在版本 3.2 中性能较差的某些查询在版本 3.6 中有了显著改进。 通过提交 support 请求升级到版本 3.6

使用 $explain 命令获取指标

在 Azure Cosmos DB 中优化查询时,第一步始终是获取查询的请求单位(RU)费用。 一个粗略的指导原则是,对于费用超过 50 RU 的查询,你可以探索降低 RU 费用的方法。

除了获取 RU 费用之外,还应使用 $explain 命令来获取查询和索引使用指标。 以下示例运行查询并使用 $explain 命令来显示查询和索引使用指标:

$explain 命令:

db.coll.find({foodGroup: "Baby Foods"}).explain({"executionStatistics": true })

输出:

{
    "stages" : [ 
        {
            "stage" : "$query",
            "timeInclusiveMS" : 905.2888,
            "timeExclusiveMS" : 905.2888,
            "in" : 362,
            "out" : 362,
            "details" : {
                "database" : "db-test",
                "collection" : "collection-test",
                "query" : {
                    "foodGroup" : {
                        "$eq" : "Baby Foods"
                    }
                },
                "pathsIndexed" : [],
                "pathsNotIndexed" : [ 
                    "foodGroup"
                ],
                "shardInformation" : [ 
                    {
                        "activityId" : "e68e6bdd-5e89-4ec5-b053-3dbbc2428140",
                        "shardKeyRangeId" : "0",
                        "durationMS" : 788.5867,
                        "preemptions" : 1,
                        "outputDocumentCount" : 362,
                        "retrievedDocumentCount" : 8618
                    }
                ],
                "queryMetrics" : {
                    "retrievedDocumentCount" : 8618,
                    "retrievedDocumentSizeBytes" : 104963042,
                    "outputDocumentCount" : 362,
                    "outputDocumentSizeBytes" : 2553535,
                    "indexHitRatio" : 0.0016802042237178,
                    "totalQueryExecutionTimeMS" : 777.72,
                    "queryPreparationTimes" : {
                        "queryCompilationTimeMS" : 0.19,
                        "logicalPlanBuildTimeMS" : 0.14,
                        "physicalPlanBuildTimeMS" : 0.09,
                        "queryOptimizationTimeMS" : 0.03
                    },
                    "indexLookupTimeMS" : 0,
                    "documentLoadTimeMS" : 687.22,
                    "vmExecutionTimeMS" : 774.09,
                    "runtimeExecutionTimes" : {
                        "queryEngineExecutionTimeMS" : 37.45,
                        "systemFunctionExecutionTimeMS" : 10.82,
                        "userDefinedFunctionExecutionTimeMS" : 0
                    },
                    "documentWriteTimeMS" : 49.42
                }
            }
        }
    ],
    "estimatedDelayFromRateLimitingInMilliseconds" : 0.0,
    "continuation" : {
        "hasMore" : false
    },
    "ok" : 1.0
}

$explain 命令输出很长,包含有关查询执行情况的详细信息。 但是一般情况下,在优化查询性能时应重点关注几个部分:

指标 DESCRIPTION
timeInclusiveMS 后端查询延迟
pathsIndexed 显示查询已使用的索引
pathsNotIndexed 显示查询可能已使用的索引(如果有)
shardInformation 特定物理分区的查询性能摘要
retrievedDocumentCount 查询引擎加载的文档数
outputDocumentCount 查询结果中返回的文档数
estimatedDelayFromRateLimitingInMilliseconds 由于速率限制而导致的额外查询延迟估计值

获取查询指标后,请将 retrievedDocumentCount 与你的查询的 outputDocumentCount 进行比较。 使用这种比较可以确定要在本文中查看的相关部分。 retrievedDocumentCount 是查询引擎需要加载的文档数。 outputDocumentCount 是查询结果所需的文档数。 如果 retrievedDocumentCount 明显高于 outputDocumentCount,则查询中至少有一个部分无法使用索引,需要进行扫描。

请参阅以下部分,了解适用于你的方案的相关查询优化。

查询的 RU 费用过高

“已检索文档计数”明显大于“输出文档计数”

“已检索文档计数”约等于“输出文档计数”

查询的 RU 费用可接受,但延迟仍然过高

“已检索文档计数”超过“输出文档计数”的查询

retrievedDocumentCount 是查询引擎需要加载的文档数。 outputDocumentCount 是查询返回的文档数。 如果 retrievedDocumentCount 明显高于 outputDocumentCount,则查询中至少有一个部分无法使用索引,需要进行扫描。

下面是一个不完全由索引提供服务的扫描查询示例:

$explain 命令:

db.coll.find(
  {
    $and : [
            { "foodGroup" : "Cereal Grains and Pasta"}, 
            { "description" : "Oat bran, cooked"}
        ]
  }
).explain({"executionStatistics": true })

输出:

{
    "stages" : [ 
        {
            "stage" : "$query",
            "timeInclusiveMS" : 436.5716,
            "timeExclusiveMS" : 436.5716,
            "in" : 1,
            "out" : 1,
            "details" : {
                "database" : "db-test",
                "collection" : "indexing-test",
                "query" : {
                    "$and" : [ 
                        {
                            "foodGroup" : {
                                "$eq" : "Cereal Grains and Pasta"
                            }
                        }, 
                        {
                            "description" : {
                                "$eq" : "Oat bran, cooked"
                            }
                        }
                    ]
                },
                "pathsIndexed" : [],
                "pathsNotIndexed" : [ 
                    "foodGroup", 
                    "description"
                ],
                "shardInformation" : [ 
                    {
                        "activityId" : "13a5977e-a10a-4329-b68e-87e4f0081cac",
                        "shardKeyRangeId" : "0",
                        "durationMS" : 435.4867,
                        "preemptions" : 1,
                        "outputDocumentCount" : 1,
                        "retrievedDocumentCount" : 8618
                    }
                ],
                "queryMetrics" : {
                    "retrievedDocumentCount" : 8618,
                    "retrievedDocumentSizeBytes" : 104963042,
                    "outputDocumentCount" : 1,
                    "outputDocumentSizeBytes" : 6064,
                    "indexHitRatio" : 0.0,
                    "totalQueryExecutionTimeMS" : 433.64,
                    "queryPreparationTimes" : {
                        "queryCompilationTimeMS" : 0.12,
                        "logicalPlanBuildTimeMS" : 0.09,
                        "physicalPlanBuildTimeMS" : 0.1,
                        "queryOptimizationTimeMS" : 0.02
                    },
                    "indexLookupTimeMS" : 0,
                    "documentLoadTimeMS" : 387.44,
                    "vmExecutionTimeMS" : 432.93,
                    "runtimeExecutionTimes" : {
                        "queryEngineExecutionTimeMS" : 45.36,
                        "systemFunctionExecutionTimeMS" : 16.86,
                        "userDefinedFunctionExecutionTimeMS" : 0
                    },
                    "documentWriteTimeMS" : 0.13
                }
            }
        }
    ],
    "estimatedDelayFromRateLimitingInMilliseconds" : 0.0,
    "continuation" : {
        "hasMore" : false
    },
    "ok" : 1.0
}

retrievedDocumentCount (8618) 明显高于 outputDocumentCount (1),表示此查询需要文档扫描。

包括必要的索引

你应检查 pathsNotIndexed 数组并添加这些索引。 在此示例中,应当为路径 foodGroupdescription 编制索引。

"pathsNotIndexed" : [ 
                    "foodGroup", 
                    "description"
                ]

Azure Cosmos DB 的用于 MongoDB 的 API 中的索引最佳做法不同于 MongoDB。 在 Azure Cosmos DB 的用于 MongoDB 的 API 中,复合索引仅用于需要按多个属性进行高效排序的查询中。 如果你的查询具有基于多个属性的筛选器,你应当为这些属性中的每一个创建单字段索引。 查询谓词可以使用多个单字段索引。

通配符索引可以简化索引编制。 与在 MongoDB 中不同,通配符索引可以在查询谓词中支持多个字段。 如果使用一个通配符索引,而不是为每个属性创建单独的索引,查询性能不会有差异。 为所有属性添加通配符索引是优化所有查询的最简单方法。

你可以随时添加新索引,而不会影响写入或读取可用性。 你可以跟踪索引转换进度

了解哪些聚合操作使用索引

在大多数情况下,Azure Cosmos DB 的 API for MongoDB 中的聚合操作将部分使用索引。 通常情况下,查询引擎会首先应用等式和范围筛选器,并使用索引。 应用这些筛选器后,查询引擎可以评估其他筛选器,并根据需要加载其余文档以计算聚合。

下面是一个示例:

db.coll.aggregate( [
   { $match: { foodGroup: 'Fruits and Fruit Juices' } },
   {
     $group: {
        _id: "$foodGroup",
        total: { $max: "$version" }
     }
   }
] )

在这种情况下,索引可以优化 $match 阶段。 为 foodGroup 添加索引将极大地提高查询性能。 与在 MongoDB 中一样,应尽早在聚合管道中放置 $match,以最大程度地利用索引。

在 Azure Cosmos DB 的用于 MongoDB 的 API 中,索引不用于实际聚合,在本例中为 $max。 在 version 上添加索引不会改进查询性能。

当“已检索文档计数”等于“输出文档计数”时的查询

如果 retrievedDocumentCount 约等于 outputDocumentCount,则查询引擎无需扫描许多不必要的文档。

最大限度地减少跨分区查询

Azure Cosmos DB 使用 分区来扩展各个容器,以应对请求单位和数据存储需求的增加。 每个物理分区具有不同的独立索引。 如果查询具有匹配容器分区键的相等性筛选器,则你只需检查相关分区的索引。 这种优化可以减少查询所需的 RU 总数。 详细了解分区内查询与跨分区查询之间的差异

如果您配置的 RU 数量很大(超过 30,000)或存储的数据量很大(超过约 100 GB),那么您的容器可能已经足够大,可以看到查询 RU 费用的显著降低。

你可以检查 shardInformation 数组,以了解每个单独物理分区的查询指标。 唯一 shardKeyRangeId 值的数目是需要在其中执行查询的物理分区的数目。 在此示例中,查询是在四个物理分区上执行的。 请务必明白,执行与索引利用完全无关。 换句话说,跨分区查询仍可使用索引。

  "shardInformation" : [ 
                    {
                        "activityId" : "42f670a8-a201-4c58-8023-363ac18d9e18",
                        "shardKeyRangeId" : "5",
                        "durationMS" : 24.3859,
                        "preemptions" : 1,
                        "outputDocumentCount" : 463,
                        "retrievedDocumentCount" : 463
                    }, 
                    {
                        "activityId" : "a8bf762a-37b9-4c07-8ed4-ae49961373c0",
                        "shardKeyRangeId" : "2",
                        "durationMS" : 35.8328,
                        "preemptions" : 1,
                        "outputDocumentCount" : 905,
                        "retrievedDocumentCount" : 905
                    }, 
                    {
                        "activityId" : "3754e36b-4258-49a6-8d4d-010555628395",
                        "shardKeyRangeId" : "1",
                        "durationMS" : 67.3969,
                        "preemptions" : 1,
                        "outputDocumentCount" : 1479,
                        "retrievedDocumentCount" : 1479
                    }, 
                    {
                        "activityId" : "a69a44ee-db97-4fe9-b489-3791f3d52878",
                        "shardKeyRangeId" : "0",
                        "durationMS" : 185.1523,
                        "preemptions" : 1,
                        "outputDocumentCount" : 867,
                        "retrievedDocumentCount" : 867
                    }
                ]

可降低查询延迟的优化

在许多情况下,当查询延迟仍然过高时,RU 费用是可接受的。 以下部分概述了降低查询延迟的提示。 如果对同一个数据集多次运行同一个查询,该查询通常每次都会产生相同的 RU 开销。 但是,每次执行查询时,查询延迟可能各不相同。

提高邻近度

从与 Azure Cosmos DB 帐户不同的区域运行的查询的延迟将高于在同一区域中运行的查询。 例如,如果你在台式计算机上运行代码,预期延迟会比从与 Azure Cosmos DB 位于同一 Azure 区域的虚拟机发出的查询高出几十毫秒甚至几百毫秒(或更多)。 在 Azure Cosmos DB 中全局分发数据非常简单,以确保能够让数据更接近您的应用。

增加预配的吞吐量

在 Azure Cosmos DB 中,预配的吞吐量以请求单位(RU)度量。 假设某个查询消耗 5 RU 吞吐量。 如果预配 1000 RU,则每秒可以运行该查询 200 次。 如果在没有足够的可用吞吐量时尝试运行查询,Azure Cosmos DB 将限制请求。 Azure Cosmos DB 的用于 MongoDB 的 API 会在等待一段时间后自动重试此查询。 受限制的请求需要花费更长的时间,因此增加预配的吞吐量可以改进查询延迟。

提高吞吐量时,estimatedDelayFromRateLimitingInMilliseconds 值提供了一个关于潜在的延迟益处的感觉。

后续步骤