しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

Cosmos DB のインデックスポリシーの基本と最適化の方法

Cosmos DB は RDB のようにインデックスを個別で付ける必要がなく、デフォルトのままでも十分なパフォーマンスが出るようになっていますが、最適化を行うとスループットの向上やコスト削減に繋がります。

特に最近は大量データの処理基盤として Cosmos DB を使うことが多くなってきたのですが、こういった大量データを処理するケースではインデックスポリシーの最適化はかなり効果が出ます。

例によって Cosmos DB の公式ドキュメントは充実しているので、基本から読んでおくと安心です。

ちなみにインデックスポリシーの最適化のを行う目的は、書き込みのコストとレイテンシを下げるためです。

読み込みに関しては不足しているインデックスを追加した場合には効果が出ますが、必要となる場面は限定的でしょう。最近だと複雑なクエリが必要なら Change Feed か Synapse Analytics を使うことが多いです。

良く使われるであろうインデックスポリシーの例は公式ドキュメントでも紹介されているので、最低ここを読んでおくだけでもかなり役に立つはずです。

とは言え突然サンプルが出てきても混乱の元なのと、自分のインデックスポリシーへの理解の整理も兼ねてメモしておきます。きっかけは Change Feed だったので若干偏ってはいます。

大前提:デフォルトは全プロパティにインデックスを作成する

Cosmos DB の特徴の一つとしてはデフォルトで全てのプロパティに対してインデックスが自動で作成されるので、id 以外でのプロパティでフィルタを実行しても高速という点があります。

The default indexing policy for newly created containers indexes every property of every item and enforces range indexes for any string or number.

Azure Cosmos DB indexing policies | Microsoft Docs

インデックスの作成はデフォルトでは即座に行われるので、少ないデータ量の場合はインデックスを気にせず使っても問題ありません。デフォルトの設定のままでも大抵の場合は十分です。

インデックスモードには consistentnone の設定が存在しています。昔は lazy がありましたが、今は設定できないようになっているので忘れていて良いです。

indexingMode = consistent

Cosmos DB で Container を作成した時のデフォルトが consistent になります。名前の通り Container デフォルトの Consistency Level と同じ挙動になります。

{
    "indexingMode": "consistent",
    "automatic": true,
    "includedPaths": [
        {
            "path": "/*"
        }
    ],
    "excludedPaths": [
        {
            "path": "/\"_etag\"/?"
        }
    ]
}

この時 _etag は常に自動的に除外されるようになっていて、消しても追加されます。デフォルトで includedPaths/* が追加されているので、全てのプロパティに対してインデックスが作成されます。

indexingMode = none

あまり使う機会は無いと思いますが、インデックスの作成を完全にオフにすることも出来ます。

大量データのインポート時にはインデックスを同時に作成する必要はないので、公式ドキュメントにはインポート前にオフにして完了後に戻す方法が紹介されています。

{
    "indexingMode": "none",
    "automatic": false,
    "includedPaths": [],
    "excludedPaths": []
}

後は id のみを使うアプリケーションの場合はオフに出来ますが、Cosmos DB を使うメリットがかなり減ってしまうので、データのインポート時や後述する Change Feed から使う場合ぐらいかなと思います。

ここまでを頭に入れておきつつ、アプリケーションからどのように使っているかを考えると、インデックスポリシーの最適化が行えるようになるはずです。

必要なプロパティへのインデックスだけ作成する

インデックスポリシー最適化の基本は必要なプロパティに対してのみインデックスを付けるという、極々当たり前の方法となります。クエリで使われないプロパティに対してインデックスを作成するのは、RU とストレージの無駄になるので避けた方が良いです。

公式ドキュメントのパフォーマンス Tips にも書かれているように、インパクトはかなり大きいです。

Cosmos DB はスキーマレスで、データを JSON として扱うストレージなので RDB とは異なり、オプトインとオプトアウトを組み合わせてインデックスを作り上げていく形になります。

オプトインする場合の例

特定のプロパティしかクエリで使われないことがわかっている場合は、オプトインでインデックスを定義するのが簡単です。プロパティを 1 つだけ含める、特定のプロパティ以下を含めるといった書き方が出来ます。

{
    "indexingMode": "consistent",
    "automatic": true,
    "includedPaths": [
        {
            "path": "/user/?"
        },
        {
            "path": "/items/*"
        }
    ],
    "excludedPaths": [
        {
            "path": "/*"
        }
    ]
}

書き方は若干特殊な感じはしますが /? で終わる場合はそのプロパティだけ、/* で終わる場合はそれ以下をすべて含むという意味になります。

オプトアウトする場合の例

逆に除外したいプロパティが明確に定まっている場合には、オプトアウトでインデックスを定義します。

例えばセンサーなどから取得された計測値などインデックスが完全に不要なデータは、プロパティを一つ追加してルート直下から外すようにしておくと簡単です。

{
    "indexingMode": "consistent",
    "automatic": true,
    "includedPaths": [
        {
            "path": "/*"
        }
    ],
    "excludedPaths": [
        {
            "path": "/values/*"
        },
        {
            "path": "/\"_etag\"/?"
        }
    ]
}

この場合は values プロパティ以下を全て除外しています。計測値はデータ量が多くなりがちなのに対し、それ以外のデータは少ないことが多いので、インデックスに含めても影響は無視できる範囲でしょう。

複合インデックスの追加を検討する

個人的には最近 Cosmos DB の SQL で複雑なクエリを書くことがないのであまり使わないのですが、複数のプロパティに対して ORDER BY や WHERE でのフィルタリングを実行している場合には、複合インデックスを追加すると RU を下げられる可能性があります。

Queries that have an ORDER BY clause with two or more properties require a composite index. You can also define a composite index to improve the performance of many equality and range queries. By default, no composite indexes are defined so you should add composite indexes as needed.

Azure Cosmos DB indexing policies | Microsoft Docs

デフォルトで作成されるインデックスは単一のプロパティに対してのものになるので、アプリケーションからフィルタリング対象となるプロパティや並べ替えが行われることがわかっている場合は、複合インデックスの追加を検討するのが良いでしょう。

公式ブログでも以前にどういった場合に複合インデックスを使うのが効果的か、という点で紹介されています。目を通しておくのをお勧めします。

複雑なクエリに関しては Change Feed で最適な形を持った別 DB を作成するか、Synapse Analytics を使って実行する方が良いという点は変わりません。

TTL を使う場合にはインデックスが必要

一定期間過ぎると自動で項目を消してくれる TTL は便利なので結構使っていますが、Cosmos DB の TTL 機能はインデックスに依存しているらしく、利用する場合には必ずインデックスが必要となります。

f:id:shiba-yan:20201123004840p:plain

インデックスポリシーが indexingMode = none の場合には TTL を設定しようとしてもエラーとなります。

なので TTL を使う必要はありつつ、インデックス自体は必要ない場合には excludedPaths/* を追加して対応します。これはドキュメントにも記載されている方法となります。

For scenarios where no property path needs to be indexed, but TTL is required, you can use an indexing policy with an indexing mode set to consistent, no included paths, and /* as the only excluded path.

Azure Cosmos DB indexing policies | Microsoft Docs

TTL を使いつつインデックスは必要ない場合のインデックスポリシーは以下のようになります。

{
    "indexingMode": "consistent",
    "automatic": true,
    "includedPaths": [],
    "excludedPaths": [
        {
            "path": "/*"
        },
        {
            "path": "/\"_etag\"/?"
        }
    ]
}

イベントで何回か紹介したことはありますが、大体はデモ中の説明だったのてテキストとして残します。

Change Feed 向けにインデックス設定を最適化する

ここまでクエリを使う前提でインデックスポリシーの最適化について書いてきましたが、Change Feed の場合は若干考え方が異なります。Change Feed ではクエリやインデックスを使うことなく、継続トークンだけを使って処理を行っているのでインデックスは完全に不要です。

この辺りは確証が持てなかったので Cosmos DB チームの Mark に聞いたところ、Change Feed のみで使う場合にはインデックスを完全にオフにすることが出来るとの回答を貰いました。

ただし Change Feed と TTL を組み合わせて使っている場合には、前述した制限によってインデックスが必要になるので、excludedPaths/* を追加したインデックスポリシーを使います。

ちなみに Change Feed の読み込みコストはドキュメントの読み込みと同じらしいです。クエリではないので RU の消費の予測が立てやすいのは良いですね。

これまで紹介した内容を実際に稼働中のアプリケーションに適用していますが、結果として書き込み RU を 1/5 ほどに削減しつつ、同時にスループットも大幅に改善することが出来ました。全体の RU は変えていないのでコストはそのままです。

ぶっちゃけ Change Feed 部分のインデックス最適化について書きたいだけでしたが、実際のアプリケーションでパフォーマンスが思ったより出なかった場合に参考にしてもらえればと思います。