使用 .NET SDK 获取 SQL 查询执行指标并分析查询性能

适用范围: NoSQL

本文介绍如何使用从 .NET SDK 检索到的 ServerSideCumulativeMetrics 分析 Azure Cosmos DB 上的 SQL 查询性能。 ServerSideCumulativeMetrics 是一个强类型化对象,其中包含有关后端查询执行的信息。 它包含了跨请求的所有物理分区聚合的累积指标、每个物理分区的指标列表和请求总费用。 优化查询性能一文中更详细地阐述了这些指标。

获取查询指标

版本 3.36.0 开始,查询指标作为 .NET SDK 中的强类型对象提供。 在此版本之前,或者如果使用其他 SDK 语言,可以通过分析 Diagnostics 来检索查询指标。 以下代码示例演示了如何从 FeedResponse 中的 Diagnostics 检索 ServerSideCumulativeMetrics

CosmosClient client = new CosmosClient(myCosmosEndpoint, myCosmosKey);
Container container = client.GetDatabase(myDatabaseName).GetContainer(myContainerName);

QueryDefinition query = new QueryDefinition("SELECT TOP 5 * FROM c");
FeedIterator<MyClass> feedIterator = container.GetItemQueryIterator<MyClass>(query);

while (feedIterator.HasMoreResults)
{
    // Execute one continuation of the query
    FeedResponse<MyClass> feedResponse = await feedIterator.ReadNextAsync();

    // Retrieve the ServerSideCumulativeMetrics object from the FeedResponse
    ServerSideCumulativeMetrics metrics = feedResponse.Diagnostics.GetQueryMetrics();
}

还可以使用 ToFeedIterator() 方法从 LINQ 查询的 FeedResponse 中获取查询指标:

FeedIterator<MyClass> feedIterator = container.GetItemLinqQueryable<MyClass>()
    .Take(5)
    .ToFeedIterator();

while (feedIterator.HasMoreResults)
{
    FeedResponse<MyClass> feedResponse = await feedIterator.ReadNextAsync();
    ServerSideCumulativeMetrics metrics = feedResponse.Diagnostics.GetQueryMetrics();
}

累积指标

ServerSideCumulativeMetrics 包含一个 CumulativeMetrics 属性,它用于表示在单次往返的所有分区上聚合的查询指标。

// Retrieve the ServerSideCumulativeMetrics object from the FeedResponse
ServerSideCumulativeMetrics metrics = feedResponse.Diagnostics.GetQueryMetrics();

// CumulativeMetrics is the metrics for this continuation aggregated over all partitions
ServerSideMetrics cumulativeMetrics = metrics.CumulativeMetrics;

还可以跨所有往返聚合查询的这些指标。 以下示例演示了如何使用 LINQ 跨给定查询的所有往返聚合查询执行时间:

QueryDefinition query = new QueryDefinition("SELECT TOP 5 * FROM c");
FeedIterator<MyClass> feedIterator = container.GetItemQueryIterator<MyClass>(query);

List<ServerSideCumulativeMetrics> metrics = new List<ServerSideCumulativeMetrics>();
TimeSpan cumulativeTime;
while (feedIterator.HasMoreResults)
{
    // Execute one continuation of the query
    FeedResponse<MyClass> feedResponse = await feedIterator.ReadNextAsync();

    // Store the ServerSideCumulativeMetrics object to aggregate values after all round trips
    metrics.Add(feedResponse.Diagnostics.GetQueryMetrics());
}

// Aggregate values across trips for metrics of interest
TimeSpan totalTripsExecutionTime = metrics.Aggregate(TimeSpan.Zero, (currentSum, next) => currentSum + next.CumulativeMetrics.TotalTime);
DoSomeLogging(totalTripsExecutionTime);

已分区指标

ServerSideCumulativeMetrics 包含一个 PartitionedMetrics 属性,它是往返的每个分区指标列表。 如果在单次往返中到达了多个物理分区,则每个分区的指标将显示在列表中。 已分区指标表示为 ServerSidePartitionedMetrics,其中每个物理分区和该分区的请求费用具有唯一标识符。

// Retrieve the ServerSideCumulativeMetrics object from the FeedResponse
ServerSideCumulativeMetrics metrics = feedResponse.Diagnostics.GetQueryMetrics();

// PartitionedMetrics is a list of per-partition metrics for this continuation
List<ServerSidePartitionedMetrics> partitionedMetrics = metrics.PartitionedMetrics;

在所有往返中累积时,通过每分区指标可以查看与其他分区相比时特定分区是否会导致性能问题。 下面是如何使用 LINQ 对每个行程的分区指标进行分组的示例:

QueryDefinition query = new QueryDefinition("SELECT TOP 5 * FROM c");
FeedIterator<MyClass> feedIterator = container.GetItemQueryIterator<MyClass>(query);

List<ServerSideCumulativeMetrics> metrics = new List<ServerSideCumulativeMetrics>();
while (feedIterator.HasMoreResults)
{
    // Execute one continuation of the query
    FeedResponse<MyClass> feedResponse = await feedIterator.ReadNextAsync();

    // Store the ServerSideCumulativeMetrics object to aggregate values after all round trips
    metrics.Add(feedResponse.Diagnostics.GetQueryMetrics());
}

// Group metrics by partition key range id
var groupedPartitionMetrics = metrics.SelectMany(m => m.PartitionedMetrics).GroupBy(p => p.PartitionKeyRangeId);
foreach(var partitionGroup in groupedPartitionMetrics)
{
    foreach(var tripMetrics in partitionGroup)
    {
        DoSomethingWithMetrics();
    }
}

获取查询请求费用

可以捕获每个查询消耗的请求单位数,以调查高开销的查询,或者消耗了大量吞吐量的查询。 可以使用 ServerSideCumulativeMetrics 中的 TotalRequestCharge 属性获取请求总费用,或使用返回的每个 ServerSidePartitionedMetricsRequestCharge 属性查看每个分区的请求费用。

也可以使用 FeedResponse 中的 RequestCharge 属性获取请求总费用。 若要详细了解如何使用 Azure 门户和不同的 SDK 获取请求费用,请参阅查找请求单位费用一文。

QueryDefinition query = new QueryDefinition("SELECT TOP 5 * FROM c");
FeedIterator<MyClass> feedIterator = container.GetItemQueryIterator<MyClass>(query);

while (feedIterator.HasMoreResults)
{
    // Execute one continuation of the query
    FeedResponse<MyClass> feedResponse = await feedIterator.ReadNextAsync();
    double requestCharge = feedResponse.RequestCharge;

    // Log the RequestCharge how ever you want.
    DoSomeLogging(requestCharge);
}

获取查询执行时间

可以根据查询指标捕获每个行程的查询执行时间。 查看请求延迟时,必须区分来自其他延迟源的查询执行时间,例如网络传输时间。 以下示例演示了如何获取每次往返的累积查询执行时间:

QueryDefinition query = new QueryDefinition("SELECT TOP 5 * FROM c");
FeedIterator<MyClass> feedIterator = container.GetItemQueryIterator<MyClass>(query);

TimeSpan cumulativeTime;
while (feedIterator.HasMoreResults)
{
    // Execute one continuation of the query
    FeedResponse<MyClass> feedResponse = await feedIterator.ReadNextAsync();
    ServerSideCumulativeMetrics metrics = response.Diagnostics.GetQueryMetrics();
    cumulativeTime = metrics.CumulativeMetrics.TotalTime;
}

// Log the elapsed time
DoSomeLogging(cumulativeTime);

获取索引利用率

查看索引利用率有助于对慢速查询进行调试。 在返回结果集之前,无法使用索引的查询会导致对容器中的所有文档进行完全扫描。

下面是扫描查询的示例:

SELECT VALUE c.description 
FROM   c 
WHERE UPPER(c.description) = "BABYFOOD, DESSERT, FRUIT DESSERT, WITHOUT ASCORBIC ACID, JUNIOR"

此查询的筛选器使用系统函数 UPPER,该函数不是由索引提供服务。 针对大型集合执行此查询在首次延续时生成了以下查询指标:

QueryMetrics

Retrieved Document Count                 :          60,951
Retrieved Document Size                  :     399,998,938 bytes
Output Document Count                    :               7
Output Document Size                     :             510 bytes
Index Utilization                        :            0.00 %
Total Query Execution Time               :        4,500.34 milliseconds
Query Preparation Time                   :             0.2 milliseconds
Index Lookup Time                        :            0.01 milliseconds
Document Load Time                       :        4,177.66 milliseconds
Runtime Execution Time                   :           407.9 milliseconds
Document Write Time                      :            0.01 milliseconds

请注意查询指标输出中的以下值:

Retrieved Document Count                 :          60,951
Retrieved Document Size                  :     399,998,938 bytes

此查询加载了 60,951 个文档,总共为 399,998,938 字节。 加载这么多的字节会导致开销或请求单位费用增大。 它还花费了较长时间来执行查询,花费在属性上的明确总时间为:

Total Query Execution Time               :        4,500.34 milliseconds

这意味着,执行该查询花费了 4.5 秒(而且这只是第一次延续)。

若要优化此示例查询,请避免在筛选器中使用 UPPER。 在创建或更新文档时,必须插入全大写的 c.description 值。 然后,该查询将变成:

SELECT VALUE c.description 
FROM   c 
WHERE c.description = "BABYFOOD, DESSERT, FRUIT DESSERT, WITHOUT ASCORBIC ACID, JUNIOR"

现在,可以从索引为此查询提供服务。 或者,可以使用计算属性为系统函数或负载计算的结果编制索引(否则该结果将会导致完全扫描)。

若要详细了解如何优化查询性能,请参阅优化查询性能一文。

参考

后续步骤