このトピックでは、テーブル設計のベストプラクティスについて説明します。
適切に設計されたプライマリキー
Tablestoreは、パーティションキーに基づいてテーブルデータをパーティションに動的に分割し、各パーティションは1つのサーバーノードでホストされます。パーティションキーは、最小パーティション単位として使用されます。同じパーティションキー値のデータは、それ以上分割できません。アプリケーションは、Tabelstoreの機能を活用するために、パーティション全体でデータ分散とアクセス分散のバランスを取る必要があります。
Tablestoreは、プライマリキーによってテーブルの行をソートします。適切に設計されたプライマリキーは、パーティション全体でのデータ分散のバランスをより適切に取ることができます。このようにして、Tablestoreの高いスケーラビリティを最大限に活用できます。
パーティションキーを選択する際は、以下の点に注意してください。
1つのパーティションキー値のすべての行のデータは、10 GBを超えることはできません。
説明10 GBは厳密な制限ではありませんが、ホットスポットを避けるために推奨されます。
同じテーブルの異なるパーティションキー値のデータは、論理的に独立しています。
連続したパーティションキー値の狭い範囲にアクセス負荷を集中させないでください。
例
たとえば、あるテーブルには、大学のすべての学生のトランザクションのレコードが学生IDカードを使用して格納されています。プライマリキー列には、CardID、SellerID、DeviceID、OrderNumberが含まれます。CardIDは学生証のID、SellerIDは販売者のID、DeviceIDはPOS端末のID、OrderNumberは注文番号を示します。レコードは以下の規則に従います。
各学生証は1つのCardIDに対応し、各販売者は1つのSellerIDに対応します。
各POS端末は、グローバルに一意のDeviceIDに対応します。
POS端末で生成された購入ごとに、1つのOrderNumberが記録されます。端末で生成されたOrderNumberは、その端末に対して一意ですが、グローバルに一意ではありません。たとえば、異なるPOS端末が同じOrderNumberを使用して2つの別々の購入レコードを生成する場合があります。
同じPOS端末で生成された各OrderNumberには、異なるタイムスタンプがあります。新しい購入レコードは、以前の購入レコードよりも大きな順次OrderNumberを持ちます。
すべての購入レコードはリアルタイムでテーブルに書き込まれます。
Tablestoreを最適に使用するには、Tablestoreでテーブルのプライマリキーを設計する際に、次のパーティションキーを検討してください。
パーティショニングモード | 説明 |
テーブルのパーティションキーとしてCardIDを使用する | テーブルのパーティションキーとしてCardIDを使用することをお勧めします。一般的に、1日に各カードの購入レコード数は同じであるため、各パーティションのアクセス圧力はバランスが取れています。これにより、予約済みの読み取り/書き込みスループットを効率的に利用できます。 |
テーブルのパーティションキーとしてSellerIDを使用する | テーブルのパーティションキーとしてSellerIDを使用することはお勧めしません。学校の販売者の数は比較的少なく、一部の販売者は多数の購入レコードを生成してホットスポットになる可能性があります。これは、各パーティションのアクセス圧力のバランスを取るのに役立ちません。 |
テーブルのパーティションキーとしてDeviceIDを使用する | テーブルのパーティションキーとしてDeviceIDを使用することをお勧めします。1日に各販売者の購入レコード数は異なる場合でも、1日に各購入デバイスで生成される購入レコード数を推定できます。この推定は、レジ係の注文処理速度に基づいて計算され、1日に購入デバイスで生成できる購入レコード数が決まります。したがって、DeviceIDは、アクセス圧力のバランスの取れた分散を保証するためのテーブルのパーティションキーとして適しています。 |
テーブルのパーティションキーとしてOrderNumberを使用する | テーブルのパーティションキーとしてOrderNumberを使用することはお勧めしません。OrderNumberは、同時に生成された購入注文が順番に増加するため、同じ期間に注文がグループ化されるため、お勧めしません。これは、読み取り/書き込みスループットの有効性を制限します。OrderNumberをパーティションキーにする必要がある場合は、ハッシュして結果のハッシュ値をOrderNumberプレフィックスとして使用できます。このプロセスにより、データが均等に分散され、分散圧力が軽減されます。 |
まとめ
テーブルのパーティションキーとして、SellerIDまたはOrderNumberではなく、CardIDまたはDeviceIDを使用することをお勧めします。パーティションキーを指定した後、アプリケーションの実際の要件に基づいて残りのプライマリキー列を設計できます。
連結パーティションキー
Tablestoreを最適に使用するには、単一のパーティションキー値のデータ量が10 GBを超えないようにすることをお勧めします。単一のテーブルパーティションキー値のすべての行の合計データ量が10 GBを超える場合は、テーブルを設計する際に、複数の元のプライマリキー列をパーティションキーに連結できます。
例
前の学生証購入レコードの例のように、プライマリキー列がDeviceID、SellerID、CardID、OrderNumberであるとします。DeviceIDはこのテーブルのパーティションキーであり、単一のDeviceIDのすべての行からの合計データ量は10 GBを超える場合があります。この場合、DeviceID、SellerID、CardIDをテーブルの最初のプライマリキー列(パーティションキー)として連結します。
次の表は、元のテーブルを示しています。
DeviceID | SellerID | CardID | OrderNumber | attrs |
16 | 'a100' | 66661 | 200001 | ... |
54 | 'a100' | 6777 | 200003 | ... |
54 | 'a1001' | 6777 | 200004 | ... |
167 | 'a101' | 283408 | 200002 | ... |
次の表は、DeviceID、SellerID、CardIDを連結して作成されたパーティションキーを示しています。
CombineDeviceIDSellerIDCardID | OrderNumber | attrs |
'16:a100:66661' | 200001 | ... |
'167:a101:283408' | 200002 | ... |
'54:a1001:6777' | 200004 | ... |
'54:a100:6777' | 200003 | ... |
元のテーブルでは、DeviceIDが54の2つの行は、同じパーティションキー値を持つ2つの購入レコードを示しています。新しく作成されたテーブルでは、これら2つの購入レコードは異なるパーティションキー値を持ちます。複数のプライマリキー列を連結してパーティションキーを形成することにより、テーブル内の各パーティションキー値の合計データ量を削減できます。
DeviceIDとSellerIDを連結する代わりに、DeviceID、SellerID、CardIDを連結してパーティションキーを作成します。これは、前のセクションで説明したテーブルでは、同じDeviceIDを持つすべての購入レコードが同じSellerIDを持つためです。単一パーティションのデータが多すぎるという問題は、DeviceIDとSellerIDを連結するだけでは解決できません。
プライマリキー列を連結してテーブルを作成することには、いくつかの欠点があります。DeviceIDはINTEGERプライマリキー列です。元のテーブルでは、DeviceIDが54の購入レコードは、DeviceIDが167の購入レコードの前にリストされています。最初の3つのプライマリキー列をSTRINGプライマリキー列に連結した後、DeviceIDが54の購入レコードは、DeviceIDが167の購入レコードの後にリストされます。アプリケーションがDeviceIDが15から100の範囲のすべての購入レコードを読み取る必要がある場合、上記のテーブルは最適ではありません。
この状況に対処するには、DeviceIDの前にゼロを追加できます。追加するゼロの数は、DeviceIDの最大桁数によって決まります。DeviceIDの範囲が0から999999の場合は、すべてのDeviceIDが6桁になるようにゼロを追加してから連結できます。次の表は、結果のテーブルを示しています。
CombineDeviceIDSellerIDCardID | OrderNumber | attrs |
'000016:a100:66661' | 200001 | ... |
'000054:a1001:6777' | 200004 | ... |
'000054:a100:6777' | 200003 | ... |
'000167:a101:283408' | 200002 | ... |
ただし、IDの前にゼロを埋め込んだ後でも、テーブルはまだ完全に最適化されていません。これは、DeviceIDが54の2つの行と、SellerIDが'a1001'の行が、SellerIDが'a100'の行の後にリストされているためです。この不一致は、コネクタとして:
が使用されていることが原因であり、辞書順に影響します。その結果、'000054:a1001'は辞書順では'000054:a100:'よりも小さいですが、'a1001'は'a100'よりも大きくなります。
この問題を解決するには、他のすべての使用可能な文字のASCIIコードよりも小さい文字を選択します。このテーブルでは、SellerID値は文字と数字を使用します。,
のASCIIコードはSellerIDで使用可能なすべての文字のASCIIコードよりも小さいため、コネクタとして,
を使用することをお勧めします。
次の表は、,
を使用してパーティションキーを連結した後の結果を示しています。
CombineDeviceiDSellerIDCardID | OrderNumber | attrs |
'000016,a100,66661' | 200001 | ... |
'000054,a100,6777' | 200003 | ... |
'000054,a1001,6777' | 200004 | ... |
'000167,a101,283408' | 200002 | ... |
パーティションキーを連結して生成された上記のテーブルでは、レコードの順序は元のテーブルの順序と一致しています。
まとめ
単一のパーティションキー値のすべての行の合計データサイズが10 GBを超える場合は、複数のプライマリキー列を連結してパーティションキーを形成し、個々のパーティションキー値のデータサイズを最小限に抑えることができます。パーティションキーを連結する際は、以下の点に注意してください。
連結するプライマリキー列を選択する際は、同じパーティションキー値の元の行が、連結後に異なるパーティションキー値を持つようにしてください。
INTEGERプライマリキー列を連結する場合は、数字の前にゼロを追加して同じ順序を維持できます。
コネクタを選択する際は、新しいパーティションキーの辞書順への影響を考慮してください。理想的な方法は、他のすべての使用可能な文字よりもASCIIコードが小さいコネクタを選択することです。
パーティションキーにハッシュプレフィックスを追加する
例
OrderNumberをテーブルのパーティションキーとして使用しないことをお勧めします。OrderNumberは順番に増加するため、購入レコードは常に最新のOrderNumber範囲に書き込まれます。その結果、以前のOrderNumber範囲には書き込み圧力がかかりません。これにより、アクセス圧力のアンバランスが生じ、予約済みの読み取り/書き込みスループットが非効率的に使用されます。順番に増加するキー値をパーティションキーとして使用する必要がある場合は、ハッシュプレフィックスをパーティションキーに連結します。このようにして、OrderNumberはテーブル全体にランダムに分散され、アクセス圧力のバランスがより適切になります。
次の表は、OrderNumberをパーティションキーとして使用する購入レコードを示しています。
OrderNumber | DeviceID | SellerID | CardID | attrs |
200001 | 16 | 'a100' | 66661 | ... |
200002 | 167 | 'a101' | 283408 | ... |
200003 | 54 | 'a100' | 6777 | ... |
200004 | 54 | 'a1001' | 6777 | ... |
200005 | 66 | 'b304' | 178994 | ... |
例として、OrderNumberの場合、md5アルゴリズムを使用してプレフィックスを計算し(他のハッシュアルゴリズムも許可されています)、それを連結してHashOrderNumberを作成できます。md5アルゴリズムによって計算されたハッシュ文字列は長すぎる可能性があるため、最初の数桁だけを取得して、順次OrderNumberのレコードをランダムに分散させることができます。この例では、最初の4桁を使用して次のテーブルを作成します。
HashOrderNumber | DeviceID | SellerID | CardID | attrs |
'2e38200004' | 54 | 'a1001' | 6777 | ... |
'a5a9200003' | 54 | 'a100' | 6777 | ... |
'c335200005' | 66 | 'b304' | 178994 | ... |
'db6e200002 | 167 | 'a101' | 283408 | ... |
'ddba200001' | 16 | 'a100' | 66661 | ... |
後で購入レコードにアクセスする場合は、同じアルゴリズムを使用してOrderNumberのハッシュプレフィックスを計算し、購入レコードに対応するHashOrderNumberを取得します。パーティションキーにハッシュプレフィックスを追加することの1つの欠点は、元々連続していたレコードが分散されることです。その結果、GetRange操作を使用して論理的に連続したレコードの範囲を取得することはできません。
データを並列で書き込む
Tablestoreテーブルが複数のパーティションに分割されると、これらのパーティションは複数のTablestoreサーバーに分散されます。データのバッチがプライマリキーによって順序付けられてTablestoreにアップロードされ、データが同じ順序で書き込まれる場合、特定のパーティションに書き込み圧力が集中する可能性があります。このパーティションは高圧になる可能性がありますが、他のパーティションはアイドル状態のままです。この操作では、予約済みの読み取り/書き込みスループットが完全に活用されず、データのインポート速度に影響を与える可能性があります。
この問題を解決するには、次のいずれかの方法を使用してデータのインポート速度を上げます。
元のデータの順序を崩してデータをインポートします。書き込まれたデータが各パーティションに均等に分散されていることを確認してください。
複数のワーカースレッドを使用してデータを並列でインポートします。大きなデータセットを複数の小さなセットに分割します。次に、ワーカースレッドはランダムに小さなセットを選択してインポートします。
コールドデータとホットデータを区別する
データは多くの場合時間依存です。前のセクションで説明した学生のトランザクションレコードを例として使用します。アプリケーションは最新のレコードを頻繁にクエリし、最新のレコードに基づいて統計を処理およびコンパイルするため、一部の購入レコードはアクセス確率が高くなる可能性があります。ただし、以前の購入レコードは引き続きストレージスペースを占有し、コールドになります。
テーブルに大量のコールドデータが含まれている場合、予約済みの読み取り/書き込みスループットは非効率的に使用され、パーティション全体でアクセス圧力のアンバランスが生じます。たとえば、卒業した学生のカードは購入レコードを生成しなくなります。カードIDが申請日に基づいて増加し、パーティションキーとして使用される場合、CardIDが卒業生に属するレコードにはアクセス圧力はかかりませんが、予約済みの読み取り/書き込みスループットが無駄になります。
時間依存データを効果的に管理するには、異なるテーブルを使用してコールドデータとホットデータを分離し、各テーブルに異なる予約済みの読み取り/書き込みスループットを設定します。たとえば、購入レコードは月ごとに異なるテーブルに分割できます。毎月新しいテーブルが作成されます。新しい購入レコードは常に当月のテーブルに書き込まれ、クエリ操作も実行されます。アクセスニーズを満たすために、当月の最新の購入レコードを含むテーブルに高い予約済みの読み取り/書き込みスループットを設定できます。新しいデータがほとんどまたはまったく書き込まれないが、クエリが引き続き実行される過去数か月の以前のテーブルには、低い予約済みの書き込みスループットと高い予約済みの読み取りスループットを設定できます。1年以上などのメンテナンス期間を超えたテーブルには、低い予約済みの読み取り/書き込みスループットを設定できます。これらのテーブルは、OSSアーカイブにエクスポートして復元するか、削除できます。