データ ストアが適切なセカンダリ インデックスを提供しない場合に、アプリケーションがクエリで頻繁に使用するデータの個別の参照テーブルを作成および管理します。 この方法では、クエリが主キーまたはパーティション キーを使用しない場合に完全なデータ スキャンを回避することで、読み取りパフォーマンスが向上します。
コンテキストと問題
多くのデータ ストアでは、主キーを使用してエンティティのコレクションのデータを整理します。 アプリケーションは、このキーを使ってデータを探し、取得することができます。 次の図は、主キーである顧客 ID で編成された顧客情報を保持するデータ ストアの例を示しています。
この図は、顧客レコードの 2 列のテーブルを示しています。 最初の列である主キー (顧客 ID) には、顧客 ID 1 から 9 が続き、その後に省略記号、ID 1000、および別の省略記号が含まれます。 2 番目の列 Customer Data には、姓と町の後に省略記号が続く顧客データが含まれています。 表示される例としては、姓 Smith と町レドモンドにマップされる ID 1、姓の Jones とシアトルの町にマップされる ID 2 などがあります。 もう 1 つの行はシカゴのスミスを示し、もう 1 行はレドモンドのスミスを示しています。 別の行には、シカゴにいるジョーンズが表示されています。
主キーは、このキーの値に基づいてデータをフェッチするクエリでは重要ですが、他のフィールドに基づいてデータを取得する必要があるアプリケーションでは、そのクエリに主キーを使用できません。 この顧客情報の例で言えば、顧客の所在地 (Town) など、主キー (Customer ID) 以外の属性の値を参照してデータを照会するだけでは、顧客を取得することができません。 町のみを参照するクエリを実行するには、アプリケーションがすべての顧客レコードをフェッチして調べる必要があり、プロセスが遅くなる可能性があります。
多くのリレーショナル データベース管理システムでは、セカンダリ インデックスがサポートされています。 セカンダリ インデックスは、1 つ以上の非プライマリ (セカンダリ) キー フィールドで構成される個別のデータ構造です。 セカンダリ インデックスは、インデックスが作成された各値のデータが格納される場所を示します。 セカンダリ インデックス内の要素は通常、高速にデータを検索できるようセカンダリ キーの値で並べ替えられます。 通常、データベース管理システムは、これらのインデックスを自動的に保持します。
リレーショナル データベースを使用すると、複数のセカンダリ インデックスでさまざまなクエリ パターンをサポートできます。 たとえば、Customer ID が主キーであるリレーショナル データベースの Customers テーブルでは、アプリケーションが居住する町で顧客を頻繁に検索する場合は、Town フィールドにセカンダリ インデックスを追加すると便利です。
セカンダリ インデックスはリレーショナル システムでは一般的ですが、すべてのデータ ストアで同等の機能が提供されるわけではありません。 セカンダリ インデックスがないデータ ストアもあれば、ワークロードのクエリ、パーティション分割、またはパフォーマンスの要件を満たさないインデックスを提供するデータ ストアもあります。 このような場合、アプリケーションはフル スキャンと手動インデックス管理のどちらかを選択する必要があります。
ソリューション
指定したキーでデータを整理するインデックス テーブルを作成します。 次のセクションでは、インデックス テーブルを構築するための 3 つの一般的な戦略について説明します。 以降の 2 つのセクションでは、特定のシナリオでのインデックス テーブルの使用について説明します。
標準的なストラクチャリング戦略
次の方法は、インデックス テーブルを構成するために一般的に使用されます。 シナリオで必要なセカンダリ インデックスの数と、アプリケーションが実行するクエリの性質に基づいて戦略を選択します。
完全な非正規化
完全な非正規化では、各インデックス テーブルのデータが複製されますが、主キー以外のキーによって整理されます。 次の図は、Town と LastName ごとに同じ顧客情報を整理するインデックス テーブルを示しています。
この戦略は、データの変更頻度が低い読み取り負荷の高いワークロードに適しています。 更新速度が上がるにつれて、すべてのコピーを維持すると処理オーバーヘッドが増加します ( 「整合性の複雑さ」を参照)。 大量のデータセットの場合、コピーを格納するには、大きな領域が必要な場合もあります。
正規化されたインデックス
正規化されたインデックス テーブルでは、主キー以外のキーによってデータが整理されます。 正規化されたインデックス テーブルは、次の図に示すように、そのデータを複製するのではなく、主キーを使用して元のデータを参照します。 元のデータは、ファクト テーブルと呼ばれます。
Tip
ここで、 ファクト テーブル は、インデックスによって参照される権限のあるソース テーブルを意味します。 次元データ モデルを意味するものではありません。
この手法なら、領域が節約され、重複データを管理するオーバーヘッドも小さくて済みます。 欠点は、アプリケーションがセカンダリ キーを使用してデータを検索するために 2 つの検索操作を実行する必要があることです。 アプリケーションは、インデックス テーブル内のデータの主キーを検索し、主キーを使用してファクト テーブル内のデータを検索する必要があります。
部分的な非正規化
部分非正規化では、頻繁に取得されるフィールドを複製し、主キー以外のキーで編成されるインデックス テーブルが作成されます。 ファクト テーブルは、アクセス頻度の低いフィールドにアクセスするために参照されます。 次の図は、一般的にアクセスされるデータが各インデックス テーブルでどのように重複しているかを示しています。
この戦略は、最初の 2 つのアプローチのバランスを取ります。 1 回の検索を使用して一般的なクエリのデータをすばやく取得できますが、領域とメンテナンスのオーバーヘッドはデータセット全体を複製するほど大きくはありません。
複合キー
一部のアプリケーションでは、"Redmond に住み、姓が Smith であるすべての顧客を検索する" など、値の組み合わせを指定してデータのクエリを実行する場合が多いです。このような場合は、Town 属性や LastName 属性など、複数の属性からインデックス キーを作成します。
異なる値の組み合わせで同じキーを生成できないように、コンポーネントの境界を保持するエンコードを使用します。 クエリがキーの順序に依存する場合は、データ ストアのキー照合順序規則の下で、エンコードで必要な並べ替え順序が保持されるようにします。
次の図は、複合キーに基づくインデックス テーブルを示しています。 キーは Town で並べ替えられた後、Town の値が同じレコードの場合は LastName で並べ替えられます。
この図には、左側にインデックス テーブル、右側にファクト テーブルが含まれています。 ファクト テーブルは、顧客データを主キーである顧客 ID 別に整理します。 各顧客データ行には、顧客の姓、町、および他のデータを示す省略記号が含まれています。 インデックス テーブルは、Town と LastName を組み合わせた複合キーによって編成されます。 2 番目の列である顧客参照 (ID) と一般的にクエリされるデータには、顧客 ID と省略記号が含まれています。 矢印はインデックス テーブルの行からファクト テーブルまで拡張され、各矢印は、そのインデックス エントリによって参照される顧客 ID 行を指します。 交差する矢印は、複合キーで順序付けされたエントリが、ファクト テーブル内の異なる位置にある顧客行を参照できることを示しています。
シャード化されたデータ上のテーブルのインデックス作成
インデックス テーブルを使用すると、シャード 化されたデータに対するクエリ操作を高速化できます。 これらは、シャード キーがハッシュされている場合に特に便利です。 次の図は、シャード キーが顧客 ID のハッシュである例を示しています。 インデックス テーブルは、ハッシュされていない値 (Town と LastName) でエントリを整理し、対応するハッシュされたシャード キーを各エントリと共に格納します。
この配置では、正しいシャードから各レコードを取得するために必要なルーティング情報を提供しながら、ハッシュされていない値の範囲と順序付けされた参照がサポートされます。 たとえば、"Redmond に住んでいるすべての顧客を検索する" などのクエリは、インデックス テーブル内の連続したブロック内の一致する項目を見つけることができます。 その後、アプリケーションは、インデックス テーブルに格納されているシャード キーを使用して、顧客データへの参照に従います。
問題と考慮事項
このパターンを実装する方法を決定するときは、次の点を考慮してください。
保守のオーバーヘッド。 セカンダリ インデックスを維持すると、オーバーヘッドが大幅に増加する可能性があります。 アプリケーションで使用されるクエリを分析して理解します。 インデックス テーブルは、定期的に使用する可能性が高い場合にのみ作成します。 アプリケーションが実行しないクエリや、たまにしか実行しないクエリをサポートする投機的インデックス テーブルは作成しないでください。 クエリ パターンの数が増えるにつれてインデックス テーブルの数が増え、それぞれが監視、保守、デバッグするための運用面を追加します。 各インデックス テーブルのクエリの利点を、別の派生データ構造を維持するための運用コストと比較します。 既存のインデックス テーブルを定期的に確認して、クエリが実行されなくなったテーブルを特定して削除します。
ストレージとスループットのコスト。 インデックス テーブル内のデータを複製すると、インデックス テーブルの数とコピーされたフィールドのサイズに比例してストレージ コストが増加します。 データの複数のコピーを保持すると、作業も増えます。 インデックス テーブルに対するすべての書き込みでは、ストレージ アカウントのAzure Table Storage単位の制限に対するトランザクションや、Azure Cosmos DBの要求ユニットに対するトランザクションなど、スループット容量が消費されます。 コストはストレージを超えて書き込み側のスループットまで拡張されます。
二重検索ペナルティ。 元のデータを参照する正規化された構造としてインデックス テーブルを実装する場合、アプリケーションは、データを見つけ出すために 2 回の検索操作を実行する必要があります。 1 回目の検索操作でインデックス テーブルから主キーを取得し、2 回目の操作でその主キーを使って目的のデータをフェッチします。
一貫性の複雑さ。 大規模なデータセットに対して多数のインデックス テーブルがシステムに組み込まれている場合、インデックス テーブルと元のデータの間の整合性を維持することは困難な場合があります。 ソース データとインデックス エントリを同じトランザクションで更新できない場合は、最終的な整合性モデルを中心にアプリケーションを設計します。 書き込みを確認する前に、各ソース変更を永続的にキャプチャします。 たとえば、データベース変更フィードを使用したり、 トランザクション送信トレイ パターン を使用して、ソースの変更と同じトランザクションに送信トレイ レコードを書き込んだり、ソース データを変更する前にコマンドをエンキューしたりできます。 コマンド ベースのアプローチでは、worker を使用してコマンドを処理し、ソース データとそのインデックスを更新します。 ソース データを更新してから、インデックス更新メッセージを個別に発行しないでください。これらの操作の間でエラーが発生すると、インデックスが古くなる可能性があるためです。
メッセージ配信や再試行により同じ更新処理が複数回実行される可能性があるため、非同期コンシューマーは べき等 になるように設計してください。 同じソース レコードの更新も、順不同で到着する可能性があります。 各インデックスの更新にソース バージョンまたはシーケンス番号を含め、インデックス内のバージョンよりも新しい場合にのみ更新プログラムを適用します。 削除の場合は、遅れて到着した古い更新によって削除されたインデックスエントリが再作成されないよう、バージョン付きのトゥームストーンまたは同等のハイウォーターマークを保持します。 ソース データの書き込みと非同期インデックスの更新の間に、インデックス テーブルに対するクエリは、ソース データで更新または削除されたレコードへの古い参照を返す可能性があり、最近追加されたレコードを省略できます。
インデックス テーブルのパーティション分割。 インデックス テーブル自体はパーティション分割またはシャード化される可能性があるため、クエリ ルーティングが複雑になり、インデックスが提供するように設計されているクエリ パターンに合わせてパーティション分割戦略が必要になります。
このパターンを使用する場合
アプリケーションがプライマリ (またはシャード) キー以外のキーを使用してデータを取得する必要が多く、データ ストアがセカンダリ インデックスをネイティブにサポートしていない場合、またはそのネイティブ インデックスがワークロードのクエリ、パーティション分割、またはパフォーマンスの要件を満たしていない場合に、このパターンを使用します。
このパターンは、次の場合に適さない場合があります。
データの変化が激しい。 データは頻繁に変更されるため、書き込み速度はインデックス テーブルを非同期的に更新できる速度を超えています。 古さの許容期間は、インデックスが恒久的に最新でなくなるまで拡大し、その結果インデックスは役に立たなくなり、インデックス テーブルの維持に伴うストレージとスループットのオーバーヘッドが、クエリで得られる削減効果を上回るようになります。
区別できないキーがあります。 インデックス テーブルのセカンダリ キーとして選択されたフィールドは非検出であり、小さな値のセット (たとえば、項目がアクティブかどうかを記録するブール型フィールド) のみを持つことができます。 インデックス テーブルでは、ストレージとスループットの完全なコストが発生しますが、クエリの選択度は最小限に抑えられます。
データ値には偏った分布があります。 インデックス テーブルのセカンダリ キーとして選択されたフィールドのデータ値のバランスが非常に偏ります。 たとえば、レコードの 90% にフィールドに同じ値が含まれている場合、インデックス テーブルを作成して保持し、このフィールドに基づいてデータを検索すると、データを順次スキャンするよりもオーバーヘッドが大きくなります。 ただし、クエリが残りの 10% にある値を頻繁に対象とする場合、このインデックスは引き続き役立ちます。
ワークロード設計
アーキテクトは、ワークロード設計でインデックス テーブル パターンを使用して、Azure Well-Architected Framework の柱で説明されている目標と原則に対処する方法を評価する必要があります。 次の表は、このパターンが各柱の目標をサポートする方法に関するガイダンスを示しています。
| 柱 | このパターンが柱の目標をサポートする方法 |
|---|---|
| 信頼性 は、冗長性を構築し、障害発生時に機能を維持することで、ワークロードが 回復性と回復のターゲット を満たすのに役立ちます。 | インデックスの非同期メンテナンスにより、一時的なインデックス更新エラーによってソース データの書き込みがブロックされるのを防ぐことができます。 べき等処理、デッドレター処理、監視、整合性の照合は、障害発生後のインデックスの整合性の回復に役立ちます。 - RE:07 自己保護 - RE:10 監視 |
| パフォーマンス効率 は、スケーリング、データ、およびコードの最適化を通じて、ワークロード の需要を効率的に満たすのに役立ちます。 | インデックス テーブルを使用すると、完全なデータ スキャンを必要とせずに、主キー以外のフィールドを高速に検索できます。 シャード 化されたデータ ストアの場合、インデックス テーブルでは、シャード キーだけでは効率的に機能できない範囲と順序付けのクエリをサポートするために、ハッシュされていない値でエントリを整理できます。 - PE:05 スケーリングとパーティショニング - PE:08 データパフォーマンス |
設計上の決定と同様に、このパターンによって柱内にトレードオフが生じる場合は、他の柱の目標に照らして検討してください。
Example
映画に関する情報を格納するアプリケーションを考えてみましょう。 カタログが大きく読み取り負荷が高く、各映画に 1 つの主要なジャンルがあり、アクター クエリが頻繁に実行され、キャスト データの変更が頻繁に行われず、インデックスの短いラグが許容されるとします。 Table Storage は、各エンティティを名前付きプロパティの構造化されたセットとして格納します。 すべてのエンティティには PartitionKey、 RowKey、タイムスタンプが含まれており、同じテーブル内のエンティティには異なるプロパティ セットを含めることができます。
Table Storage では、 PartitionKey と RowKeyで構成される複合主キーが使用されます。
PartitionKey値は、エンティティが格納されるパーティションを決定します。 パーティション内では、 RowKey 値によってエンティティが一意に識別されます。 Table Storage は、両方のキーを指定するクエリ、または 1 つのパーティション内の連続した行キー値の範囲をフェッチするクエリ用に最適化されています。
Tip
Table Storage では、エンティティ グループ トランザクションを通じて、同じテーブルとパーティション内のエンティティのトランザクション更新がサポートされます。 トランザクションは、ファクト テーブルと別のインデックス テーブルにまたがることができません。 ファクト エンティティとインデックス エンティティをアトミックに更新するには、同じ PartitionKeyを持つ同じテーブルに格納します。 エンティティ グループ トランザクションは、バッチあたり 100 個のエンティティに制限され、最大ペイロードは 4 MiB です。
この例では、エンコードされたジャンル識別子をパーティション キーとして使用し、安定した一意のムービー識別子を行キーとして使用して、各ジャンルのパーティションを含むAzure テーブルを作成します。 ジャンル名と映画名をプロパティとして格納します。 次の図では、識別子の代わりに読み取り可能な名前を使用して、例のフォローを容易にします。
この方法は、アプリケーションで主演俳優別に映画を検索する必要もある場合、有効性が低くなります。 その場合は、インデックス テーブルとして機能する個別のAzure テーブルを作成します。 パーティション キーとしてエンコードされた安定したアクター識別子を使用し、行キーとしてムービー識別子を使用します。 アクター名とムービー名をプロパティとして格納します。 次の図では、識別子の代わりに読み取り可能な名前を使用します。 映画が複数のアクターをスターしている場合、同じムービーが複数のパーティションで発生します。
次の図は、アクター インデックス テーブルを示しています。
設計チュートリアル
ムービー テーブルでは、ジャンルをパーティション キーとして使用します。つまり、ジャンルでフィルター処理するクエリは、連続する行キー範囲に対するパーティション スキャンとして効率的に実行されます。 ただし、Table Storage では、 PartitionKey と RowKeyでサポートされるクラスター化インデックスは 1 つだけです。 セカンダリ インデックスはありません。 "特定のアクターが出演しているすべての映画を検索する" などのクエリでは、すべてのジャンル パーティションにわたる完全なテーブル スキャンが必要です。これは大規模にコストがかかります。
アクター インデックス テーブルは、アクセス パターンを逆にすることで、この制限に対処します。 各アクター識別子はパーティション キーになり、各ムービー識別子は行キーになるため、アクターベースのクエリは効率的なパーティション参照として解決されます。 各パーティションはアクターのムービーを 1 つだけ保持するため、クエリは関連のないデータをスキャンせずに連続したエンティティ範囲を返します。
ムービー エントリとアクター エントリは別々のテーブルとパーティション キーを使用するため、これらのエントリはエンティティ グループ トランザクションを共有できません。 永続的な非同期変更キャプチャ メカニズムを通じてアクター インデックスを維持し、クエリ結果の短時間の古さを許容できるようにアプリケーションを設計します。
インデックス テーブルは部分的な非正規化を適用します。各エントリは、一般的にアクセスされるフィールド (他のアクターの名前など) を複製するため、最も頻繁なクエリは 1 回の検索でインデックス テーブルから単独で応答できます。 アクセス頻度の低いフィールドの場合、エントリには元のムービー テーブルのジャンル パーティション キーが含まれるため、完全なレコードのジャンル パーティションに対するターゲット ポイント クエリが有効になります。 この設計では、クエリ速度とストレージ コストとメンテナンスのオーバーヘッドのバランスが取られます。
次のステップ
Table Storage クエリの設計ガイダンス では、同じパーティションまたは別のパーティションにインデックス エントリを格納する方法など、
PartitionKeyとRowKeyを使用するセカンダリ インデックス パターンについて説明します。データパーティション分割戦略 では、クエリ パターン、データ分散、トランザクション要件、スケーラビリティの目標に基づいてパーティション キーと行キーを選択する方法について説明します。
データ パフォーマンスを最適化するためのアーキテクチャ戦略 では、クエリ パターン、インデックス、パーティション、監視の要件を評価するためのガイダンスが提供されます。
Azure Cosmos DBの整合性レベルでは、Azure Cosmos DBでインデックス テーブルを管理するときに関連する整合性モデルについて説明します。
関連するリソース
このパターンを実装する場合は、次のパターンも関連している可能性があります。
シャーディング パターン。 一般にインデックス テーブル パターンは、シャードを使ってパーティション分割されたデータと組み合わせて使用します。 シャーディング パターンでは、データ ストアを一連のシャードに分割する方法について説明します。
Materialized View Pattern (具体化されたビュー パターン) データを集計するクエリをサポートするためにデータのインデックスを作成する代わりに、データの具体化されたビューを作成する方が適切な場合があります。 このパターンでは、効率的なサマリー クエリをサポートするために、データに対して事前設定されたビューを生成する方法について説明します。
トランザクショナル アウトボックス パターン。 トランザクション送信トレイ パターンを使用すると、ソース データとインデックス エントリを 1 つのトランザクションで更新できない場合に、非同期インデックスメンテナンスの変更を確実に発行できます。