こんにちは、サイオステクノロジーの佐藤 陽です。
今回ははCosmosDBのインデックス機能についてご紹介したいと思います。
RDBにおいても登場するインデックスですが、CosmosDBにおいても重要な概念です。
ただRDBとは設定方法などは異なってくるため、CosmosDBにおけるインデックスについてご紹介していきたいと思います。
はじめに
CosmosDBのインデックスについてご紹介していきます。
インデックスを正しく設定・使用することで、検索速度の向上や、使用するRUの削減にもつながります。
CosmosDBにおいて
- どのようにインデックスが構築されるのか
- どのようにインデックスが使われるのか
について見ていきたいと思います。
インデックスの作成
CosmosDBにおいては、アイテムが追加・削除などされたタイミングでインデックスが自動で設定されます。
そのため基本的に何も設定しなくても高い検索パフォーマンスが得られます。
逆インデックス
CosmosDBにおいては、格納されるアイテムを元にツリーが形成され、そのツリーを元に逆インデックスというものが構築されます。
これはアイテムが追加されるたびに行われ、常にツリーの内容に反映されます。
と言われてもピンとこない気がするので、
前回まで扱っていた値(を少し修正したもの)を例にして提示したいと思います。
以下のデータがCosmosDBにおいて格納されているとします。
{
"id": "1",
"name": "寿司",
"price": 2000,
"ingredients": [
{"name":"米"},
{"name":"魚"}]
},
{
"id": "2",
"name": "うどん",
"price": 500,
"ingredients": [
{"name":"小麦"},
{"name":"塩"},
{"name":"水"}]
},
{
"id": "3",
"name": "パスタ",
"price": 1000,
"ingredients": [
{"name":"デュラム小麦"},
{"name":"塩"},
{"name":"水"}]
}
その場合、これらのアイテムは以下のようなツリーで表示されます。
配列を持つアイテムに関しては、配列内のそのエントリのインデックス (0、1 など) でラベル付けされた中間ノードを取得します。
これをベースとして、逆インデックスを作成していきます。
まず、各アイテムのidがツリーにおけるどのルートに対応するかを見ていきます。
全てのルートに対して図示すると見づらいので、Ingredientsのノードだけ確認します。
ここで①、②、③はそれぞれ各アイテムのidを表します。
例えばroot/ingredients/1/name/塩
というルートはうどん
,パスタ
のアイテムの値が存在しているため②,③のマーキングがしてあります。
このツリーを元に、逆インデックス表を作成します。
(あくまでイメージです)
パス | 値 | id |
---|---|---|
/name | 寿司 | 1 |
/name | うどん | 2 |
/name | パスタ | 3 |
/price | 500 | 2 |
/price | 1000 | 3 |
/price | 2000 | 1 |
/ingredients/0/name | 米 | 1 |
/ingredients/0/name | 小麦 | 2 |
/ingredients/0/name | デュラム小麦 | 3 |
/ingredients/1/name | 魚 | 1 |
/ingredients/1/name | 塩 | 2,3 |
/ingredients/2/name | 水 | 2,3 |
この逆インデックス表がどのように使われるのかというと
例えば、うどん
という名前の料理を取得したい場合は以下のクエリを実行します。
select * from c where c.name = "うどん"
この時、クエリエンジンは各アイテム(寿司、うどん、パスタ)を個別には検索しません。
逆インデックス表から
「name=うどん」ということは、idが1のアイテムだな
という事がすぐに判明するため、id=1のアイテムのみを取得します。
…といったものが全体像になります。
ここからインデックスを用いた検索についてもう少し詳しく見ていきます。
検索
検索の流れとしては、
- 検索エンジンがクエリのフィルターを評価する
- 作成したツリーを利用してルートからプロパティまで走査する
- 該当するアイテムを返す
といった流れです。
検索エンジンは、次に記載する評価方法のうち最も効率的なものを自動で選択し、評価を行います。
なお、これらの評価方法一覧は公式ドキュメントにまとまってるので、概要は一旦こちらで確認してもらって
本記事ではもう少し具体的な例に落とし込んで紹介したいと思います。
インデックスシーク
インデックスシークは最も効率の良い検索方法になります。
クエリエンジンが、指定されたフィールド値との完全一致したもののみを検索します。
先程挙げた例となりますが
以下のようなクエリの場合はnameがうどん
と完全一致するものだけを検索すればよいので、インデックスシークが行われます。
select * from c where c.name = "うどん"
また、検索するものが2つ以上ある場合でもインデックスシークが行われます。
これはうどん
,パスタ
それぞれの値に対する完全一致のみを検索しているためです。
select * from c where c.name IN ("うどん", "パスタ")
検索する際に、完全一致か否かが判断のポイントかと思います。
インデックススキャン
インデックススキャンは、クエリエンジンが対象となフィールドに対して、可能性のある全ての値を見つけるよう検索します。
先程のインデックスと比較して完全一致のものだけを探すのではなく、可能性があるものを操作することがポイントです。
また、インデックススキャンに関してもは、更に3つに細かく分類されるため、それぞれの値を見ていきたいと思います。
正確なインデックス スキャン
以下のようなクエリが対象です。
select * from c where c.price >= 1000
この場合、priceというフィールドの値に対して比較を行います。
この比較の処理を、ドキュメントではバイナリ検索と定義しています。
このバイナリ検索によって、複雑さが増し、インデックス シークから正確なインデックス スキャンへと変化しました。
そしてここでポイントなのですが、検索時に用いられる逆インデックスにおいて、一部の値に関しては昇順に並び替えられています。
パス | 値 | id |
---|---|---|
/price | 500 | 2 |
/price | 1000 | 3 |
/price | 2000 | 1 |
上記した表から抜粋
つまり、今回であれば1000
という値を見つけた後は、逆インデックス表を昇っていくだけで値を取得することが可能です。
一方でこういったクエリも正確なインデックススキャンの対象となります。
select * from c where startswith(c.name, "うど")
ここからは公式ドキュメントに明言されてなかった部分なので、考察になるのですが
文字列に関しても、数値と同様に逆インデックス上においては昇順に並び替えられるのだと思います。
(そのため、上記に示した逆インデックスももしかしたら並び順は正確ではないかもしれません)
そのため、今回であればうど
で検索されたポイントから昇順で検索していくだけでよいので、正確なインデックススキャンが使用されると思います。
なお、これに関しては後述する拡張インデックススキャン
の例と比較すると分かりやすいかと思います。
逆インデックスに関する並び順
逆インデックスにおいては値が昇順に並んでいるという言及をしましたが、公式ドキュメントに以下の記載がありました。
逆インデックスには、2 つの重要な属性があります。
・特定のパスでは、値は昇順に並べ替えられます。 そのため、クエリ エンジンでインデックスから簡単に ORDER BY を提供できます。
・特定のパスについて、クエリ エンジンで可能な値の個別のセットをスキャンして、結果が存在するインデックス ページを識別できます。
「特定のパスでは」という表現があるので、全てではないと思うのですが、数値・文字列・日付などは昇順に並び替えられ
これらに関しては正確なインデックススキャンが利用されるのではないかと予想しています。
拡張インデックススキャン
拡張インデックススキャンに関しても、正確インデックススキャンと同じで特定のフィールドに対してバイナリ検索を行うものになります。
具体的なクエリを見てみたいと思います。
select * from c where startswith(c.name, "うど", true)
先程の正確なインデックススキャンの例のstartwithにtrue
の引数が追加されました。
これは大文字・小文字を区別するかしないかを表すパラメータです。
そのため、正直日本語の検索ワードに対しては意味ないのですが、そこは雰囲気で感じ取ってください…。
大文字小文字を区別しないことで、「正確なインデックスキャン」から「拡張インデックススキャン」になるわけですが
これが、先程言及した逆インデックス上の文字列の並び順に影響していると考えています。
逆インデックス上で文字列が昇順に並び替えられるわけですが、この時恐らく大文字と小文字は別物として並び替えられます。
つまり、大文字・小文字を区別せずに検索する場合、昇順に上がっていくだけではなく、降っていく範囲の検索も必要になると予想されます。
そのため、検索範囲が「拡張」され、拡張インデックススキャンとして検索されるのだと思われます。
フルインデックススキャン
フルインデックススキャンはこれまで述べてきたものよりも、更に検索範囲が広いものになります。
以下のようなクエリのケースがフルインデックススキャンにあたります。
select * from c where endswith(c.name, "どん")
この場合、name
のお尻の文字でフィルタリングをかけようとしているため、逆インデックスの並び順は役立ちません。
結局、name
のプロパティを持つアイテムを全て検索することになります。
そのため、拡張インデックススキャンと比べても更に検索範囲が広がることが分かります。
これらの点から分かることとして、フィルタリングに使う句は必要最低限のものを採用するべきであることが分かります。
たとえば、startswith
句でのフィルタリングで要件を満たすはずなのに、contains
句を採用してしまうことで、
正確なインデックススキャンで済んでいたものが、フルインデックススキャンとして実行され、検索速度が劣化し、余計なRUも発生します。
クエリを書く際は果たしてそれが最適なフィルタリングになっているかどうかをよく考える必要がありそうです。
フルスキャン
最後にフルスキャンについてです。
これはそもそもインデックスを利用できない場合の評価方法になります。
フルインデックススキャンの例の場合は、name
のプロパティを持つアイテムだけを全てスキャンしていましたが
フルスキャンの場合はname
のプロパティを持たないアイテムも含め、全てのアイテムをスキャンするような形となります。
検証
インデックスによる検索によって、検索速度やRU量にどの程度影響が出ているかはCosmosDBのデータエクスプローラ空も確認することができます。
クエリを実行した後にQuery Stats
を確認すると、Request Chargeとindex lookup timeという値が確認できます。
今回は入れているデータが少なすぎたため、検索方法を切り替えてもあまり差異は見られませんでした。
データが膨大である場合はこのあたりの数値に変化がみられると思うので、是非一度自分の環境でも試してみてください。
まとめ
今回はCosmosDBにおけるインデックスの概念と、インデックスを利用した検索方法の内容についてご紹介しました。
逆インデックスの性質を把握し、正しいフィルター句を選択することで検索速度の向上や、コストの抑制が行えることが分かりました。
あとはインデックスに関しては、他にもインデックスポリシーという概念があり、インデックスを作成するプロパティなどを切り替え、より最適化を行うことができます。
このインデックスポリシーに関しては、また別途記事にしたいと思います。
ではまた!
参考
https://learn.microsoft.com/ja-jp/azure/cosmos-db/index-overview
https://learn.microsoft.com/ja-jp/training/modules/define-indexes-azure-cosmos-db-sql-api/2-understand-indexes