SQL クエリの実行メトリックを取得し、.NET SDK を使用してクエリのパフォーマンスを分析する

適用対象: NoSQL

この記事では、.NET SDK から取得した ServerSideCumulativeMetrics を使って、Azure Cosmos DB で SQL クエリのパフォーマンスをプロファイルする方法について説明します。 ServerSideCumulativeMetrics は、バックエンド クエリの実行に関する情報を含む、厳密に型指定されたオブジェクトです。 これには、要求のすべての物理パーティションにわたって集計された累積メトリック、各物理パーティションのメトリックの一覧、要求の合計料金が含まれます。 これらのメトリックの詳細については、クエリ パフォーマンスのチューニングに関する記事を参照してください。

クエリのメトリックを取得する

クエリ メトリックは、バージョン 3.36.0 以降、.NET SDK で厳密に型指定されたオブジェクトとして使用できます。 これより前のバージョン、または別の SDK 言語を使っている場合は、Diagnostics を解析することでクエリ メトリックを取得できます。 次のコード サンプルは、FeedResponseDiagnostics から 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 には、1 回のラウンド トリップのすべてのパーティションにわたって集計されたクエリ メトリックを表す 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(response.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 プロパティが含まれています。 1 回のラウンド トリップで複数の物理パーティションに到達した場合、それらのそれぞれのメトリックが一覧に表示されます。 パーティション分割されたメトリックは、物理パーティションごとに一意の識別子と、そのパーティションの要求料金と共に 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(response.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();
    }
}

クエリ要求の料金を取得する

各クエリで使用された要求単位をキャプチャして、コストの高いクエリや高スループットを消費するクエリを調査することができます。 ServerSideCumulativeMetricsTotalRequestCharge プロパティを使用して要求の合計料金を取得できます。あるいは、返される各 ServerSidePartitionedMetricsRequestCharge プロパティを使用して各パーティションからの要求料金を確認できます。

要求の合計料金は FeedResponseRequestCharge プロパティからも確認できます。 Azure portal と各種の 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

このクエリでは、合計で 399,998,938 バイトになる 60,951 のドキュメントが読み込まれました。 この大量のバイトの読み込みは、高いコストや要求ユニット使用量につながります。 さらに、クエリの実行に長い時間がかかりますが、これは、次の費やされた時間の合計を示すプロパティによって明確に示されています。

Total Query Execution Time               :        4,500.34 milliseconds

これは、クエリの実行に 4.5 秒かかったことを意味しています (しかもこれは 1 つの継続だけのものです)。

この例のクエリを最適化するには、フィルターで UPPER を使用しないようにします。 代わりに、ドキュメントが作成または更新されたときに、すべての大文字に c.description 値を挿入する必要があります。 クエリは次のようになります。

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

これで、このクエリでは、インデックスからサービスが提供されるようになりました。 または、コンピューティング プロパティを使って、フル スキャンが必要となるシステム関数や複雑なコンピューティングの結果にインデックスを付けることもできます。

クエリ パフォーマンスのチューニングの詳細については、クエリ パフォーマンスのチューニングに関する記事を参照してください。

リファレンス

次の手順