インデックスはすべてのデータベースの適切なデータ モデリングの重要な部分であり、 DynamoDBも例外ではありません。DynamoDB のセカンダリ インデックスは、データへの新しいアクセス パターンを可能にする強力なツールです。
この投稿では、 DynamoDB セカンダリインデックスについて説明します。まず、DynamoDB についてどう考えるべきか、またセカンダリインデックスが解決する問題について、概念的なポイントから説明します。次に、セカンダリインデックスを効果的に使用するための実用的なヒントをいくつか紹介します。最後に、セカンダリインデックスを使用すべき場合と、他のソリューションを検討すべき場合について考えます。
始めましょう。
セカンダリインデックスのユースケースとベストプラクティスに入る前に、まずDynamoDB セカンダリインデックスとは何かを理解する必要があります。そして、そのためには、DynamoDB の仕組みについて少し理解する必要があります。
これは、DynamoDB の基本的な知識があることを前提としています。セカンダリ インデックスを理解するために知っておく必要のある基本的なポイントについて説明しますが、DynamoDB を初めて使用する場合は、より基本的な概要から始めることをお勧めします。
DynamoDB はユニークなデータベースです。OLTP ワークロード向けに設計されているため、大量の小さな操作を処理するのに最適です。たとえば、ショッピング カートにアイテムを追加する、ビデオに「いいね」を付ける、Reddit にコメントを追加するなどです。このように、MySQL、PostgreSQL、 MongoDB 、Cassandra など、これまで使用していた他のデータベースと同様のアプリケーションを処理できます。
DynamoDB の主な約束は、あらゆる規模で一貫したパフォーマンスを保証することです。テーブルに 1 メガバイトのデータがある場合でも、1 ペタバイトのデータがある場合でも、DynamoDB は OLTP のようなリクエストに対して同じレイテンシーを実現しようとします。これは大きな問題です。多くのデータベースでは、データ量や同時リクエストの数が増えるとパフォーマンスが低下します。ただし、これらの保証を提供するにはトレードオフが必要であり、DynamoDB には、効果的に使用するために理解する必要がある独自の特性がいくつかあります。
まず、DynamoDB は、データを内部的に複数のパーティションに分散することで、データベースを水平方向に拡張します。これらのパーティションはユーザーには表示されませんが、DynamoDB の動作の中核をなしています。テーブルのプライマリ キー (「パーティション キー」と呼ばれる単一の要素、またはパーティション キーとソート キーの組み合わせ) を指定すると、DynamoDB はそのプライマリ キーを使用して、データが保存されるパーティションを決定します。ユーザーが行うすべてのリクエストは、リクエスト ルーターを通過し、どのパーティションがリクエストを処理するかを決定します。これらのパーティションは小さいため (通常 10 GB 以下)、移動、分割、複製、その他の管理を個別に行うことができます。
シャーディングによる水平スケーラビリティは興味深いものですが、DynamoDB に固有のものではありません。リレーショナルと非リレーショナルの両方を含む他の多くのデータベースは、水平スケーリングにシャーディングを使用しています。ただし、DynamoDB に固有の点は、データにアクセスするために主キーを使用するように強制することです。リクエストを一連のクエリに変換するクエリ プランナーを使用するのではなく、DynamoDB では、データにアクセスするために主キーを使用するように強制します。基本的に、データに直接アドレス指定可能なインデックスを取得していることになります。
DynamoDB の API はこれを反映しています。個々のアイテムに対する一連の操作 ( GetItem
、 PutItem
、 UpdateItem
、 DeleteItem
) があり、個々のアイテムの読み取り、書き込み、削除を行うことができます。さらに、同じパーティション キーを持つ複数のアイテムを取得できるQuery
操作もあります。複合プライマリ キーを持つテーブルがある場合、同じパーティション キーを持つアイテムは同じパーティションにグループ化されます。それらはソート キーに従って順序付けされるため、「ユーザーの最新の注文を取得する」や「IoT デバイスの最新の 10 個のセンサー読み取り値を取得する」などのパターンを処理できます。
たとえば、ユーザーのテーブルを持つ SaaS アプリケーションを想像してみましょう。すべてのユーザーは 1 つの組織に属しています。次のようなテーブルがあるとします。
パーティション キーが「組織」、ソート キーが「ユーザー名」の複合プライマリ キーを使用しています。これにより、組織とユーザー名を指定して個々のユーザーを取得または更新する操作を実行できます。 Query
操作に組織のみを指定して、単一の組織のすべてのユーザーを取得することもできます。
基本的な事項を念頭に置いた上で、セカンダリ インデックスについて見ていきましょう。セカンダリ インデックスの必要性を理解する最も良い方法は、セカンダリ インデックスが解決する問題を理解することです。DynamoDB がプライマリ キーに従ってデータをパーティション分割する方法と、データにアクセスするためにプライマリ キーを使用するように促す方法について説明しました。これは一部のアクセス パターンでは問題ありませんが、別の方法でデータにアクセスする必要がある場合はどうでしょうか。
上記の例では、組織とユーザー名でアクセスするユーザーのテーブルがありました。ただし、電子メール アドレスで 1 人のユーザーを取得する必要がある場合もあります。このパターンは、DynamoDB が推奨する主キー アクセス パターンには適合しません。テーブルはさまざまな属性でパーティション化されているため、必要な方法でデータにアクセスする明確な方法はありません。テーブル全体をスキャンすることもできますが、これは時間がかかり、非効率的です。データを別の主キーを持つ別のテーブルに複製することもできますが、複雑さが増します。
ここでセカンダリ インデックスが役立ちます。セカンダリ インデックスは基本的に、異なるプライマリ キーを持つデータの完全に管理されたコピーです。インデックスのプライマリ キーを宣言することで、テーブルにセカンダリ インデックスを指定します。テーブルに書き込みが行われると、DynamoDB はデータを自動的にセカンダリ インデックスに複製します。
注*: このセクションの内容はすべて、グローバルセカンダリインデックスに適用されます。DynamoDB では、少し異なるローカルセカンダリインデックスも提供されています。ほとんどの場合、グローバルセカンダリインデックスが必要になります。違いの詳細については、グローバルセカンダリインデックスまたはローカルセカンダリインデックスの選択に関するこの記事をご覧ください。
この場合、パーティション キーが「Email」であるセカンダリ インデックスをテーブルに追加します。セカンダリ インデックスは次のようになります。
これは同じデータですが、異なる主キーで再編成されているだけであることに注目してください。これで、電子メール アドレスでユーザーを効率的に検索できます。
ある意味では、これは他のデータベースのインデックスと非常に似ています。どちらも、特定の属性の検索に最適化されたデータ構造を提供します。ただし、DynamoDB のセカンダリ インデックスは、いくつかの重要な点で異なります。
まず、そして最も重要なのは、DynamoDB のインデックスはメイン テーブルとはまったく異なるパーティションに存在することです。DynamoDB は、すべての検索を効率的かつ予測可能にし、線形水平スケーリングを提供したいと考えています。これを実現するには、クエリに使用する属性ごとにデータを再分割する必要があります。
他の分散データベースでは、通常、セカンダリ インデックス用にデータを再シャーディングしません。通常は、シャード上のすべてのデータのセカンダリ インデックスを維持するだけです。ただし、インデックスでシャード キーが使用されない場合は、シャード キーのないクエリでは、探しているデータを見つけるためにすべてのシャードにわたってスキャッター ギャザー操作を実行する必要があるため、データを水平方向にスケーリングする利点の一部が失われます。
DynamoDB のセカンダリ インデックスが他と異なる 2 つ目の点は、セカンダリ インデックスにアイテム全体をコピーする (ことが多い) ことです。リレーショナル データベースのインデックスの場合、インデックスにはインデックス付けされるアイテムのプライマリ キーへのポインターが含まれることがよくあります。インデックスで関連レコードを見つけた後、データベースはアイテム全体をフェッチする必要があります。DynamoDB のセカンダリ インデックスはメイン テーブルとは異なるノードにあるため、元のアイテムへのネットワーク ホップを回避する必要があります。代わりに、読み取りを処理するために必要なだけのデータをセカンダリ インデックスにコピーします。
DynamoDB のセカンダリ インデックスは強力ですが、いくつかの制限があります。まず、セカンダリ インデックスは読み取り専用です。セカンダリ インデックスに直接書き込むことはできません。代わりに、メイン テーブルに書き込み、DynamoDB がセカンダリ インデックスへのレプリケーションを処理します。次に、セカンダリ インデックスへの書き込み操作には料金がかかります。したがって、テーブルにセカンダリ インデックスを追加すると、テーブルの合計書き込みコストが 2 倍になることがよくあります。
セカンダリ インデックスとは何か、どのように機能するかがわかったので、次はセカンダリ インデックスを効果的に使用する方法について説明します。セカンダリ インデックスは強力なツールですが、誤用される可能性があります。セカンダリ インデックスを効果的に使用するためのヒントをいくつか紹介します。
最初のヒントは明白です。セカンダリ インデックスは読み取りにのみ使用できるため、セカンダリ インデックスでは読み取り専用パターンを設定する必要があります。しかし、この間違いを頻繁に見かけます。開発者は、まずセカンダリ インデックスから読み取り、次にメイン テーブルに書き込みます。これにより、余分なコストと余分な待ち時間が発生しますが、事前に計画を立てることで回避できる場合がよくあります。
DynamoDB データ モデリングについて読んだことがあるなら、まずアクセス パターンについて考える必要があることはご存知でしょう。これは、最初に正規化されたテーブルを設計し、次にそれらを結合するクエリを作成するリレーショナル データベースとは異なります。DynamoDB では、アプリケーションが実行するアクションについて考え、それらのアクションをサポートするようにテーブルとインデックスを設計する必要があります。
テーブルを設計するときは、まず書き込みベースのアクセス パターンから始めるようにしています。書き込みでは、ユーザー名の一意性やグループ内のメンバーの最大数など、何らかの制約を維持することがよくあります。理想的には、DynamoDB トランザクションを使用したり、競合状態になる可能性のある読み取り、変更、書き込みパターンを使用したりせずに、これを簡単に実行できるようにテーブルを設計したいと考えています。
これらを進めていくと、書き込みパターンと一致するアイテムを識別する「主要な」方法があることが一般的にわかります。これが最終的に主キーになります。その後、セカンダリ インデックスを使用して、追加のセカンダリ読み取りパターンを簡単に追加できます。
前のユーザーの例では、すべてのユーザー リクエストに組織とユーザー名が含まれる可能性があります。これにより、個々のユーザー レコードを検索したり、ユーザーによる特定のアクションを承認したりできるようになります。電子メール アドレスの検索は、「パスワードを忘れた場合」フローや「ユーザーを検索」フローなど、あまり目立たないアクセス パターンのために使用される場合があります。これらは読み取り専用のパターンであり、セカンダリ インデックスに適しています。
セカンダリ インデックスを使用する 2 つ目のヒントは、アクセス パターン内の変更可能な値にセカンダリ インデックスを使用することです。まずその理由を理解し、次にそれが適用される状況を見てみましょう。
DynamoDB では、 UpdateItem
オペレーションを使用して既存の項目を更新できます。ただし、更新で項目の主キーを変更することはできません。主キーは項目の一意の識別子であり、主キーを変更することは基本的に新しい項目を作成することです。既存の項目の主キーを変更する場合は、古い項目を削除して新しい項目を作成する必要があります。この 2 段階のプロセスは時間がかかり、コストがかかります。多くの場合、最初に元の項目を読み取り、次にトランザクションを使用して元の項目を削除し、同じリクエストで新しい項目を作成する必要があります。
一方、セカンダリ インデックスのプライマリ キーにこの変更可能な値がある場合、レプリケーション中に DynamoDB がこの削除 + 作成プロセスを処理します。値を変更するには、単純なUpdateItem
リクエストを発行するだけで、DynamoDB が残りの処理を行います。
このパターンは主に 2 つの状況で発生します。1 つ目は最も一般的なもので、並べ替えの基準となる可変属性がある場合です。ここでの標準的な例としては、人々が継続的にポイントを獲得するゲームのリーダーボードや、最近更新されたアイテムを最初に表示したい、継続的に更新されるアイテム リストなどがあります。ファイルを「最終更新日」で並べ替えることができる Google ドライブのようなものを想像してみてください。
これが発生する 2 番目のパターンは、フィルタリングする可変属性がある場合です。ここでは、ユーザーの注文履歴を持つ e コマース ストアについて考えてみましょう。ユーザーがステータス別に注文をフィルタリングできるようにし、'発送済み' または '配達済み' のすべての注文を表示できるようにしたい場合があります。これをパーティション キーまたはソート キーの先頭に組み込むと、完全一致フィルタリングが可能になります。アイテムのステータスが変わると、ステータス属性を更新し、DynamoDB を利用してセカンダリ インデックスでアイテムを正しくグループ化できます。
どちらの場合も、この変更可能な属性をセカンダリ インデックスに移動すると、時間とコストを節約できます。読み取り、変更、書き込みのパターンを回避することで時間を節約し、トランザクションの余分な書き込みコストを回避することでコストを節約できます。
さらに、このパターンは前のヒントとよく一致することに注意してください。以前のスコア、以前のステータス、または最後に更新された日時などの可変属性に基づいて、書き込み対象のアイテムを識別することはほとんどありません。むしろ、ユーザーの ID、注文 ID、またはファイルの ID などのより永続的な値で更新します。次に、セカンダリ インデックスを使用して、可変属性に基づいて並べ替えとフィルター処理を行います。
上で説明したように、DynamoDB は主キーに基づいてデータをパーティションに分割します。DynamoDB はこれらのパーティションを 10 GB 以下に小さく保つことを目指しており、DynamoDB のスケーラビリティのメリットを得るには、パーティション全体にリクエストを分散することを目指す必要があります。
これは通常、パーティション キーで高カーディナリティ値を使用する必要があることを意味します。ユーザー名、注文 ID、センサー ID などを考えてみましょう。これらの属性には多数の値があり、DynamoDB はトラフィックをパーティション全体に分散できます。
メイン テーブルではこの原則を理解しているのに、セカンダリ インデックスでは完全に忘れているというケースをよく見かけます。多くの場合、テーブル全体にわたってアイテムの種類ごとに順序付けをしたいのです。ユーザーをアルファベット順に取得したい場合は、すべてのユーザーのパーティション キーとしてUSERS
、ソート キーとしてユーザー名を持つセカンダリ インデックスを使用します。または、e コマース ストアで最新の注文を順序付けしたい場合は、すべての注文のパーティション キーとしてORDERS
、ソート キーとしてタイムスタンプを持つセカンダリ インデックスを使用します。
このパターンは、DynamoDB パーティションのスループット制限に近づかない小規模トラフィックのアプリケーションでは機能しますが、高トラフィックのアプリケーションでは危険なパターンです。すべてのトラフィックが単一の物理パーティションに集中し、そのパーティションの書き込みスループット制限にすぐに達する可能性があります。
さらに、最も危険なのは、メイン テーブルに問題が発生する可能性があることです。レプリケーション中にセカンダリ インデックスの書き込みが制限されると、レプリケーション キューがバックアップされます。このキューがバックアップされすぎると、DynamoDB はメイン テーブルへの書き込みを拒否し始めます。
これはユーザーを支援するために設計されています。DynamoDB はセカンダリ インデックスの古さを制限したいので、大きな遅延のあるセカンダリ インデックスを防止します。ただし、予期しないときに突然発生する驚くべき状況になる場合があります。
セカンダリ インデックスは、すべてのデータを新しいプライマリ キーで複製する方法であると考えられることがよくあります。ただし、すべてのデータをセカンダリ インデックスに格納する必要はありません。インデックスのキー スキーマと一致しない項目がある場合、その項目はインデックスに複製されません。
これは、データにグローバル フィルターを提供するのに非常に便利です。このために私が使用する標準的な例は、メッセージの受信トレイです。メイン テーブルには、特定のユーザーのすべてのメッセージを、作成時刻順に保存できます。
しかし、あなたも私と同じなら、受信トレイにたくさんのメッセージが入っているでしょう。さらに、未読メッセージを「ToDo」リスト、つまり誰かに返信するための小さなリマインダーとして扱うかもしれません。したがって、私は通常、受信トレイの未読メッセージだけを見たいと思っています。
セカンダリインデックスを使用しunread == true
のグローバルフィルターを提供できます。セカンダリインデックスのパーティションキーは${userId}#UNREAD
のようなもので、ソートキーはメッセージのタイムスタンプです。最初にメッセージを作成すると、セカンダリインデックスのパーティションキー値が含まれるため、未読メッセージのセカンダリインデックスにレプリケートされます。後でユーザーがメッセージを読んだときに、 status
をREAD
に変更し、セカンダリインデックスのパーティションキー値を削除できます。その後、DynamoDB はそれをセカンダリインデックスから削除します。
私はいつもこのトリックを使っていますが、これは驚くほど効果的です。さらに、スパース インデックスを使用するとコストを節約できます。読み取りメッセージの更新はセカンダリ インデックスに複製されないため、書き込みコストを節約できます。
最後のヒントとして、前のポイントをもう少し詳しく説明しましょう。先ほど、アイテムにインデックスの主キー要素がない場合、DynamoDB はセカンダリ インデックスにアイテムを含めないことを説明しました。このトリックは、主キー要素だけでなく、データ内の非キー属性にも使用できます。
セカンダリ インデックスを作成するときに、メイン テーブルのどの属性をセカンダリ インデックスに含めるかを指定できます。これは、インデックスの投影と呼ばれます。メイン テーブルのすべての属性、主キー属性のみ、または属性のサブセットを含めるように選択できます。
すべての属性をセカンダリ インデックスに含めたくなるかもしれませんが、これはコストのかかる間違いになる可能性があります。投影された属性の値を変更するメイン テーブルへのすべての書き込みは、セカンダリ インデックスに複製されることに注意してください。完全な投影を含む 1 つのセカンダリ インデックスでは、テーブルの書き込みコストが実質的に 2 倍になります。セカンダリ インデックスを追加するたびに、書き込みコストが1/N + 1
増加します。ここで、 N
新しいセカンダリ インデックスの前のセカンダリ インデックスの数です。
さらに、書き込みコストはアイテムのサイズに基づいて計算されます。テーブルに書き込まれる 1 KB のデータごとに WCU が使用されます。4 KB のアイテムをセカンダリ インデックスにコピーする場合は、メイン テーブルとセカンダリ インデックスの両方で 4 つの WCU 全額を支払うことになります。
したがって、セカンダリ インデックスの投影を狭めることでコストを節約できる方法は 2 つあります。まず、特定の書き込みを完全に回避できます。セカンダリ インデックスの投影の属性にまったく影響しない更新操作がある場合、DynamoDB はセカンダリ インデックスへの書き込みをスキップします。次に、セカンダリ インデックスにレプリケートされる書き込みについては、レプリケートされる項目のサイズを減らすことでコストを節約できます。
このバランスを正しく取るのは難しい場合があります。セカンダリ インデックスの投影は、インデックスの作成後は変更できません。セカンダリ インデックスに追加の属性が必要になった場合は、新しい投影を使用して新しいインデックスを作成し、古いインデックスを削除する必要があります。
セカンダリ インデックスに関する実用的なアドバイスをいくつか検討したところで、一歩下がって、より根本的な質問をしてみましょう。セカンダリ インデックスはそもそも使用すべきでしょうか?
これまで見てきたように、セカンダリ インデックスはデータに別の方法でアクセスするのに役立ちます。ただし、これには追加の書き込みという犠牲が伴います。したがって、セカンダリ インデックスに関する私の経験則は次のとおりです。
読み取りコストの削減が書き込みコストの増加を上回る場合は、セカンダリ インデックスを使用します。
これは言うと明らかなようですが、モデリングしているときには直感に反することがあります。他のアプローチを考えずに「セカンダリ インデックスに投入する」と言うのは非常に簡単なように思えます。
このことを理解するために、セカンダリ インデックスが意味をなさない可能性がある 2 つの状況を見てみましょう。
DynamoDB では、通常、主キーを使用してフィルタリングを行う必要があります。DynamoDB でクエリを使用し、アプリケーションで独自のフィルタリングを実行するたびに、少しイライラします。なぜそれを主キーに組み込めないのでしょうか?
私の本能的な反応にもかかわらず、データを過剰に読み取り、アプリケーションでフィルタリングしたい状況がいくつかあります。
これが発生する最も一般的な場所は、ユーザーに対してデータにさまざまなフィルターを提供したいが、関連するデータ セットが制限されている場合です。
ワークアウト トラッカーについて考えてみましょう。ワークアウトの種類、強度、期間、日付など、多くの属性でユーザーがフィルターできるようにしたい場合があります。ただし、ユーザーのワークアウトの数は管理可能な範囲です。パワー ユーザーでも 1,000 を超えるワークアウトはしばらくかかるでしょう。これらすべての属性にインデックスを設定するのではなく、ユーザーのワークアウトをすべて取得して、アプリケーションでフィルターすることができます。
ここでは計算を行うことをお勧めします。DynamoDB を使用すると、これら 2 つのオプションを簡単に計算して、アプリケーションにどちらがより適しているかを把握できます。
状況を少し変えてみましょう。アイテム コレクションが大きい場合はどうなるでしょうか。ジム用のワークアウト トラッカーを構築していて、ジムのオーナーがジムのすべてのユーザーに対して、上記のすべての属性をフィルターできるようにしたい場合はどうでしょうか。
これにより状況が変わります。ここでは、数百または数千人のユーザーがおり、それぞれが数百または数千のワークアウトを行っている状況です。アイテム コレクション全体を過度に読み取り、結果に対して事後フィルタリングを行うのは意味がありません。
しかし、セカンダリ インデックスはここでもあまり意味がありません。セカンダリ インデックスは、関連するフィルターが存在することが期待できる既知のアクセス パターンに適しています。ジムのオーナーがさまざまな属性 (すべてオプション) でフィルターできるようにしたい場合、これを機能させるには多数のインデックスを作成する必要があります。
クエリ プランナーの潜在的な欠点については前に説明しましたが、クエリ プランナーには利点もあります。より柔軟なクエリを可能にすることに加えて、クエリを作成する際に複数のインデックスからの部分的な結果を確認するインデックスの交差などの操作も実行できます。同じことを DynamoDB でも実行できますが、アプリケーションとのやり取りが頻繁に発生し、それを理解するための複雑なアプリケーション ロジックが必要になります。
このような種類の問題が発生した場合、私は通常、このユースケースに適したツールを探します。データセット全体にわたって柔軟なセカンダリインデックスのようなフィルタリングを提供するツールとして、 RocksetとElasticsearch をおすすめします。
この記事では、DynamoDB セカンダリ インデックスについて学習しました。まず、DynamoDB の仕組みとセカンダリ インデックスが必要な理由を理解するために、いくつかの概念的な部分を確認しました。次に、セカンダリ インデックスを効果的に使用する方法を理解し、その特定の癖を知るために、いくつかの実用的なヒントを確認しました。最後に、セカンダリ インデックスについてどのように考えるべきかを確認し、他のアプローチを使用する必要がある場合を確認しました。
セカンダリ インデックスは DynamoDB ツールボックスの強力なツールですが、万能薬ではありません。すべての DynamoDB データ モデリングと同様に、始める前にアクセス パターンを慎重に検討し、コストを計算してください。
セカンダリ インデックスのようなフィルタリングに Rockset を使用する方法の詳細については、Alex DeBrie のブログ「DynamoDB フィルタリングと集計クエリ (Rockset での SQL 使用)」を参照してください。