Amazon Web Services ブログ
DynamoDB グローバルセカンダリインデックスを使用してクエリのパフォーマンスを向上させ、コストを削減する方法
経験豊富な Amazon DynamoDB ユーザーであろうと始めたばかりのユーザーであろうと、DynamoDB プライマリキーモデルの基本についてはよく知っているかもしれません。このキーモデルは、あらゆるスケールで一貫して 1 桁ミリ秒のパフォーマンスを提供します。プライマリキー以外の属性を使用してアプリケーションに効率的にアクセスする必要がある場合は、グローバルセカンダリインデックスが役立ちます。たとえば、employee_id
のプライマリキーを持つ従業員テーブルを照会して、特定の部署のすべての従業員を検索する場合を例にとってみましょう。
ここで、グローバルなセカンダリインデックスでは、複数の属性クエリが可能です。これらは実際、 DynamoDB の中で最も汎用性の高いツールの 1 つです。最近では、テーブルあたりのグローバルセカンダリインデックスの最大数が 5 から 20 に、制限が引き上げられました。そのため、今が DynamoDB の使用を最適化するためのグローバルセカンダリインデックスの使用方法を学ぶ恰好のタイミングです。
グローバルセカンダリインデックスは、ソーステーブルの属性のサブセットを含む個別の DynamoDB テーブルと考えてください。また、クエリ操作をサポートする代替プライマリキーも含まれています。ソーステーブルに加えられた変更は、結果整合性 (リレーショナルデータベースのマテリアライズドビューと同様) を伴ってグローバルセカンダリインデックスに伝播されます。これは、ソーステーブルのパフォーマンスや可用性に影響を与えることなく行われます。詳細については、「Global Secondary Indexes」をご覧ください。
この記事では、グローバルセカンダリインデックスを使用してデータを照会し、アプリケーションのパフォーマンスを向上させ、毎月の DynamoDB 請求金額を削減する方法をいくつかご紹介します。
グローバルセカンダリインデックスの使用パターン
以下、グローバルセカンダリインデックスの一般的な使用パターンをいくつかご紹介します。
複数の属性によるデータのクエリとソート
グローバルセカンダリインデックスを作成する際の最も一般的な使用パターンは、複数の属性によるデータのクエリとソートです。この使用パターンでは、その属性のクエリをサポートするために、テーブルのプライマリキーの一部ではない属性にグローバルセカンダリインデックスを作成します。
グローバルセカンダリインデックスは、次の 3 種類のクエリをサポートします。
- フィルタリングされたデータの取得: パーティションキーとオプションのソートキーでデータを照会する方法。フィルタリング属性をプライマリキーとして、どのテーブル属性にもグローバルセカンダリインデックスを作成できます。グローバルセカンダリインデックスを作成すると、テーブル全体をスキャンしてデータをフィルタリングする必要がなくなります。
- ソート済みデータの取得: パーティションキーとソートキーの両方を使用して、グローバルセカンダリインデックスのソートキーのデータをソートする方法。グローバルセカンダリインデックスのクエリの結果は、常にソートキー値によってソートされます。ソートされたデータの取得を必要とする属性は、グローバルセカンダリインデックスのソートキーにすることができます。クエリ結果の順序は、クエリの ScanIndexForward パラメータを使用して制御できます。
- 範囲ベースのデータ取得: グローバルセカンダリインデックスのソートキーに
BEGINSWITH
、BETWEEN
、GREATER THAN
、LESS THAN
などのクエリを含める方法。グローバルセカンダリインデックスクエリは、範囲ベースのデータ取得を可能にする KeyConditionExpressions をサポートします。
オンライン音楽ストリーミングサービスで、サービスが提供するすべての楽曲に関する情報を格納している Songs
テーブルがあるとします。楽曲に関して保存される情報には、楽曲の SingerId
、SingerName
、Genre
、LaunchYear
、Album
、SongDuration
、NumberOfViews
、および NumberOfLikes
が含まれます。テーブルのパーティションキーは SongId
です。これはランダムな文字列です。SongId
は各楽曲を一意に識別します。SongId
ごとに 1 つのレコードしかないため、テーブルにはソートキーがありません。次の表に、データベーステーブルのスキーマを示します。
次の表に、音楽ストリーミングサービスのアプリケーションに必要なクエリのリストを示します。それぞれに、グローバルセカンダリインデックスの候補プライマリキー属性と、それを使用する理由が含まれています。
クエリ | グローバルセカンダリインデックスの 候補プライマリキー属性 |
理由 | |
パーティションキー | ソートキー | ||
SingerId の楽曲をすべて検索する |
SingerId |
パーティションキーがクエリに答えるのに十分であるため、ソートキーは必要ありません。 | グローバルセカンダリインデックスは、その区画にわたる項目の均一なキー分散のために最適化されています。
|
お気に入りの数で注文したジャンルのすべての楽曲を検索する | Genre |
NumberOfLikes |
グローバルセカンダリインデックスはソートキーの順にデータを格納するため、NumberOfLikes をソートキーに保持すると、NumberOfLikes でソートされたデータを取得できます。 |
2010 年に発売された SingerId の楽曲のうち、ビュー数が最も多い曲を検索する |
SingerId |
LaunchYear:Views |
パーティションキーとして ソートキーで複数の属性を連結すると、複数の属性のデータをクエリすることができます。また連結されたソートキーの順にソートされたデータを保持します。この場合、ソートキーとして |
ある歌手の 2010~2014 年のすべての楽曲を検索する | SingerId |
LaunchYear |
SingerId はパーティションキーとしてクエリをサポートします。ソートキーとして LaunchYear を使用すると、BETWEEN KeyConditionExpression を使用して範囲クエリを実行できます。 |
データフィルタリング
場合によっては、アプリケーションで使用する前に、テーブルまたは既存のグローバルセカンダリインデックスから照会したデータをフィルタリングする必要があります。そのような場合、フィルタが適用される前に読んだすべてのアイテムを読み込むためのコストが発生します。
データフィルタリングパターンは、テーブルを照会するか、既存のグローバルセカンダリインデックスを使用する読み取りコストが、新しいグローバルセカンダリインデックスとしてデータのフィルタ済みビューを維持する書き込みコストよりも高い場合に便利です。
たとえば、食事の出前サービスで DynamoDB テーブルに、Orders
という保留中の注文が保存されているとします。テーブルのパーティションキーは RestaurantId
で、ソートキーは OrderId
です。注文のステータスは、FoodInPreparation
、ReadyForPickup
、または OutForDelivery
である可能性があります。次の表に、この DynamoDB テーブルのスキーマを示します。
出前サービスは、ReadyForPickup
の OrderStatus
で注文を割り当てることで、配送ドライバーが注文のピックアップの準備がいつできるのかを知ることができるようにする必要があります。この表は、ReadyForPickup
のように OrderStatus
のフィルター句を持つパーティションキーである RestaurantId
ごとにレストランのすべての未処理のオーダーを照会することをすでにサポートしています。
出前サービスが 30 秒ごとにすべての未処理注文を照会して、顧客の待ち時間を最小限に抑えると仮定します。この場合、ReadyForPickup
ステータスにある注文のフィルタリングされたビューを維持するのではなく、テーブルを照会すると、読み取るアイテムが大幅に増加します。これは、数多くのアイテムが、不必要にテーブルから読み取られる FoodInPreparation
および OutForDelivery
ステータスにあるためです。
この食事の出前サービスに関する次の表には、推奨されるスキーマとともに、出前サービスでグローバルセカンダリインデックスにフィルタリングされたビューを格納するために使用できるグローバルセカンダリインデックススキーマが含まれています。
クエリ
|
グローバルセカンダリインデックスの 候補プライマリキー属性 |
理由 | 推奨事項 | |
パーティションキー | ソートキー | |||
ReadyForPickup の OrderStatus で RestaurantId の注文を検索する |
RestaurantId |
ReadyForPickup |
グローバルセカンダリインデックスのパーティションキーに
|
これは、グローバルセカンダリインデックスで推奨されるスキーマです。これにより、グローバルなセカンダリインデックスのパーティション間でデータの分散が容易になり、フィルタリングされたデータを格納することによって読み書きコストも削減されます。 |
RestaurantId |
OrderStatus |
このスキーマでは、上記のグローバルセカンダリインデックスの分割方法と同じデータ分散のメリットが得られます。 ソートキーを
|
書き込み容量単位の消費量とストレージコストが高いため、推奨できません。 | |
OrderStatus |
RestaurantId |
パーティションキーとして OrderStatus を保持すると、パーティション内に並んだ同じ注文ステータスのレストランにわたってデータが保存されます。このアプローチは、異なるレストランのデータに同時にアクセスしながらホットスポットを見つけます。ホットスポットを頻繁に要求すると、プロビジョニングされた読み書き容量の使用が非効率的になり、最終的にはスロットリングが発生します。前述のアプローチと同様に、このパーティション方式では、すべての注文ステータスがグローバルセカンダリインデックスで維持されているため、より多くの書き込み容量とストレージコストが必要となります。 |
グローバルセカンダリインデックスのパーティションにわたってデータの分散が不十分なため、お勧めしません。 |
大容量アイテムの読み取り容量単位 (RCU) の消費量を減らす
場合によっては、テーブル内のアイテムに多数の属性が含まれている場合がありますが、アプリケーションクエリではこれらの属性の小さなサブセットを読み取ることのみを要します。このような場合、テーブルからのデータのクエリは、アイテムのサイズが大きいため、読み取り容量単位を多く消費してしまいます。このパターンは、特定のタイプの読み取り負荷の高いワークロードに役立ちます。このような読み取り負荷の高いワークロードは、テーブルからの問合せデータの価格が、別のグローバルセカンダリインデックスに必要な属性のみを維持する価格よりもはるかに高い場合に発生します。
このパターンは、カウントクエリで特に便利です。このようなクエリを使用して、パーティションキーの下にあるソートキーの合計数を見つけます。これらのクエリでは、各アイテムのデータには関心がないでしょうが、アイテム数の合計カウントは必要になります。そのような場合に別個のキーのみのグローバルセカンダリインデックスを維持すると、読み取られるアイテム単位のサイズが大幅に減少します。
オンラインショッピングプラットフォームは、Products
という DynamoDB テーブルに製品情報を格納しているとします。ショッピングプラットフォームは、このテーブルをクエリすることにより、これらの製品をウェブサイトに表示します。テーブルのパーティションキーは ProductId
です。ProductId
はプロダクトを一意に識別するため、テーブルにはソートキーがありません。テーブルの各項目の書き込みは一度ですが、読み込みは 100 回行われます。これにより、読み込み負荷が高くなります。
次の表に、DynamoDB テーブルのスキーマを示します。
テーブルの各項目のサイズは 100 KBですが、ウェブサイトに製品のプレビューを表示するには、ProductName
、ProductDescription
、価格
という少数の属性のみが必要となります。これらは合計で 4 KB 未満です。Products
テーブルは、すでに必要な属性へのアクセスをサポートしています。ただし、ウェブサイトに表示されるフィールドのみでグローバルセカンダリインデックスを維持するのと比較して、アイテムごとに読み取り容量単位の数倍を消費します。この場合、別のグローバルセカンダリインデックスを持つと、読み取りコストが大幅に削減されます。
読み取りワークロードの分離
この使用パターンでは、表と同じプライマリキーでグローバルセカンダリインデックスを作成します。けれども、読み取り容量単位は、特定の読者に対応するために個別にプロビジョニングされます。表のデータに複数のアクセスパターンがあり、アクセスパターンが互いに影響しないように分離が必要な場合は、このパターンを使用します。
一般的なシナリオの 1 つに、優先度の高いユーザー要求と優先度の低いオフライン処理との間で共有されるデータがあります。優先度の低いオフライン要求がテーブルで優先度の高いユーザー要求を抑制しないようにするには、オフライン要求をオフロードして、テーブルの代わりにグローバルセカンダリインデックスをスキャンしてクエリする方法があります。
一般的な使用例とシナリオ
グローバルセカンダリインデックスの一般的な使用例とシナリオを次に示します。
Internet of Things (IoT) アプリケーション
AWS のお客様は、インターネット接続された家庭、医療機器、車両、産業機械、小売店など、さまざまなユースケースにわたって IoT アプリケーションを導入しています。これらのアプリケーションのほとんどは、データ取り込みの共通パターンがあり、リアルタイムの分析と監視機能が必要です。グローバルセカンダリインデックスを使用することで、最小限の管理で IoT アプリケーションをリアルタイムで監視できます。
一例として、数百の IoT センサーを備えた多数の建物で動作する産業用アプリケーションについて考えてみましょう。各センサーはさまざまなディメンション間のデータを検出し、DynamoDB テーブルのデータを更新します。テーブルには、各メトリックの最新の値が格納されます。
この IoT アプリケーションでは、メトリックのいずれかが設定されたしきい値を超える場合、リアルタイムの監視とアラームが必要です。これを実現するには、これらの各ディメンションにグローバルセカンダリインデックスを作成し、各メトリックのしきい値を超える値についてインデックスを定期的に照会します。BuildingId
を各グローバルセカンダリインデックスのパーティションキーとして、メトリック名をソートキーとして、グローバルセカンダリインデックスを作成できます。
場合によっては、IoT アプリケーションが Speed
などの他の測定基準の測定を開始する必要があり、マシンが速すぎる場合はアラームが必要になることがあります。このような場合、メトリックをテーブル属性として追加し、そのテーブルにグローバルセカンダリインデックスを作成できます。
広告技術
ターゲティング広告、リアルタイム入札 (RTB)、広告のパーソナライゼーションなど、さまざまな広告テクノロジーのユースケースで、グローバルセカンダリインデックスを使用できます。DynamoDB は、これらのユースケースに必要なパフォーマンスと可用性を提供します。
RTB ユースケースを例にとって、グローバルセカンダリインデックスを使用してその問題を解決する方法を見てみましょう。RTB は、入札前に各広告の価値を判断するための最適な決定を行う必要があります。通常、入札と入札単価の計算の間にかかる時間は 100 ミリ秒です。DynamoDB を使用して、特定のユーザーが属するカテゴリーと、ユーザーが特定のセグメントに割り当てられた時間を含むユーザープロファイル情報を格納できます。
このユーザー情報は、入札決定ロジックの一環として使用できます。パーティションキーとして User Id
のグローバルセカンダリインデックスを使用し、ソートキーとして Category
を使用すると、広告のカテゴリー別にリアルタイムでユーザーデータを照会できます。
ゲーム
スピードとスケーラビリティは、ゲームアプリケーションの重要な側面です。これらのアプリケーションでは、通常、ゲームプレイ中のレイテンシーを避けるために、1 桁のミリ秒の読み書きが必要です。グローバルセカンダリインデックスを使用して、そのようなシナリオを処理できます。
グローバルセカンダリインデックスは、最高スコアのリーダーボード、プレーヤーのソーシャルグラフ、ゲーム内チャットメッセージを作成するのに役立ちます。新しいクエリが発生したときにグローバルセカンダリインデックスを簡単に作成および削除できるため、開発期間と市場投入までの時間が短縮されます。
まとめ
グローバルセカンダリインデックスは、DynamoDB のクエリ機能を強化します。この記事では、グローバルセカンダリインデックスをデータフィルタリングやデータの順序付けなどのパターンを使用して、読み取りの分離を実現し、クエリのコストを削減する方法を紹介しました。DynamoDB テーブルごとのグローバルセカンダリインデックスの最大数の制限が 5 から 20 へ最近引き上げられたことは、ヒット制限を気にせずにこれらの使用パターンを適用するのに役立つでしょう。
グローバルセカンダリインデックスを最大限に活用する方法について質問がある方は、下記にコメントを残すか、 @DynamoDB でツイートしてください。
著者について
Shubham Sethi は アマゾン ウェブ サービスのソフトウェア開発エンジニアです。