优化 Azure Cosmos DB 的查询性能

适用范围: NoSQL

Azure Cosmos DB 提供了一个用于查询数据的 NoSQL API,不需要使用架构或辅助索引。 本文为开发者提供了以下信息:

  • 有关 Azure Cosmos DB 的 SQL 查询执行如何工作的概要详细信息
  • 有关查询性能的提示和最佳做法
  • 有关如何利用 SQL 查询执行指标来调试查询性能的示例

关于 SQL 查询执行

在 Azure Cosmos DB 中,数据存储在容器中,支持将容器扩展到任意存储大小或请求吞吐量。 Azure Cosmos DB 可在后台无缝地在物理分区之间缩放数据,以应对数据增长或预配吞吐量的提升。 可以使用 REST API 或受支持的 SQL SDK 之一向任何容器发出 SQL 查询。

分区的简要概述:定义一个分区键(例如“city”),它决定了如何在物理分区之间拆分数据。 属于单个分区键(例如,“city”==“Seattle”)的数据会存储在物理分区内,并且单个物理分区可以存储来自多个分区键的数据。 当分区达到其存储限制时,服务会将分区无缝拆分为两个新分区。 数据均匀分布在新分区上,可将单个分区键的所有数据保存在一起。 因为分区是暂时的,因此,API 使用“分区键范围”的抽象,它表示分区键哈希的范围。

当向 Azure Cosmos DB 发出查询时,SDK 执行以下逻辑步骤:

  • 分析 SQL 查询来确定查询执行计划。
  • 如果查询包括一个针对分区键的筛选器,例如 SELECT * FROM c WHERE c.city = "Seattle",则系统会将其路由到单个分区。 如果查询没有针对分区键的筛选器,则系统会在所有分区中执行该查询,并会在客户端合并每个分区的结果。
  • 查询将根据客户端配置在每个分区内串行或并行执行。 在每个分区内,查询可能会进行一次或多次往返,具体取决于查询复杂性、所配置的页面大小和预配的集合吞吐量。 每个执行都返回由查询执行使用的请求单位数以及查询执行统计信息。
  • SDK 对跨分区的查询结果进行汇总。 例如,如果查询涉及跨分区的 ORDER BY,则会对来自各个分区的结果进行合并排序以返回多区域排序的结果。 如果查询是类似于 COUNT 的聚合,则会对来自各个分区的计数进行求和以生成总数。

SDK 针对查询执行提供了各种选项。 例如,在 .NET 中,QueryRequestOptions 类中提供了以下选项。 下表介绍了这些选项以及它们对查询执行时间的影响。

选项 说明
EnableScanInQuery 只有针对所请求的筛选器路径禁用了索引编制时才适用。 如果你选择退出编制索引并希望使用完整扫描运行查询,则必须设置为 true。
MaxItemCount 到服务器的每次往返要返回的最大项数。 可以将它设置为 -1,以便服务器管理要返回的项目数。
MaxBufferedItemCount 并行查询期间可在客户端缓冲的最大项目数。 正属性值会将缓冲项目的数量限制为所设置的值。 可以将它设置为小于 0,以便系统自动决定要缓冲的项目数。
MaxConcurrency 获取或设置并行执行查询期间客户端运行的并发操作数。 属性值为正会将并发操作数限制为所设置的值。 可以将它设置为小于 0,以便系统自动决定要运行的并发操作数。
PopulateIndexMetrics 启用索引指标集合 ,以了解查询引擎如何使用现有索引,以及如何使用潜在的新索引。 此选项会产生开销,因此仅应在调试慢查询时加以启用。
ResponseContinuationTokenLimitInKb 可以限制服务器返回的继续标记的最大大小。 如果应用程序主机对响应头大小有限制,则可能需要设置此值,但它会增加查询的总持续时间和消耗的 RU。

例如,以下是使用 .NET SDK 对按 /city 分区的容器的查询:

QueryDefinition query = new QueryDefinition("SELECT * FROM c WHERE c.city = 'Seattle'");
QueryRequestOptions options = new QueryRequestOptions()
{
    MaxItemCount = -1,
    MaxBufferedItemCount = -1,
    MaxConcurrency = -1,
    PopulateIndexMetrics = true
};
FeedIterator<dynamic> feedIterator = container.GetItemQueryIterator<dynamic>(query);

FeedResponse<dynamic> feedResponse = await feedIterator.ReadNextAsync();

每个查询执行对应于一个 REST API POST,其中包含为查询请求选项设置的标头和正文中的 SQL 查询。 有关 REST API 请求标头和选项的详细信息,请参阅使用 REST API 查询资源

有关查询性能的最佳做法

通常情况下,以下因素对 Azure Cosmos DB 的查询性能影响最大。 本文更深入发掘了其中的每一个因素。

因子 提示
预配的吞吐量 度量每个查询的 RU,并确保你具有查询所需的预配吞吐量。
分区和分区键 支持在查询的筛选器子句中使用分区键值以降低延迟。
SDK 和查询选项 遵循 SDK 最佳做法(例如直接连接)并优化客户端查询执行选项。
网络延迟 在与 Azure Cosmos DB 帐户相同的区域中运行应用程序,以尽可能减少延迟。
索引编制策略 确保具有查询所需的索引路径/策略。
查询执行指标 对查询执行指标进行分析来查明潜在的查询和数据形状重写。

预配的吞吐量

在 Azure Cosmos DB 中,创建数据容器,每个容器都具有以每秒请求单位 (RU) 数表示的预留吞吐量。 读取 1-KB 文档为 1 个 RU,每个操作(包括查询)都根据其复杂性规范化为固定数量的 RU。 例如,如果你为容器预配了 1000 RU/s,并且你具有使用 5 个 RU 的类似于 SELECT * FROM c WHERE c.city = 'Seattle' 的查询,则每秒可以执行 200 个这样的查询((1000 RU/s) / (5 RU/查询) = 200 查询/s)。

如果你每秒提交超过 200 个查询(或使所有预配置 RU 饱和的某些其他操作),该服务将开始对传入请求进行速率限制。 SDK 会通过执行回退/重试自动处理速率限制,因此你可能会注意到这些查询的延迟较高。 将预配的吞吐量提高到所需的值可以改进查询延迟和吞吐量。

若要了解请求单位的详细信息,请参阅请求单位

分区和分区键

使用 Azure Cosmos DB,系统会按照通常情况下最快/最高效到最慢/最低效的顺序对数据读取方案进行排序。

  • 获取单个分区键和项目 ID,也称为点读取
  • 具有针对单个分区键的筛选器子句的查询
  • 对任意属性使用等式或范围筛选器子句进行查询
  • 没有筛选器的查询

需要在所有分区上执行的查询延迟较高,并且可能消耗较多的 RU。 因为每个分区都具有针对所有属性的自动索引编制功能,因此,在这种情况下,可以基于索引高效地执行查询。 可以通过使用并行度选项使跨分区的查询更快地执行。

若要了解有关分区和分区键的详细信息,请参阅在 Azure Cosmos DB 中进行分区

SDK 和查询选项

请参阅查询性能提示性能测试,了解如何使用 SDK 从 Azure Cosmos DB 获得最佳客户端性能。

网络延迟

请参阅 Azure Cosmos DB 多区域分发,了解如何设置多区域分发以及如何连接到最近的区域。 当需要进行多次往返或需要通过查询检索大型结果集时,网络延迟对查询性能有显著影响。

可以使用查询执行指标来检索查询的服务器执行时间,从而区分查询执行所用时间与网络传输所用时间。

索引编制策略

如要了解索引编制路径、种类和模式以及它们对查询执行有何影响,请参阅配置索引编制策略。 默认情况下,Azure Cosmos DB 对所有数据应用自动索引,并对字符串和数字使用范围索引,这对于相等条件查询非常有效。 对于高性能插入方案,考虑排除路径以降低每个插入操作的 RU 成本。

可以使用索引指标来标识每个查询使用的索引,以及是否有任何缺失的索引可以提高查询性能。

查询执行指标

系统会为诊断请求中的每个查询执行返回详细的指标。 这些指标描述了查询执行期间的时间花费情况,并支持高级故障排除。

详细了解如何获取查询指标

指标 计价单位 说明
TotalTime 毫秒 总查询执行时间
DocumentLoadTime 毫秒 加载文档所用时间
DocumentWriteTime 毫秒 编写和序列化输出文档所用时间
IndexLookupTime 毫秒 在物理索引层中花费的时间
QueryPreparationTime 毫秒 准备查询所用时间
RuntimeExecutionTime 毫秒 总查询运行时执行时间
VMExecutionTime 毫秒 查询运行时执行查询所用时间
OutputDocumentCount count 结果集中的输出文档数
OutputDocumentSize count 输出文档的总大小(以字节为单位)
RetrievedDocumentCount count 检索的文档总数
RetrievedDocumentSize 字节 检索的文档的总大小(字节)
IndexHitRatio 比率 [0,1] 由筛选器匹配出的文档数与加载的文档数的比率

客户端 SDK 可以在内部执行多个查询操作来为每个分区中的查询提供服务。 如果结果总数超出了请求选项的最大项目数、查询超出了分区的预配吞吐量,或者查询有效负载达到了每页最大大小或查询达到了系统分配的超时限制,则客户端会对每个分区进行多次调用。 每一部分查询执行都会为该页返回一个查询指标。

下面提供了一些示例查询,并说明了如何解释从查询执行返回的某些指标:

查询 示例指标 说明
SELECT TOP 100 * FROM c "RetrievedDocumentCount": 101 为匹配 TOP 子句而检索的文档数为 100+1。 由于系统会执行扫描,查询时间主要花费在 WriteOutputTimeDocumentLoadTime 方面。
SELECT TOP 500 * FROM c "RetrievedDocumentCount": 501 RetrievedDocumentCount 现在较高(为匹配 TOP 子句为 500+1)。
SELECT * FROM c WHERE c.N = 55 "IndexLookupTime": "00:00:00.0009500" IndexLookupTime 中花费的用于查找键的时间大约为 0.9 毫秒,因为它是针对 /N/? 的索引查找。
SELECT * FROM c WHERE c.N > 55 "IndexLookupTime": "00:00:00.0017700" IndexLookupTime 中花费的时间(1.7 毫秒)稍高于范围扫描,因为它是针对 /N/? 的索引查找。
SELECT TOP 500 c.N FROM c "IndexLookupTime": "00:00:00.0017700" DocumentLoadTime 上花费的时间与前面的查询相同,但 DocumentWriteTime 较低,因为我们仅对一个属性进行了投影。
SELECT TOP 500 udf.toPercent(c.N) FROM c "RuntimeExecutionTime": "00:00:00.2136500" RuntimeExecutionTime 中对 c.N 的每个值执行 UDF 时花费的时间大约为 213 毫秒。
SELECT TOP 500 c.Name FROM c WHERE STARTSWITH(c.Name, 'Den') "IndexLookupTime": "00:00:00.0006400", "RuntimeExecutionTime": "00:00:00.0074100" IndexLookupTime 中在 /Name/? 上花费的时间大约为 0.6 毫秒。 大多数查询执行时间花费在了 RuntimeExecutionTime 中(~7 毫秒)。
SELECT TOP 500 c.Name FROM c WHERE STARTSWITH(LOWER(c.Name), 'den') "IndexLookupTime": "00:00:00", "RetrievedDocumentCount": 2491, "OutputDocumentCount": 500 查询是作为扫描执行的,因为它使用了 LOWER,并且返回了所检索的 2491 个文档中的 500 个。

后续步骤