目次
とある日
AWS SAAでDynamoDBの勉強をしているときに、グローバルセカンダリインデックスとローカルセカンダリインデックスの違いがわからなくなってきたので、AWSのドキュメントを参考にしながら自分の考えをまとめたいと思います。
はじめに
内容について注意点
記事作成日時(2021年8月12日)
本記事は、2021年8月12日時点のサービス内容及び資料に基づいて記述しています。
最新の情報はAWS公式のドキュメントをご確認ください。
説明しないこと
DynamoDBのサービスの詳細説明は行いません。
DynamoDBのセカンダリインデックスのことに注力して記述しています。
ハンズオン
この記事では、実際にDynamoDBを作成したりする行為は行いません。
あくまでも、机上の話をまとめるものとなります。
本記事概要
グローバルセカンダリインデックスとローカルセカンダリインデックスの違いについて深堀りをしてより理解をします。
RDBMSに照らし合わせた解釈を記述したいと思います。
DynamoDB
本記事で使用するDynamoDBについて軽くおさらいします。
基礎知識
どんな規模にも対応する高速で柔軟な NoSQL データベースサービス
Amazon DynamoDB は、規模に関係なく数ミリ秒台のパフォーマンスを実現する、key-value およびドキュメントデータベースです。これはフルマネージド型でマルチリージョン、マルチアクティブで耐久性があるデータベースであり、セキュリティ、バックアップと復元、インターネット規模のアプリケーション用のインメモリキャッシュが組み込まれています。DynamoDB は、1 日に 10 兆件以上のリクエストを処理することができ、毎秒 2,000 万件を超えるリクエストをサポートします。
上記の3つのどれか要件を満たすのであれば、DynamoDBをデータベースとして選択するという認識です。
セカンダリインデックスについて
そもそもグローバルやローカルの前にセカンダリインデックスについて理解します。
Amazon DynamoDB によって、プライマリキーの値を指定して、テーブルの項目に高速アクセスすることが可能になります。しかし多くのアプリケーションでは、プライマリキー以外の属性を使って、データに効率的にアクセスできるようにセカンダリ(または代替)キーを 1 つ以上設定することで、メリットが得られることがあります。これに対応するために、1 つのテーブルで 1 つ以上のセカンダリインデックスを作成して、それらのインデックスに対して
Query
またはScan
リクエストを実行することができます。
RDB的に言うと、CREATE INDEX
で新しくINDEXを作成することと同じです。
テーブルを読み込む処理で、より高速に読み込むためにINDEXを作るという根本的な役割は同じ。
DynamoDB は、次の 2 種類のセカンダリインデックスをサポートしています。
ちなみに、RDBでも列を削除するとインデックスにも変更が反映されますが、DynamoDBではそこらへんは自動でやってくれるそうです。
すべてのセカンダリインデックスは、DynamoDB によって自動的にメンテナンスされます。ベーステーブルの項目を追加、変更、または削除すると、そのテーブルのインデックスも更新され、この変更が反映されます。
グローバルセカンダリインデックス(GSI)とは
グローバルセカンダリインデックスインデックス(以下GSIという)について例を交えながら記述します。
パーティションキーおよびソートキーを持つインデックス。ベーステーブルのものとは異なる場合があります。このインデックスに関するクエリがすべてのパーティションにまたがり、ベーステーブル内のすべてのデータを対象とする可能性があるため、グローバルセカンダリインデックスは「グローバル」と見なされます。グローバルセカンダリインデックスは、ベーステーブルとは別に独自のパーティション領域に保存され、ベーステーブルとは別にスケーリングします。
下記のような要求の場合すべてのデータスキャンとなり低速で非効率的になります。
- ゲーム Meteor Blasters で記録された最高スコアはいくつですか?
- Galaxy Invaders で最高スコアを獲得したユーザーは誰ですか?
- 勝敗の最も高い比率は何ですか?
非効率的になる理由は、これらの要求はキー属性 (UserId
と GameTitle
) 以外の非キー属性に対しての要求が含まれるからです。
Scan
はRDB的に言うと、テーブルフルスキャンとなり効率が著しく悪くなる可能があります。
RDB的クエリ速度解決策も適切なカラムに対してインデックスを貼ります、それと同じ解釈です。
このような要求に答えるときは、GSIを作成して効率の良いインデックスで検索することにします。
GameScores
たとえば、
GameScores
という名前のテーブルがあり、モバイルゲームアプリケーションのユーザーとスコアを記録しているとします。GameScores
の各項目は、パーティションキー (UserId
) およびソートキー (GameTitle
) で特定されます。次の図は、テーブル内の項目の構成を示しています。一部表示されていない属性もあります。
- パーティションキー
- UserId
- ソートキー
- GameTitle
UserId | GameTitle | TopScore | TopScoreDateTime | Wins | Losses |
---|---|---|---|---|---|
"101" | "Meteor Blasters" | 1000 | "2015-10-22:23:18:01" | 12 | 3 |
"101" | "Galaxy Invaders" | 5842 | "2015-09-15:17:24:31" | 21 | 72 |
"101" | "Starship X" | 24 | "2015-08-31:13:14:21" | 4 | 9 |
現状のテーブル構成だと、キー属性 (UserId
と GameTitle
) を指定したクエリは非常に効率的です。
下記のクエリは、効率的になると思われます。
DynamoDB でのクエリの使用の例を一部改変しています。
GameScores
テーブルに対して、特定の UserId
(パーティションキー) についてのクエリを実行しますが、今回は指定の GameTitle
(ソートキー) を持つ項目のみを返します。
aws dynamodb query \ --table-name GameScores \ --key-condition-expression "UserId = :id and GameTitle = :title" \ --expression-attribute-values file://values.json
--expression-attribute-values
の引数は、ファイルvalues.json
に保存されます。
{ ":id":{"S":"101"}, ":title":{"S":"Meteor Blasters"} }
ゲーム Meteor Blasters で記録された最高スコアはいくつですか?
ドキュメントにあった、ゲーム Meteor Blasters で記録された最高スコアはいくつですか?の場合を考えます。
この要求は、TopScore
がソートを行う必要があるクエリとなります。
非キー属性 (TopScore
) を指定したクエリの場合だと、キー属性に含まれないため上記で説明した通り非効率になります。
本要件を満たすために、GSI GameTitleIndex
を作成します。
GameTitleIndex
- パーティションキー
- GameTitle
- ソートキー
- TopScore
ベーステーブルのプライマリキー属性は必ずインデックスに射影されるので、
UserId
属性も存在します。
UserId | GameTitle | TopScore |
---|---|---|
123 | Comet Quest | 0 |
201 | Comet Quest | 0 |
301 | Comet Quest | 0 |
上記のようなGSIを作成することで、TopScore
を含めたScanも高速に効率よく検索することが可能になります。
さらなる詳細を知りたい方は、公式ドキュメントを御覧ください。
ローカルセカンダリインデックス(LSI)とは
ローカルセカンダリインデックス(以下LSIという)について例を交えながら記述します。
パーティションキーはベーステーブルと同じで、ソートキーが異なるインデックス。ローカルセカンダリインデックスは、ローカルセカンダリインデックスのすべてのパーティションの範囲が同じパーティションキーバリューを持つベーステーブルのパーティションに限定されるという意味で「ローカル」です。
リクエストによっては、より複雑なデータアクセスパターンが要求される場合があります。以下に例を示します。
- ビューと返信の数が最も多いのはどのフォーラムか?
- 特定のフォーラムでメッセージ数が最も多いのはどのスレッドか?
- 特定の期間中に特定のフォーラムに投稿されたスレッド数は?
これらの要求では、Query
アクションでは十分ではありません。代わりに、テーブル全体の Scan
を実行する必要があります。
テーブル全体のScan
になると非効率的になります。
LSI作成条件
注意点として、LSI
では下記の条件を満たさなくてはなりません。
- パーティションキーはそのベーステーブルのパーティションキーと同じである。
- ソートキーは完全に 1 つのスカラー属性で構成されている。
- ベーステーブルのソートキーがインデックスに射影され、非キー属性として機能する。
Thread
たとえば、
Thread
で定義した DynamoDB でのコード例用のテーブルの作成とデータのロード テーブルがあります。このテーブルは、AWS ディスカッションフォーラムのようなアプリケーションで役立ちます。次の図は、テーブル内の項目の構成を示しています。一部表示されていない属性もあります。
- パーティションキー
- ForumName
- ソートキー
- Subject
ForumName | Subject | LastPostDateTime | Replies |
---|---|---|---|
"S3" | "aaa" | "2015-03-15:17:24:31" | 12 |
"S3" | "bbb" | "2015-01-22:23:18:01" | 3 |
"S3" | "ccc" | "2015-02-31:13:14:21" | 4 |
"S3" | "ddd" | "2015-01-03:09:21:11" | 9 |
特定のフォーラムに過去 3 か月以内に投稿されたすべてのスレッドはどれですか?
ドキュメントにあった、特定のフォーラムに過去 3 か月以内に投稿されたすべてのスレッドを検索する場合を考えます。
この要求は、LastPostDateTime
がソートを行う必要があるクエリとなります。
非キー属性 (LastPostDateTime
) を指定したクエリの場合だと、キー属性に含まれないため上記で説明した通り非効率になります。
本要件を満たすために、LSILastPostIndex
を作成します。
LastPostIndex
- パーティションキー
- ForumName
- ソートキー
- LastPostDateTime
LSIの条件としてパーティションはベーステーブルと同じにする必要があるのでForumName
を指定します。
ソートキーは、過去三ヶ月の要求を満たすためLastPostDateTime
を指定します。
ForumName | LastPostDateTime | Subject |
---|---|---|
"S3" | "2015-03-15:17:24:31" | "aaa" |
"S3" | "2015-01-22:23:18:01" | "bbb" |
"S3" | "2015-02-31:13:14:21" | "ccc" |
"S3" | "2015-01-03:09:21:11" | "ddd" |
ベーステーブルのSubject
は、ソートキーに含まれるのでインデックスに射影されますが、インデックスキーの一部ではないです。
上記のようなLSIを作成することで、LastPostDateTime
を含めたScanも高速に効率よく検索することが可能になります。
さらなる詳細を知りたい方は、公式ドキュメントを御覧ください。
DynamoDB のセカンダリインデックスの一般的なガイドライン
ここまでで、セカンダリインデックスの概要がある程度理解できたと思います。
最後になりますがここでは、AWSが定義しているDynamoDB
のベストプラクティスのセカンダリインデックスについて記述します。
結論としては、「一般的には、ローカルセカンダリインデックスではなく、グローバルセカンダリインデックスを使用する必要があります。」とAWSでは言われています。
しかし、例外も存在します。
例外についても、下記で紹介したいと思います。
グローバルセカンダリインデックスとローカルセカンダリインデックスの相違点
それぞれのセカンダリインデックスで下記の相違点が存在します。
詳細については、AWSの公式ドキュメントを御覧ください。
ここでは、セカンダリインデックスのベストプラクティスの観点から2つだけ紹介します。
キーの属性
GSIの場合
インデックスパーティションキーとソートキー (存在する場合) は、文字列、数値、またはバイナリ型の任意のベーステーブル属性とすることができます。
LSIの場合
インデックスのパーティションキーは、ベーステーブルのパーティションキーと同じ属性です。ソートキーは、文字列、数値、またはバイナリ型の任意のベーステーブル属性とすることができます。
つまり、LSIの方では指定できるキーに制限が存在します。
読み込み整合性
GSIの場合
グローバルセカンダリインデックスのクエリは結果整合性をサポートします。
LSIの場合
ローカルセカンダリインデックスのクエリを実行するとき、結果整合性または強い整合性のどちらかを選択できます。
DynamoDBでは、読み取りの整合性で結果整合性と強い整合性があります。
一般的には、GSIを使用すると結論で述べましたが例外として、クエリ結果に強力な整合性が必要な場合があるときは。
LSIを選択する必要があります。
インデックスを作成する際に留意すべき一般的な原則と設計パターン
インデックスを効率的に使用する
インデックス数は最小限に抑えます。 頻繁にクエリを行わない属性では、セカンダリインデックスを作成しないようにします。ほとんど使用されていないインデックスは、ストレージおよび I/O のコスト増大の一因になり、アプリケーションのパフォーマンスには効果がありません。
RDBでも同じことが言えると思います。
無駄なインデックスがあるとテーブル状態が変わったときにインデックスが更新されるときに余計な処理となるため、
不必要なインデックスは作成しないように。
計画を慎重に選択する
セカンダリインデックスはストレージとプロビジョニング済みのスループットを消費するため、インデックスのサイズは可能な限り小さくすべきです。また、インデックスが小さいほど、テーブル全体に対してクエリを行うのに比べてパフォーマンスが向上します。使用するクエリが属性の一部しか返さないことが多く、それらの属性のサイズを合計しても項目全体より大幅に小さい場合には、頻繁にリクエストを行う属性だけを対象とするようにします。
DynamoDBのセカンダリインデックスはRDBのインデックスと定義が少し異なると思います。
セカンダリインデックスの場合、インデックスよりテーブルに近いと思いました。
セカンダリインデックスを定義することで、新しいテーブルとしてパーティションキーとソートキーを再定義している感じです。
なので、インデックスが大きければストレージの圧迫に繋がります。
フェッチを回避するための頻繁なクエリの最適化
レイテンシーを可能な限り小さくしてクエリを最速にするには、クエリによって返ることが予想されるすべての属性を計画します。特に、計画されていない属性のローカルセカンダリインデックスにクエリを実行すると、DynamoDB は自動的にテーブルからこれらの属性を取得します。そのため、項目全体をテーブルから読み取る必要があります。これにより、レイテンシーと不要な I/O オペレーションを減らすことができます。
ローカルセカンダリインデックスに射影されていない属性を読み込むインデックスクエリの場合、DynamoDB は射影された属性をインデックスから読み込むのに加えて、それらの属性をベーステーブルからフェッチする必要があります。
メモリー上にないデータをストレージから読みだす処理と同じですね。
とどのつまり、必要な属性と適切なインデックスを指定しましょうということです。
ローカルセカンダリインデックス作成時に項目コレクションのサイズ制限を確認する
項目コレクションとは、テーブルとそのローカルセカンダリインデックス内で、同じパーティションキーを持つすべての項目を意味します。10 GB を超えることができる項目コレクションはないため、パーティションキーバリューによっては容量が不足する可能性があります。
テーブル項目を追加または更新すると、DynamoDB は影響を受けるローカルセカンダリインデックスをすべて更新します。インデックスが付けられた属性がテーブル内で定義されている場合は、ローカルセカンダリインデックスも増加します。
LSIには、パーティションキー値ごとのサイズ制限が存在します。
パーティションキーの値ごとに、すべてのインデックス付き項目の合計サイズが、10 GB 以下である必要があります。
なので、テーブル項目を追加すると合計サイズを超えないように気をつけましょうねという話です。
〆
かなり量が多くなりましたが、それだけDynamoDBが奥深いということになります。
今回は、ドキュメントの内容からセカンダリインデックスの概要を理解しましたが、これだけではDynamoDBを理解できたとは言わないのでハンズオンでさらに知識を深めたいと思う今日このごろ。