Azure Cosmos DB SDK のクエリのパフォーマンスに関するヒント

適用対象: NoSQL

Azure Cosmos DB は、高速で柔軟性に優れた分散データベースです。待ち時間とスループット レベルが保証されており、シームレスにスケーリングできます。 Azure Cosmos DB でデータベースをスケーリングするために、アーキテクチャを大きく変更したり、複雑なコードを記述したりする必要はありません。 スケールアップとスケールダウンは、API 呼び出しを 1 回行うだけの簡単なものです。 詳細については、コンテナーのスループットのプロビジョニングまたはデータベースのスループットのプロビジョニングに関するページを参照してください。

クエリ プランの呼び出しを減らす

クエリを実行するには、クエリ プランを作成する必要があります。 これは一般に、Azure Cosmos DB ゲートウェイへのネットワーク要求を表し、クエリ操作の待機時間が増加します。 この要求をなくし、クエリ操作の待機時間を短縮するには、2 つの方法があります。

オプティミスティック直接実行を使用した単一パーティション クエリの最適化

Azure Cosmos DB NoSQL には、オプティミスティック直接実行 (ODE) と呼ばれる最適化があり、特定の NoSQL クエリの効率を向上させることができます。 具体的には、分散を必要としないクエリ (1 つの物理パーティション上で実行できるクエリや、改ページ処理を必要としない応答を持つクエリなど) です。 分散を必要としないクエリでは、クライアント側のクエリ プランの生成やクエリの書き換えなどの一部のプロセスを自信を持ってスキップできるため、クエリの待機時間と RU コストが削減されます。 要求またはクエリ自体の中でパーティション キーを指定する (または物理パーティションが 1 つしかない) 場合、かつクエリの結果に改ページ処理が必要ない場合に、ODE はクエリを改善できます。

注意

分散を必要としないクエリのパフォーマンスが向上したオプティミスティック直接実行 (ODE) と、アプリケーションをバックエンド レプリカに接続するためのパスであるダイレクト モードを混同しないでください。

ODE は、.NET SDK バージョン 3.38.0 以降で使用できるようになりました。 クエリを実行し、要求またはクエリ自体の中でパーティション キーを指定する場合、またはデータベースに物理パーティションが 1 つしかない場合、クエリ実行は ODE の利点を活用できます。 ODE を有効にするには、QueryRequestOptions で EnableOptimisticDirectExecution を true に設定します。

GROUP BY、ORDER BY、DISTINCT、および集計関数 (合計、平均、最小、最大など) を主とする単一パーティション クエリは、ODE の使用から大きな恩恵を受けることができます。 ただし、クエリが複数パーティションを対象としているか、改ページ処理を依然として必要とするシナリオでは、クエリ応答の待機時間と RU コストが ODE を使用しない場合よりも増す可能性があります。 したがって、ODE を使用する場合は、次のことを行うことをお勧めします。

  • 呼び出しまたはクエリ自体の中でパーティション キーを指定します。
  • データ サイズが増加してパーティションが分割される原因となっていないことを確認します。
  • ODE から最大限の恩恵を受けるために、クエリ結果が改ページ処理を必要としないことを確認します。

ODE の恩恵を受けることができる単純な単一パーティション クエリの例をいくつか次に示します。

- SELECT * FROM r
- SELECT * FROM r WHERE r.pk == "value"
- SELECT * FROM r WHERE r.id > 5
- SELECT r.id FROM r JOIN id IN r.id
- SELECT TOP 5 r.id FROM r ORDER BY r.id
- SELECT * FROM r WHERE r.id > 5 OFFSET 5 LIMIT 3 

データ項目の数が時間の経過と共に増加し、Azure Cosmos DB データベースがパーティションを分割する場合、単一パーティション クエリが依然として分散を必要とする場合が存在する可能性があります。 これが発生する可能性があるクエリの例には次が含まれます。

- SELECT Count(r.id) AS count_a FROM r
- SELECT DISTINCT r.id FROM r
- SELECT Max(r.a) as min_a FROM r
- SELECT Avg(r.a) as min_a FROM r
- SELECT Sum(r.a) as sum_a FROM r WHERE r.a > 0 

一部の複雑なクエリは、単一パーティションをターゲットにしている場合でも、常に分散を必要とする可能性があります。 そのようなクエリの例には次が含まれます。

- SELECT Sum(id) as sum_id FROM r JOIN id IN r.id
- SELECT DISTINCT r.id FROM r GROUP BY r.id
- SELECT DISTINCT r.id, Sum(r.id) as sum_a FROM r GROUP BY r.id
- SELECT Count(1) FROM (SELECT DISTINCT r.id FROM root r)
- SELECT Avg(1) AS avg FROM root r 

ODE は常にクエリ プランを取得できるとは限らず、結果としてサポートされていないクエリに対して不許可や無効化を行えないことに注意することが重要です。 たとえば、パーティション分割後、そのようなクエリは ODE の対象ではなくなり、その結果、クライアント側のクエリ プランの評価がこれらをブロックするため、実行されません。 互換性/サービス継続性を確保するには、ODE を使用しないシナリオで完全にサポートされている (つまり、一般的なマルチパーティション ケースで実行されて正しい結果を生成する) クエリのみが ODE で使用されるようにすることが重要です。

注意

ODE を使用すると、新しい種類の継続トークンが生成される可能性があります。 このようなトークンは、古い SDK では設計上認識されず、結果として正しくない継続トークン例外が発生する可能性があります。 新しい SDK から生成されたトークンが古い SDK によって使用されるシナリオがある場合は、次の 2 つの手順でアップグレードすることをお勧めします:

  • 新しい SDK へのアップグレードと、ODE の無効化を 1 つのデプロイの一部として実施します。 すべてのノードがアップグレードされるのを待ちます。
    • ODE を無効にするには、QueryRequestOptions で EnableOptimisticDirectExecution を false に設定します。
  • 2 番目のデプロイの一部として、すべてのノードの ODE を有効にします。

クエリ プランのローカル生成を使用する

SQL SDK には、ローカル環境でクエリを解析して最適化するためのネイティブ ServiceInterop.dll が含まれています。 ServiceInterop.dll は、Windows x64 プラットフォームでのみサポートされています。 次の種類のアプリケーションでは、既定で 32 ビットのホスト処理が使用されます。 ホスト処理を 64 ビット処理に変更するには、アプリケーションの種類に基づいて次の手順のようにします。

  • 実行可能なアプリケーションの場合は、[プロジェクトのプロパティ] ウィンドウの [ビルド] タブで [プラットフォーム ターゲット][x64] に設定することで、ホスト処理を変更できます。

  • VSTest ベースのテスト プロジェクトの場合は、Visual Studio の [テスト] メニューで [テスト]>[テストの設定]>[既定のプロセッサ アーキテクチャ] > [X64] の順に選択することで、ホスト処理を変更できます。

  • ローカルでデプロイされた ASP.NET Web アプリケーションの場合は、 [ツール]>[オプション]>[プロジェクトおよびソリューション]>[Web プロジェクト] の順に選択して、 [Web サイトおよびプロジェクト用 IIS Express の 64 ビット バージョンを使用する] をオンにすることで、ホスト処理を変更できます。

  • Azure にデプロイされた ASP.NET Web アプリケーションの場合は、Azure portal の [アプリケーションの設定]64 ビット プラットフォームを選択することで、ホスト処理を変更できます。

注意

既定では、新しい Visual Studio プロジェクトは、 [任意の CPU] に設定されます。 x86 に切り替わらないように、プロジェクトを x64 に設定することをお勧めします。 [任意の CPU] に設定されたプロジェクトは、x86 のみの依存関係が追加されると、簡単に x86 に切り替わる可能性があります。
ServiceInterop.dll は、SDK DLL が実行されるフォルダーに配置する必要があります。 これは、手動で DLL をコピーする場合、またはカスタム ビルドおよびデプロイ システムを使用する場合にのみ、問題になります。

単一パーティション クエリを使用する

QueryRequestOptionsPartitionKey プロパティを設定することによってパーティション キーを対象とし、(Distinct、DCount、Group By を含む) 集計を含まないクエリの場合。 この例では、/state のパーティション キー フィールドが値 Washington にフィルター処理されます。

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle' AND c.state = 'Washington'"
{
    // ...
}

必要に応じて、要求オプション オブジェクトの一部としてパーティション キーを指定できます。

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("Washington")}))
{
    // ...
}

重要

Linux や MacOS などの Windows 以外の OS を実行しているクライアントでは、パーティション キーを要求オプション オブジェクトで必ず指定する必要があります。

注意

クロスパーティション クエリでは、SDK が既存のすべてのパーティションにアクセスして結果を調べる必要があります。 コンテナーの物理パーティションが多いほど、遅くなる可能性があります。

必要のない反復子を再作成しないようにする

すべてのクエリ結果が現在のコンポーネントによって使用される場合は、すべてのページの継続で反復子を作成し直す必要はありません。 改ページが別の呼び出し元コンポーネントによって制御されていない限り、クエリを完全にドレインすることを常にお勧めします。

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { PartitionKey = new PartitionKey("Washington")}))
{
    while (feedIterator.HasMoreResults) 
    {
        foreach(MyItem document in await feedIterator.ReadNextAsync())
        {
            // Iterate through documents
        }
    }
}

並列処理の次数を調整する

クエリで、QueryRequestOptionsMaxConcurrency プロパティを調整して、アプリケーションに最適な構成を識別します。これは、(パーティション キー値に対するフィルターなしで) クロス プラットフォーム クエリを実行する場合に特に当てはまります。 MaxConcurrency は、並列タスクの最大数、つまり並列でアクセスされるパーティションの最大数を制御します。 値を -1 に設定すると、SDK によって最適なコンカレンシーが決定されます。

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxConcurrency = -1 }))
{
    // ...
}

以下のように仮定します

  • D = 並列処理の既定の最大数 (= クライアント コンピューター上のプロセッサの総数)
  • P = ユーザー指定の並列タスクの最大数
  • N = クエリに応答するためにアクセスする必要があるパーティションの数

P の値が異なると、並列クエリがどのように動作するかの結果を次に示します。

  • (P == 0) => シリアル モード
  • (P == 1) => 最大 1 つのタスク
  • (P > 1) => Min (P, N) の並列タスク
  • (P < 1) => Min (N, D) の並列タスク

ページ サイズを調整する

SQL クエリを発行するとき、結果セットが大きすぎると、セグメント化された形式で結果が返されます。

注意

MaxItemCount プロパティは、改ページ位置の自動修正のみに使用しないでください。 主な用途は、1 ページに返される項目の最大数を減らすことで、クエリのパフォーマンスを向上させることです。

また、使用可能な Azure Cosmos DB SDK を使用してページ サイズを設定することもできます。 QueryRequestOptionsMaxItemCount プロパティでは、列挙操作で返される項目の最大数を設定できます。 MaxItemCount が-1 に設定されている場合、ドキュメント サイズに応じて最適な値が SDK によって自動的に見つけられます。 次に例を示します。

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxItemCount = 1000}))
{
    // ...
}

クエリが実行されると、結果のデータは TCP パケット内で送信されます。 MaxItemCount に指定した値が小さすぎると、TCP パケット内でデータを送信するために必要なトリップ数が多くなり、パフォーマンスに影響します。 そのため、MaxItemCount プロパティに設定する値がわからない場合は、-1 に設定して SDK で自動的に既定値を選択することをお勧めします。

バッファー サイズを調整する

並列クエリは、結果の現在のバッチがクライアントによって処理されている間に結果をプリフェッチするように設計されています。 このプリフェッチは、クエリの全体的な遅延の削減に役立ちます。 QueryRequestOptionsMaxBufferedItemCount プロパティは、プリフェッチされる結果の数を制限します。 MaxBufferedItemCount を、返される結果の予期される数 (またはそれ以上の数) に設定すると、クエリに対するプリフェッチの効果が最大になります。 この値を -1 に設定すると、バッファーに格納される項目の数はシステムによって自動的に決定されます。

using (FeedIterator<MyItem> feedIterator = container.GetItemQueryIterator<MyItem>(
    "SELECT * FROM c WHERE c.city = 'Seattle'",
    requestOptions: new QueryRequestOptions() { 
        PartitionKey = new PartitionKey("Washington"),
        MaxBufferedItemCount = -1}))
{
    // ...
}

プリフェッチは、並列処理の次数とは無関係に同じように動作し、すべてのパーティションからのデータに対して単一のバッファーが存在します。

次のステップ

.NET SDK を使用するときのパフォーマンスの詳細を学習するには:

クエリ プランの呼び出しを減らす

クエリを実行するには、クエリ プランを作成する必要があります。 これは一般に、Azure Cosmos DB ゲートウェイへのネットワーク要求を表し、クエリ操作の待機時間が増加します。

クエリ プランのキャッシュを使用する

1 つのパーティションを対象とするクエリのクエリ プランは、クライアントにキャッシュされます。 これにより、最初の呼び出しの後でクエリ プランを取得するためにゲートウェイを呼び出す必要がなくなります。 キャッシュされたクエリ プランで重要なのは、SQL クエリの文字列です。 クエリがパラメーター化されていることを確認する必要があります。 そうでない場合、クエリ文字列が呼び出し間で同一になる可能性が低いため、クエリ プランのキャッシュ参照でキャッシュ ミスが頻繁に発生します。 クエリ プランのキャッシュは、Java SDK バージョン 4.20.0 以降および Spring Data Azure Cosmos DB SDK バージョン 3.13.0 以降では、既定で有効になります。

パラメーター化された単一パーティションのクエリを使用する

CosmosQueryRequestOptionssetPartitionKey でパーティション キーにスコープが設定され、集計 (Distinct、DCount、Group By を含む) が含まれないパラメーター化されたクエリの場合、クエリ プランを回避できます。

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));

ArrayList<SqlParameter> paramList = new ArrayList<SqlParameter>();
paramList.add(new SqlParameter("@city", "Seattle"));
SqlQuerySpec querySpec = new SqlQuerySpec(
        "SELECT * FROM c WHERE c.city = @city",
        paramList);

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

注意

クロスパーティション クエリでは、SDK が既存のすべてのパーティションにアクセスして結果を調べる必要があります。 コンテナーの物理パーティションが多いほど、遅くなる可能性があります。

並列処理の次数を調整する

並列クエリは、複数のパーティションに並列にクエリを実行することによって機能します。 ただし、個々のパーティション分割されたコンテナーからのデータは、クエリごとに順番に取得されます。 そのため、CosmosQueryRequestOptionssetMaxDegreeOfParallelism を使って、値をパーティションの数に設定します。 パーティションの数が不明な場合は、setMaxDegreeOfParallelism を使って大きな数を設定すると、システムが並列処理の最大限度として最小値 (パーティションの数、ユーザー指定の入力) を選びます。 値を -1 に設定すると、SDK によって最適なコンカレンシーが決定されます。

並列クエリが最も有効に機能するのは、クエリに対するデータがすべてのパーティションに均等に分散している場合であることに注意する必要があります。 パーティション分割されたコンテナーが、クエリによって返されるすべてまたは大部分のデータがわずかな数のパーティション (最悪の場合は 1 つのパーティション) に集中するように分割されている場合、クエリのパフォーマンスが低下します。

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));
options.setMaxDegreeOfParallelism(-1);

// Define the query

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

以下のように仮定します

  • D = 並列処理の既定の最大数 (= クライアント コンピューター上のプロセッサの総数)
  • P = ユーザー指定の並列タスクの最大数
  • N = クエリに応答するためにアクセスする必要があるパーティションの数

P の値が異なると、並列クエリがどのように動作するかの結果を次に示します。

  • (P == 0) => シリアル モード
  • (P == 1) => 最大 1 つのタスク
  • (P > 1) => Min (P, N) の並列タスク
  • (P == -1) => 並列タスク数の Min (N, D)

ページ サイズを調整する

SQL クエリを発行するとき、結果セットが大きすぎると、セグメント化された形式で結果が返されます。 既定では、100 項目または 4 MB (先に達した方) のチャンク単位で結果が返されます。 ページ サイズを大きくすると、必要なラウンド トリップの数が減り、100 を超える項目を返すクエリのパフォーマンスが向上します。 設定する値がわからない場合、通常は 1000 をお勧めします。 ページ サイズが大きくなるとメモリ消費量が増えるので、ワークロードがメモリの影響を受けやすい場合は、より小さい値を考慮してください。

同期 API では iterableByPage()、非同期 API では byPage()pageSize パラメーターを使って、ページ サイズを定義できます。

//  Sync API
Iterable<FeedResponse<MyItem>> filteredItemsAsPages =
    container.queryItems(querySpec, options, MyItem.class).iterableByPage(continuationToken,pageSize);

for (FeedResponse<MyItem> page : filteredItemsAsPages) {
    for (MyItem item : page.getResults()) {
        //...
    }
}

//  Async API
Flux<FeedResponse<MyItem>> filteredItemsAsPages =
    asyncContainer.queryItems(querySpec, options, MyItem.class).byPage(continuationToken,pageSize);

filteredItemsAsPages.map(page -> {
    for (MyItem item : page.getResults()) {
        //...
    }
}).subscribe();

バッファー サイズを調整する

並列クエリは、結果の現在のバッチがクライアントによって処理されている間に結果をプリフェッチするように設計されています。 プリフェッチは、クエリの全体的な遅延の削減に役立ちます。 CosmosQueryRequestOptionssetMaxBufferedItemCount は、プリフェッチされる結果の数を制限します。 プリフェッチを最大化するには、maxBufferedItemCount を、pageSize より大きな数値に設定します (注: これによりメモリ使用量が増加する可能性もあります)。 プリフェッチを最小限に抑えるには、maxBufferedItemCountpageSize と等しくなるように設定します。 この値を 0 に設定すると、バッファーに格納される項目の数はシステムによって自動的に決定されます。

CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
options.setPartitionKey(new PartitionKey("Washington"));
options.setMaxBufferedItemCount(-1);

// Define the query

//  Sync API
CosmosPagedIterable<MyItem> filteredItems = 
    container.queryItems(querySpec, options, MyItem.class);

//  Async API
CosmosPagedFlux<MyItem> filteredItems = 
    asyncContainer.queryItems(querySpec, options, MyItem.class);

プリフェッチは、並列処理の次数とは無関係に同じように動作し、すべてのパーティションからのデータに対して単一のバッファーが存在します。

次のステップ

Java SDK を使用するときのパフォーマンスの詳細を学習するには:

クエリ プランの呼び出しを減らす

クエリを実行するには、クエリ プランを作成する必要があります。 これは一般に、Azure Cosmos DB ゲートウェイへのネットワーク要求を表し、クエリ操作の待機時間が増加します。 この要求を削除し、単一パーティション クエリ操作の待機時間を短縮する方法があります。 単一パーティション クエリでは、項目のパーティション キー値を指定し、それを partition_key 引数として渡します。

items = container.query_items(
        query="SELECT * FROM r where r.city = 'Seattle'",
        partition_key="Washington"
    )

ページ サイズを調整する

SQL クエリを発行するとき、結果セットが大きすぎると、セグメント化された形式で結果が返されます。 max_item_count を使用すると、列挙操作で返される項目の最大数を設定できます。

items = container.query_items(
        query="SELECT * FROM r where r.city = 'Seattle'",
        partition_key="Washington",
        max_item_count=1000
    )

次のステップ

Python SDK for API for NoSQL の使用の詳細については、以下を参照してください。:

クエリ プランの呼び出しを減らす

クエリを実行するには、クエリ プランを作成する必要があります。 これは一般に、Azure Cosmos DB ゲートウェイへのネットワーク要求を表し、クエリ操作の待機時間が増加します。 この要求を削除し、単一パーティション クエリ操作の待機時間を短縮する方法があります。 単一パーティションのクエリの場合、クエリを 1 つのパーティションにスコープ設定するには、2 つの方法があります。

パラメーター化されたクエリ式を使用して、クエリ ステートメントでパーティション キーを指定します。 このクエリはプログラムによって SELECT * FROM todo t WHERE t.partitionKey = 'Bikes, Touring Bikes' に構成されます。

// find all items with same categoryId (partitionKey)
const querySpec = {
    query: "select * from products p where p.categoryId=@categoryId",
    parameters: [
        {
            name: "@categoryId",
            value: "Bikes, Touring Bikes"
        }
    ]
};

// Get items 
const { resources } = await container.items.query(querySpec).fetchAll();

for (const item of resources) {
    console.log(`${item.id}: ${item.name}, ${item.sku}`);
}

または、FeedOptionspartitionKey を指定し、引数として渡します。

const querySpec = {
    query: "select * from products p"
};

const { resources } = await container.items.query(querySpec, { partitionKey: "Bikes, Touring Bikes" }).fetchAll();

for (const item of resources) {
    console.log(`${item.id}: ${item.name}, ${item.sku}`);
}

ページ サイズを調整する

SQL クエリを発行するとき、結果セットが大きすぎると、セグメント化された形式で結果が返されます。 maxItemCount を使用すると、列挙操作で返される項目の最大数を設定できます。

const querySpec = {
    query: "select * from products p where p.categoryId=@categoryId",
    parameters: [
        {
            name: "@categoryId",
            value: items[2].categoryId
        }
    ]
};

const { resources } = await container.items.query(querySpec, { maxItemCount: 1000 }).fetchAll();

for (const item of resources) {
    console.log(`${item.id}: ${item.name}, ${item.sku}`);
}

次のステップ

Node.js SDK for API for NoSQL の使用の詳細については、以下を参照してください。