Amazon Web Services ブログ
Amazon DynamoDB グローバルセカンダリインデックスを設計する方法
大学時代、私はリレーショナルデータベースのシステム要件をモデル化するために、エンティティ関係図を作成しました。このプロセスでは、ソフトウェアシステムのすべてのエンティティを検索し、それらのエンティティ間の関係を定義しました。次に、データベースがどのクエリをサポートする必要があるかを判断する前に、関係とエンティティをデータベーステーブルにモデル化しました。データベーススキーマを設計するこの方法は、スケーラビリティとより一貫したパフォーマンスを利用するために非リレーショナルデータベースを使い始めるまではうまく機能しました。
非リレーショナルデータベースでは、スキーマ設計のアプローチは逆になります。データベーススキーマを設計する前に、アプリケーションが必要とするクエリを特定するために「クエリ優先」アプローチを使用するのです。そのため、データはアプリケーションが使用する必要がある方法で明示的に保存され、クエリの効率が向上します。
また、クエリに柔軟性を追加したい場合は、Amazon DynamoDB でグローバルセカンダリインデックスを使用することができます。DynamoDB テーブルでグローバルセカンダリインデックスを使用すると、非キー属性を使用して他のディメンションでデータを柔軟に照会できます。
ただし、効率的なクエリパフォーマンスを維持するには、DynamoDB テーブルのスキーマを設計したのと同じ方法で、グローバルセカンダリインデックスのスキーマを慎重に設計する必要があります。このブログ記事では、グローバルセカンダリインデックスのスキーマを設計するためのアプローチを示し、設計プロセスにおける一般的な落とし穴を回避する方法を説明し、コストを削減するためのヒントを提供します。
グローバルセカンダリインデックスのスキーマ設計プロセス
次の図は、グローバルセカンダリインデックスのスキーマを設計する方法について、この記事で説明しているアプローチをまとめたものです。
クエリパターンを特定する
アプリケーション固有のクエリパターン (テーブルがサポートするクエリの種類) が、グローバルセカンダリインデックスの設計を推進します。設計を推進する中心的な質問は、「グローバルセカンダリインデックスが回答する、どのような質問が必要であるか?」ということです。
回答が必要な質問が決まったら、質問をテーブルデータのクエリにマッピングします。「より大きい」、「より小さい」、「の間」、「で始まる」などの範囲クエリに基づくデータフィルターを使用します。 アクセスしなければならないがフィルタリングやソートを必要としない他のデータについても考慮する必要があります。たとえば、オンラインショッピングのウェブサイトに商品情報を表示するには、商品の ProductId でデータをフィルター処理します。ただし、クエリでアクセスする必要があるその他のデータには、製品の説明、価格、重量、製品の色などがあります。できるだけ多くのクエリを事前に特定します。スキーマ設計でクエリを考慮に入れると、グローバルセカンダリインデックスのコストとパフォーマンスを最適化するのに役立ちます。
それでは例を見て、アプリケーション固有のクエリがテーブルクエリにどのように変換されるのかを確認しましょう。たとえば、オンラインショッピングのウェブサイトが、顧客の注文をすべて OrderId をパーティションキーとして Orders テーブルに保存しているとします。また、このテーブルには、OrderDate、CustomerId、Status など、注文に関するその他のデータも保存されています。次の表は、アプリケーション固有の一般的な質問とそれに対応するテーブルクエリの一部を示しています。
アプリケーション固有の質問 | テーブルクエリ |
注文日順に並べ替えられた顧客のすべての注文を検索する | Orders テーブルのすべての注文を CustomerId でフィルター処理してから、OrderDate で並べ替える |
特定の日付範囲内の特定の顧客の注文を取得する | Orders テーブルのすべての注文を CustomerId でフィルター処理してから、OrderDate での範囲照会でフィルター処理する |
顧客の保留中の注文をすべて探す | Orders テーブルのすべての注文を CustomerId でフィルター処理してから、Status を「Pending」としてフィルター処理する |
5 日以上経過している顧客の保留中の注文をすべて探す | Orders テーブルのすべての注文を CustomerId でフィルター処理してから、Status を「Pending 」とし、OrderDate < CurrentDate-5 の範囲照会でフィルター処理する |
顧客のすべての注文の OrderId 、OrderDate 、Status を取得する |
Orders テーブルのすべての注文を CustomerId でフィルター処理してから、それらの OrderId 、OrderDate 、Status を取得する |
候補フィールドを特定する
クエリパターンを識別したら、これらのクエリパターンのデータを照会するために必要なグローバルセカンダリインデックスの候補フィールドを特定します。まず、候補フィールドがどのように形成され、どのように使用される必要があるかを見てみましょう。
グローバルセカンダリインデックスの候補フィールドには、候補パーティションキー、候補ソートキー、属性投影が含まれます。グローバルセカンダリインデックスは、DynamoDB テーブルでパーティションキーが使用される方法と似ている、内部ハッシュ関数への入力としてパーティションキーの値を使用します。ハッシュ関数からの出力値によって、項目が保存されているパーティションが決まります。グローバルセカンダリインデックスを照会するには、パーティションキー値が常に必要です。
ソートキーは、すべての項目を同じパーティションキー値で保存して、ソートキー値順に並べて物理的に近くなるようにすることができるオプションのキーです。グローバルセカンダリインデックスを照会する場合、ソートキーに条件を適用して、特定の値の範囲内の項目だけを返すようにすることができます。
属性投影には、テーブルからグローバルセカンダリインデックスにコピーされる一連の属性が含まれます。テーブルのパーティションキーとソートキーは、常にグローバルセカンダリインデックスに投影されます。グローバルセカンダリインデックスを照会することによって、投影された属性にアクセスできます。
次のテーブルは、いくつかのより一般的なクエリパターンに対して選択する候補フィールドを示しています。
クエリ | 候補パーティションキー | 候補ソートキー | 属性投影 |
A のテーブルレコードをフィルター処理する |
A | ||
A のテーブルレコードをフィルター処理し、B でソートする |
A | B | |
A と B のテーブルレコードをフィルター処理する |
A | B | |
A と B のテーブルレコードをフィルター処理し、C でソートする |
A:B (複合パーティションキー) | C | |
A のテーブルレコードをフィルター処理し、B と C でソートする |
A | B:C (複合ソートキー) | |
A のテーブルレコードをフィルター処理してから、GREATER THAN 、LESS THAN 、STARTS WITH 、BETWEEN などの範囲照会によって B でフィルター処理する |
A | B | |
A のテーブルレコードをフィルター処理し、属性 B 、C 、D のサブセットにアクセスするが、これらの属性によるフィルター処理やソートは行わない。 |
A | B, C, D |
グローバルセカンダリインデックスでは、プライマリキー以外のフィールドのデータを照会することはできません。つまり、データをフィルター処理する必要があるすべてのフィールドは、候補パーティションキーまたは候補ソートキーのいずれかに含まれる必要があります。
以下の質問に答えると、フィールドを候補パーティションキーにするか候補ソートキーにするかを判断するのに役立ちます。
- このフィールドではデータの並べ替えが必要ですか? もし必要なら、そのフィールドは候補ソートキーの一部である必要があります。
- このフィールドでは範囲に基づく照会が必要ですか? もし必要なら、そのフィールドは候補ソートキーの一部である必要があります。
- 複数のフィールドでデータを並べ替える必要がありますか? もし必要なら、これらのフィールドを単一の複合フィールドに組み合わせて、候補ソートキーとして使用します。
- 複数のフィールドでデータをフィルター処理する必要がありますか? もし必要なら、これらのフィールドを単一の複合フィールドに組み合わせて、候補パーティションキーとして使用します。
- 別のフィールドに対する範囲照会と一緒に、フィールドに対してデータのソートが必要ですか? もし必要なら、これらのフィールドを単一の複合属性に組み合わせて、候補ソートキーとして使用します。
- ある属性でフィルター処理してから、別の属性でフィルター処理する必要がありますか? もし必要なら、最初のフィルター処理のためのフィールドは候補パーティションキーの一部であり、その後のフィルター処理のためのフィールドは候補ソートキーの一部である必要があります。
アクセスしなければならないがデータのフィルター処理を必要としないクエリ内の他のすべてのフィールドは、属性投影に含める必要があります。
次のテーブルは、前に使用したオンラインショッピングのウェブサイトストアのデータを使用して、例を示しています。
テーブルクエリ | 候補パーティションキー | 候補ソートキー | 属性投影 |
Orders テーブルのすべての注文を CustomerId でフィルター処理してから、OrderDate で並べ替える |
CustomerId | OrderDate | |
Orders テーブルのすべての注文を CustomerId でフィルター処理してから、OrderDate での範囲照会でフィルター処理する |
CustomerId | OrderDate | |
Orders テーブルのすべての注文を CustomerId でフィルター処理してから、Status を「Pending 」としてフィルター処理する |
CustomerId | Status | |
Orders テーブルのすべての注文を CustomerId でフィルター処理してから、Status を「Pending 」とし、OrderDate < CurrentDate-5 の範囲照会でフィルター処理する |
CustomerId | Status:OrderDate (複合ソートキー) | |
Orders テーブルのすべての注文を CustomerId でフィルター処理してから、それらの OrderId 、OrderDate 、Status を取得する |
CustomerId | OrderId、OrderDate、Status |
コストとパフォーマンスのために候補フィールドを最適化する
グローバルセカンダリインデックスのスキーマで使用する前に、コストとパフォーマンスのために各クエリに対して特定する候補フィールドを最適化する必要があります。
候補フィールドのコストを最適化する
すべてのグローバルセカンダリインデックスは独立してプロビジョニングされ、ベーステーブルとは別にデータの独自のコピーを保持します。その結果、複数のクエリに回答するためにインデックスを共有すると、インデックスを維持するためのコストを削減できます。
以下の質問に答えると、最適なスキーマを設計してコストを最適化するのに役立ちます。
- 単一のグローバルセカンダリインデックスを使用して複数のクエリに回答することはできますか?
単一のグローバルセカンダリインデックスを使用して複数のクエリに回答できる場合があります。グローバルセカンダリインデックスを再利用できる一般的なユースケースは以下のとおりです。- 複合プライマリキー (パーティションキーおよびソートキー) でのグローバルセカンダリインデックスの使用: 複合プライマリキーを使用すると、データを照会するときの柔軟性がさらに高まります。複合プライマリキー使用する場合は、パーティションキーを指定するか、またはパーティションキーとソートキーの両方を指定することによってデータを照会することができます。それでは、前に使用したオンラインショッピングのウェブサイトの例に戻り、この技法を使用して複数のクエリに回答するためにグローバルセカンダリインデックスを再利用する方法を理解しましょう。次のテーブルは、単一のグローバルセカンダリインデックスを使用して回答できるクエリを示しています。
クエリ テーブルクエリ 候補パーティションキー 候補ソートキー 属性投影 1 Orders テーブルのすべての注文を CustomerId
でフィルター処理してから、OrderDate
で並べ替えるCustomerId OrderDate OrderId、OrderDate、Status 2 Orders テーブルのすべての注文を CustomerId
でフィルター処理してから、OrderDate
での範囲照会でフィルター処理する3 Orders テーブルのすべての注文を CustomerId
でフィルター処理してから、それらのOrderId
、OrderDate
、Status
を取得する上のテーブルのクエリ 1 と 2 では、パーティションキーとソートキーの両方を使用してグローバルセカンダリインデックスを照会することができます。クエリ 3 では、同じグローバルセカンダリインデックスをパーティションキーだけで照会することができます。結果として、パーティションキーとして CustomerId、ソートキーとして OrderDate を持つ単一のグローバルセカンダリインデックスは、3 つのクエリすべてに回答するために再利用できます。クエリごとに個別のグローバルセカンダリインデックスを作成する必要はありません。
- 複合ソートキーの使用: 複合ソートキーは、複数の属性を組み合わせて作成されたソートキーです。グローバルセカンダリインデックスのソートキーとして複合ソートキーを使用すると、
BeginsWith
、Greater Than
、Less Than
、Between
などのさまざまなキー条件を使用する、強力な照会機能が可能になります。たとえば、ローカルの検索エンジンアプリケーションで、ローカル企業の場所に対応した検索クエリが許可されているとします。それぞれの企業一覧は、パーティションキーとしてBusinessId
を持つ Business という名前の DynamoDB テーブルに保存されます。テーブルには、ソートキーはありません。それぞれの企業一覧には、BusinessType
、Country
、State
、City
、BusinessName
、Business Address
、PhoneNumber
の各属性が関連付けられています。アプリケーションは、以下のクエリパターンのサポートを必要とします。Business Type A
およびCountry B
の企業のフィルター処理。Business Type A
、Country B
、State C
の企業のフィルター処理。Business Type A
、Country B
、State C
、City D
の企業のフィルター処理。
次のテーブルは、これらのクエリのそれぞれに対するグローバルセカンダリインデックスの候補フィールドを示しています。
クエリ テーブルクエリ 候補パーティションキー 候補ソートキー 属性投影 1 BusinessType A
およびCountry B
の企業のフィルター処理BusinessType Country 2 BusinessType A
、Country B
、State C
の企業のフィルター処理BusinessType Country:State 3 BusinessType A
、Country B
、State C
、City D
の企業のフィルター処理BusinessType Country:State:
Cityパーティションキーとして
BusinessType
、ソートキーとしてCountry:State:City
を持つ単一のグローバルセカンダリインデックスは、3 つのクエリをすべてサポートできます。クエリごとに個別のグローバルセカンダリインデックスを作成する必要はありません。ここでの考え方は、グローバルセカンダリインデックスからデータを照会しながら、ソートキー条件にBeginsWith
条件を使用することです。次のテーブルは、このグローバルセカンダリインデックスのレコードの一部を示しています。
企業の種類 (パーティションキー) 国:州:都市 (ソートキー) 企業名 企業住所 電話番号 フィットネスセンター USA:Washington:Seattle Business1 Address1 111-111-1111 コーヒーショップ USA:California:PaloAlto Business2 Address2 222-222-2222 レストラン India:Maharashtra:Mumbai Business3 Address3 123-456-7890 この例で、複合グローバルセカンダリインデックスを使用して必要なデータを取得するアプリケーションクエリを見てみましょう。
クエリ パーティションキー条件 ソートキー条件 アメリカのすべてのコーヒーショップを探す BusinessType が CoffeeShop と等しい Country:State:City が USA で始まる ワシントン州のレストランをすべて探す BusinessType が Restaurant と等しい Country:State:City が USA:Washington で始まる シアトルのすべてのフィットネスセンターを探す BusinessType が FitnessCenter と等しい Country:State:City が USA:Washington:Seattle と等しい
- 複合プライマリキー (パーティションキーおよびソートキー) でのグローバルセカンダリインデックスの使用: 複合プライマリキーを使用すると、データを照会するときの柔軟性がさらに高まります。複合プライマリキー使用する場合は、パーティションキーを指定するか、またはパーティションキーとソートキーの両方を指定することによってデータを照会することができます。それでは、前に使用したオンラインショッピングのウェブサイトの例に戻り、この技法を使用して複数のクエリに回答するためにグローバルセカンダリインデックスを再利用する方法を理解しましょう。次のテーブルは、単一のグローバルセカンダリインデックスを使用して回答できるクエリを示しています。
- 正しいデータを投影していますか?クエリの出力をフィルター処理する必要がある属性ではなく、フィルター処理されたレコードを保存する属性にグローバルセカンダリインデックスを作成することで、ストレージと読み取りのコストを削減することができます。決して照会されないと分かっている属性を投影しないでください。照会で必要になることがほとんどないと思われる属性を投影しないようにすることも良いことです。インデックスに投影されている属性を更新するたびに、インデックスを更新するための追加コストも発生します。テーブルを照会することで、まだ投影されていない属性を取得することができることを覚えておいてください。
プロビジョンされたスループットの使用を最適化する
読み取りおよび書き込みリクエストをパーティション間で分散させることで、グローバルセカンダリインデックスにプロビジョンされたスループットをより効率的に使用できます。つまり、パーティションキーが一様にアクセスされるように、グローバルセカンダリインデックスのスキーマを設計する必要があります。効率的なスループットレベルを達成するためにすべてのパーティションキー値にアクセスする必要はなく、アクセスされたパーティションキー値の割合は必ずしも高い必要はありません。ただし、ワークロードがアクセスする個別のパーティションキー値の数を最大にすることを目標とする必要があります。これは、パーティション化された領域全体に分散するリクエストの数が増えるためです。一般に、パーティションキー値の総数に対するアクセスされたパーティションキー値の比率が高いほど、プロビジョンされたスループットをより効率的に使用できます。
多数の異なる値を持つグローバルセカンダリインデックスのパーティションキー属性を選択してください。うまく分散されたパーティションキーを達成するためのいくつかの一般的な方法:
- 一意の属性: 各項目に異なる値を持つ
emailId
、phoneNumber
、orderId
、customerId
、sessionId
などの属性 (選択度が高い属性) を使用します。 - 複合属性: アクセスパターンで機能する場合は、複数の属性を組み合わせて一意のキーを形成します。たとえば、Orders テーブルには、パーティションキーとして
customerid+productid+countrycode
、ソートキーとしてorder_date
がある可能性があります。 - 乱数を追加する: 所定の範囲 (たとえば、1 から 10 までの固定数) から、乱数を選択してそれをパーティションに追加します。これは、ブールフラグや列挙値など、値が少ないフィールドでグローバルセカンダリインデックスが必要な場合に適用されます。
たとえば、Orders テーブルには、パーティションキーとして OrderId
、属性の 1 つとして Status
がある可能性があります。注文の Status
フィールドは、Ordered
、In Transit
、Delivered
のいずれかです。アプリケーションでは、特定のステータスにあるすべての注文に対してクエリを実行する必要があります。[Status
] フィールドにグローバルセカンダリインデックスがあると、グローバルセカンダリインデックスのパーティションキーの選択度が低くなります。パーティションキーの選択度が低いとキーの分布が偏ってしまうため、1 から 10 までのランダムに生成されたサフィックスを各パーティションキーに追加することをお勧めします。その後、アプリケーションは、10 のパーティションキーのそれぞれを使用してグローバルセカンダリインデックスに並行して照会し、結果をマージして特定の状況にあるすべての注文を取得することができます。
グローバルセカンダリインデックスのベストプラクティス
グローバルセカンダリインデックスを使用するときは、以下の推奨事項を考慮してください。
- 十分なキャパシティをプロビジョンする: グローバルセカンダリインデックスに十分なキャパシティをプロビジョンすることは非常に重要です。そうしないと、ベーステーブルの書き込みに対してプロビジョンされたスループットの例外が発生する可能性があります。テーブルの更新には、対応するグローバルセカンダリインデックスの更新が必要であり、テーブルではなくグローバルセカンダリインデックスのプロビジョンされた書き込みキャパシティが消費されます。グローバルセカンダリインデックスへのプロビジョンが不足すると、インデックスへの更新が遅くなるだけでなく、最終的にはテーブルへの書き込みが失敗します。プロビジョニング不足を防ぐ最も簡単な方法は、ベーステーブルに対してオンデマンドキャパシティモードを選択することです (ベーステーブルに関連付けられているすべてのグローバルセカンダリインデックスにも適用されます)。
- 結果整合性を維持する: グローバルセカンダリインデックスは、属性が非同期的に投影されるため、結果整合性があります。更新は通常、ほんの一瞬でグローバルセカンダリインデックスに伝播されます。ほとんどのアプリケーションは、インデックスへの伝播が高速である限り、ユースケースに対する強力な整合性の保証は必要としません。少し時間をかけて、ユースケースが本当に強力な整合性の保証を必要とするか考えてください。必要とする場合は、テーブル間で強力な整合性を強制する方法として代わりに DynamoDB トランザクションを使用することを検討してください。
グローバルセカンダリインデックスを使用する際のコスト削減のための追加のヒント
以下の追加のヒントは、グローバルセカンダリインデックスを使用する際のコスト削減に役立ちます。
- グローバルセカンダリインデックスに合わせて属性名を賢く選択する
項目のサイズは、その属性名と値の長さを加算することによって計算されます。属性名を短くすると、ストレージと書き込みのコストを削減できます。たとえば、属性名としてCustomer_Age
を使用する代わりに、Age を使用することを検討します。 - スパースなインデックスを使用する
DynamoDB は、グローバルセカンダリインデックスキー値が項目に存在する場合にのみ、対応するグローバルセカンダリインデックス項目を書き込みます。キーがすべてのテーブル項目には現れない場合、グローバルセカンダリインデックスはスパースであると言われます。グローバルセカンダリインデックスキー属性に、NULL 値および空の値を保存しないでください。グローバルセカンダリインデックスキー属性の値が NULL または空の場合は、書き込むときにその属性をスキップするほうがよいでしょう。グローバルセカンダリインデックスは別々に保存されるため、NULL または空の属性の書き込みをスキップすると、グローバルセカンダリインデックスに投影されず、ストレージと書き込みコストが節約されます。可能な限り、スパースなインデックスを使用します。 - 項目のサイズを検討する
項目を DynamoDB に書き込むために必要な書き込みキャパシティーユニットの総数は、項目のサイズによって異なります。そのため、テーブルでプロビジョンドキャパシティモードとオンデマンドキャパシティモードのどちらを使用するかに関係なく、項目のサイズを小さく抑えることは費用対効果に優れています。DynamoDB テーブルに大きな属性を保存するときは、以下の点を考慮します。- DynamoDB に保存する前に属性を圧縮する: 大きな属性値を圧縮すると、DynamoDB 項目のサイズを減らし、ストレージコストを削減するのに役立ちます。
- Amazon S3 に属性を保存し、DynamoDB テーブルに Amazon S3 マッピングを含める: 属性を Amazon S3 のオブジェクトとして保存してから、そのオブジェクト識別子を DynamoDB 項目に保存することができます。項目全体を読み取る必要があるときはいつでも、項目に保存されているオブジェクト識別子を使用して Amazon S3 から読み取ることができます。
項目サイズの縮小についての詳細は、大きな項目と属性を保存するためのベストプラクティスを参照してください。
まとめ
この記事では、グローバルセカンダリインデックスのスキーマを設計するプロセスについて詳しく説明しました。優れたスキーマ設計は、パフォーマンスを最大化し、グローバルセカンダリインデックスからデータを照会するコストを最小化するのに役立ちます。優れたスキーマ設計には、グローバルセカンダリインデックスを再利用して複数のクエリをサポートし、プロビジョンされたスループットの使用を最適化することが含まれます。また、グローバルセカンダリインデックスを使用する際の落とし穴を避けるのに役立つベストプラクティスについても説明しました。
著者について
Shubham Sethi は、AWS のソフトウェア開発エンジニアです。