スキーマ設計のベスト プラクティス

このページでは、Bigtable のスキーマ設計について説明します。このページを読む前に、Bigtable の概要を理解する必要があります。このページで取り上げるトピックは、次のとおりです。

一般的なコンセプト

Bigtable スキーマの設計は、リレーショナル データベースのスキーマ設計とは異なります。Bigtable のスキーマは、スキーマ定義オブジェクトやファイルではなく、アプリケーション ロジックによって定義されます。テーブルの作成時または更新時にテーブルに列ファミリーを追加できますが、列と行キーのパターンはテーブルに書き込むデータによって定義されます。

Bigtable において、スキーマとは、次のテーブルの構成要素の構造を含むテーブルの設計図またはモデルです。

  • 行キー
  • 列ファミリー(ガベージ コレクション ポリシーを含む)

Bigtable では、スキーマの設計は基本的にテーブルに送信するクエリまたは読み取りリクエストによって決まります。Bigtable データを読み取る最も速い方法は行範囲の読み取りであるため、このページの推奨事項では、行範囲の読み取りを最適化するための情報を提供しています。ほとんどの場合、これは行キーの接頭辞に基づいてクエリを送信することを意味します。

もう一つの考慮事項はホットスポットの回避です。ホットスポットを回避するには、書き込みパターンを検討し、短時間で小さいキースペースが頻繁にアクセスされないようにする方法を考慮する必要があります。

Bigtable のスキーマ設計には、次の一般的なコンセプトが適用されます。

  • Bigtable はリレーショナル ストアではなく Key-Value のストアです。結合はサポートされず、トランザクションは 1 つの行内でのみサポートされます。
  • 各テーブルのインデックス(行キー)は 1 つのみです。セカンダリ インデックスはありません。各行キーは一意である必要があります。
  • 行は、行キーの(最小バイト文字列から最大バイト文字列まで)辞書順に並べ替えられます。行キーは、ビッグ エンディアン順(ネットワーク バイト順とも呼ばれ、バイナリのアルファベト順に相当する)に並べ替えられます。
  • 列ファミリーは特定の順序では保存されません。
  • 列は、列ファミリー別にグループ化され、列ファミリー内で辞書順に並べ替えられます。たとえば、列修飾子に ProcessNameUser%CPUIDMemoryDiskReadPriority を持つ SysMonitor という名前の列ファミリーでは、Bigtable は次の順序で列を格納します。
SysMonitor
%CPU DiskRead ID メモリ 優先度 ProcessName User
  • 行と列の交差には、タイムスタンプ付きのセルを複数含めることができます。各セルには、その行と列のデータに付けられた、タイムスタンプ付きの一意のバージョンが格納されます。
  • 集計列ファミリーには集計セルが含まれます。 集計セルのみを含む列ファミリーを作成できます。集計を使用すると、新しいデータをセル内の既存のデータと結合できます。
  • すべてのオペレーションは行レベルでアトミックに実行されます。オペレーションは、行全体に影響を与えるか、どの行にも影響を与えないかのどちらかです。
  • 読み取りと書き込みは(テーブルの行スペース全体に)均等に分散されるのが理想的です。
  • Bigtable のテーブルはスパースです。列を使用していない行では、列による空間の消費はありません。

ベスト プラクティス

スキーマが適切であれば、高いパフォーマンスとスケーラビリティを得られますが、スキーマが不適切であればパフォーマンスが悪いシステムになることがあります。ユースケースはそれぞれ異なり、独自の設計が必要ですが、ほとんどのユースケースでは以下のおすすめの方法が利用されます。例外事項も示します。

テーブルレベルから始め、行キーレベルまで、以降のセクションではスキーマ設計のベスト プラクティスについて説明します。

すべてのテーブル要素、特に行キーは、想定される読み取りリクエストを考慮して設計する必要があります。全テーブル要素の上限の推奨値とサイズのハードリミットについては、割り当てと上限をご覧ください。

1 つのインスタンス内のすべてのテーブルは同じタブレットに保存されます。このため、1 つのテーブル内にホットスポットが生じるスキーマ設計では、同じインスタンス内の他のテーブルのレイテンシに影響する可能性があります。ホットスポットは、短時間でテーブルの一部が頻繁にアクセスされることで発生します。

テーブル

似たスキーマを持つデータセットは、同じテーブルに保存します。別のテーブルではありません。

他のデータベース システムでは、件名と列数に基づいて複数のテーブルにデータを格納する場合があります。しかし、Bigtable ではすべてのデータを 1 つのテーブルに格納することをおすすめします。データセットごとに、使用する一意の行キー接頭辞を割り当てることで、Bigtable が関連するデータを連続した行範囲に保存し、それを行キー接頭辞でクエリできるようになります。

Bigtable にはインスタンスあたり 1,000 個のテーブルという上限がありますが、通常のテーブル数ははるかに少ない数になります。多数のテーブルを作成しないでください。理由は以下のとおりです。

  • リクエストを多数のテーブルに送信すると、バックエンド接続のオーバーヘッドが増加して、テール レイテンシが増加します。
  • サイズが異なる複数のテーブルを使用すると、Bigtable のパフォーマンスを高めるためのバックグラウンドのロード バランシングが中断される可能性があります。

別のスキーマを必要とする異なるユースケース用に別のテーブルを使用することもできますが、類似したデータには別々のテーブルを使用しないでください。たとえば、新しい年または新しいお客様用に新しいテーブルを作成しないでください。

列ファミリー

関連する列は、同じ列ファミリーに配置します。互いに関連する複数の値が行に含まれている場合は、そうした値を含む列を同じ列ファミリーにグループ化することをおすすめします。データをできる限り密にグループ化して複雑なフィルタの設計を避け、最もよく発生する読み取りリクエストでは、必要な情報のみを取得するようにしてそれ以上は取得しないようにします。

列ファミリーの作成は、1 つのテーブルに約 100 個までとします。 100 個を超える列ファミリーを作成すると、パフォーマンスが低下する場合があります。

列ファミリーの名前は、短いものにします。名前は、各リクエストで転送されるデータに含まれます。

データ保持のニーズが異なる列は、別の列ファミリーに配置します。ストレージ費用を抑えたい場合、これは重要なポイントになります。ガベージ コレクション ポリシーは、列レベルではなく列ファミリー レベルで設定されます。たとえば、あるデータの最新バージョンのみを保持する必要がある場合、別の情報を 1,000 バージョン分保存するように設定されている列ファミリーに保存しないでください。そうしなければ、不要な 999 セル分の保存データに対する料金が発生します。

(省略可)列修飾子をデータとして扱います。すべての列に対して列修飾子を保存する必要があるため、値を使用して列に名前を付けることによってスペースを節約できます。例として、友人関係に関するデータが Friends 列ファミリーに格納されているテーブルについて考えてみましょう。各行は、人物とそのすべての友人関係を表します。各列修飾子に友人の ID を指定できます。その行の各列の値は、友人が属している社会的集団になります。この場合、各行は次のように表示されることが考えられます。

行キー 列修飾子: 値 列修飾子: 値 列修飾子: 値
ホセ Fred:読書クラブ Gabriel:仕事 Hiroshi:テニス
Sofia Hiroshi:仕事 Seo Yoon:学校 Jakob:チェスクラブ

このスキーマとは対照的に、同じデータで、列修飾子をデータとして扱わず、すべての行に同じ列を持つスキーマを使用します。

行キー 列修飾子: 値 列修飾子: 値
Jose#1 Friend:Fred Circle:読書クラブ
Jose#2 Friend:Gabriel Circle:仕事
Jose#3 Friend:Hiroshi Circle:テニス
Sofia#1 Friend:Hiroshi Circle:仕事
Sofia#2 Friend:Seo Yoon Circle:学校
Sofia#3 Friend:Jakob Circle:チェスクラブ

2 つ目のスキーマ設計では、テーブルがはるかに早く肥大化します。

列修飾子を使用してデータを保存する場合は、列修飾子に短くて意味のある名前を付けます。この方法により、各リクエストで転送されるデータの量を削減できます。最大サイズは 16 KB です。

テーブルには、必要な数だけ列を作成します。Bigtable テーブルはスパースであり、行内の空列によって領域が消費されることはありません。256 MB の行あたりの上限を超える行が存在しない限り、テーブル内に数百万個の列を作成できます。

1 行に列は多すぎないようにします。1 つのテーブルに非常に多くの列がある場合でも、1 の中に多くの列を作るべきではありません。このベスト プラクティスには、次のことが関係しています。

  • Bigtable が行内の各セルを処理するため時間がかかります。
  • テーブルに格納されネットワークへ送信されるデータ量に対して、セルのそれぞれが、いくらかのオーバーヘッドを追加します。たとえば、1 KB(1,024 バイト)のデータを格納する場合、1 バイトずつ 1,024 個のセルに分散させるのではなく、データを 1 つのセルに格納したほうがスペース効率が良くなります。

Bigtable が効率的に処理できる行より多くの行を、データセットが論理的に必要とする場合は、データを protobuf で 1 列に保存することを検討してください。

1 行のすべての値のサイズを 100 MB 未満にします。1 行のデータが 256 MB を超えないようにしてください。この上限を超える行があると、読み取りパフォーマンスが低下する可能性があります。

エンティティのすべての情報を 1 行に保存します。ほとんどのユースケースでは、不一致を避けるために、アトミックに読み取る必要があるデータの保存や、複数の行へ一度にデータを保存することは避けてください。たとえば、テーブルの 2 つの行を更新する場合、一方の行の更新に成功し、他方の行の更新に失敗するという可能性があります。関連データの精度を確保するために、スキーマが同時に複数行の更新を必要としないようにしてください。これにより、書き込みリクエストの一部が失敗するか、再送する必要がある場合に、データの一部が一時的に不完全な状態になることを避けられます。

例外: あるエンティティを 1 行に保存すると行が数百 MB になる場合は、複数の行にデータを分割する必要があります。

関連するエンティティは隣接する行に保存して、読み取り効率を高めてください。

セル

1 つのセルには 10 MB を超えるデータを保存しないでください。セルとは、特定の行および列に一意のタイムスタンプによって保存されたデータであり、該当する行と列が交差する箇所に複数のセルを保存できる点に再度留意してください。列で保持されるセル数は、その列を含む列ファミリーに設定したガベージ コレクション ポリシーによって管理されます。

集計セルを使用して集計データを保存、更新する小売店での従業員 1 人あたりの月次売上合計など、エンティティのイベントの集計値のみが必要な場合は、集計を使用できます。詳細については、書き込み時の集計値プレビュー)をご覧ください。

行キー

行キーは、データの取得に使用するクエリに基づいて設計します。適切に設計された行キーは、Bigtable のパフォーマンスを最大限に高めます。Bigtable のクエリでは、次のいずれかを使用してデータを取得するのが最も効率的です。

  • 行キー
  • 行キーの接頭辞
  • 開始行キーと終了行キーで定義された行の範囲

その他のクエリでは全テーブル スキャンが行われるため、効率が大幅に低下します。設計の段階で正しい行キーを選択すれば、後で面倒なデータ移行処理を行わずに済みます。

行キーは短いものにします。1 つの行キーは 4 KB 以下にする必要があります。長い行キーを使用すると、メモリとストレージが余分に消費されるだけでなく、Bigtable サーバーからのレスポンス返却に要する時間も長くなります。

複数の区切られた値は、各行キーに保存します。Cloud Bigtable を効率的にクエリする最も良い方法は、行キーを指定することです。多くの場合、行キーに複数の識別子を含めると便利です。行キーに複数の値が含まれている場合は、データの使い方を明確に理解しておくことがとりわけ重要となります。

通常、行キーセグメントは、コロン、スラッシュ、ハッシュ記号などの区切り文字で区切られます。最初のセグメントや一連の連続するセグメントは行キー接頭辞で、最後のセグメントや一連の連続するセグメントは行キー接尾辞です。

行キーのサンプル

うまく設計された行キー接頭辞を使用すると、Bigtable 組み込みの並び順を利用して関連データを連続した行に格納できます。関連データを連続した行に格納すると、行の範囲として関連データにアクセスでき、効率の悪いテーブル スキャンを行わなくて済みます。

データに数値を格納または並べ替える整数が含まれている場合は、整数を先頭のゼロでパディングします。Bigtable は、データを辞書順に格納します。たとえば、辞書順に 3 > 20、20 > 03 となります。3 の前に 0 をパディングすることで、数字が数として並べ替えられます。範囲ベースのクエリを使用するタイムスタンプには、このテクニックが重要です。

適切に定義された行の範囲を取得できる行キーを作成することが重要です。作成しない場合は、クエリにテーブル スキャンが必要になります。これは、特定の行を取得するよりもはるかに時間がかかります。

たとえば、モバイル デバイスのデータをアプリが追跡する場合、デバイスタイプ、デバイス ID、データが記録された日付で構成される行キーを持つことができます。このデータの行キーは次のようになります。

        phone#4c410523#20200501
        phone#4c410523#20200502
        tablet#a0b81f74#20200501
        tablet#a0b81f74#20200502

この行キーの設計により、1 つのリクエストに対して次のデータを取得できます。

  • デバイスタイプ
  • デバイスタイプとデバイス ID の組み合わせ

特定の日のすべてのデータを取得する場合は、この行キーの設計は最適ではありません。日付は 3 番目のセグメント(行キー接尾辞)に保存されるため、単に行キーの接尾辞や中間セグメントに基づいて行の範囲をリクエストすることはできません。代わりに、テーブル全体をスキャンして日付の値を探すフィルタを付けて、読み取りリクエストを送信する必要があります。

可能な限り行キーには、人が読める形式の文字列を使用します。この方法により、Key Visualizer ツールを使用した、Bigtable に関する問題のトラブルシューティングが容易になります。

多くの場合、先頭が共通の値であり、末尾が詳細な値である行キーを設計する必要があります。たとえば、行キーに大陸、国、都市が含まれている場合は、次のような行キーを作成して、カーディナリティの低い値を使用して、最初に自動的に並べ替えを行うことができます。

        asia#india#bangalore
        asia#india#mumbai
        asia#japan#osaka
        asia#japan#sapporo
        southamerica#bolivia#cochabamba
        southamerica#bolivia#lapaz
        southamerica#chile#santiago
        southamerica#chile#temuco

避けたい行キー

行キーのタイプによっては、データのクエリが難しくなるものや、パフォーマンスが低下するものがあります。このセクションでは、Bigtable で使用すべきではない行キーのタイプについて説明します。

先頭がタイムスタンプである行キー。このパターンでは、1 つのノードに連続した書き込みが push され、ホットスポットが発生します。行キーにタイムスタンプを付与する場合は、ホットスポットの発生を回避するため、ユーザー ID などの高いカーディナリティ値を先頭に設定します。

関連データをグループ化できない行キー。関連するデータを連続しない行に格納する行キーは避けます。行範囲が連続していないと、まとめて読み取ることが非効率になります。

シーケンシャル数値 ID。お使いのシステムで、アプリケーションの各ユーザーに数値 ID が割り当てられているとします。このような場合には、テーブルの行キーとしてユーザーの数値 ID を使いたくなるかもしれません。しかし、新規のユーザーのほうがアクティブなユーザーになる可能性が高いため、このような方法では、大半のトラフィックがごく少数のノードに集中してしまいます。

より安全な方法として、ユーザーの数値 ID のリバース版を使用する方法があります。この方法なら、Bigtable テーブルのすべてのノードに均等にトラフィックが分散されます。

頻繁に更新される識別子。頻繁に更新する必要のある値を識別するために単一の行キーを使用しないでください。たとえば、複数のデバイスのメモリ使用状況に関するデータを毎秒保存する場合は、デバイス ID と保存される指標で構成されるデバイスごとに 1 つの行キー(4c410523#memusage など)を使用して、行を繰り返し更新することは回避してください。このような操作を実行すると、使用頻度の高い行が格納されているテーブルが過負荷状態になります。また、ガベージ コレクションの際にセルが削除されるまで列の以前の値が容量を消費するため、行のサイズが上限を超えてしまう可能性もあります。

代わりに、新しく読み取るごとに新しい行に保存します。メモリ使用量の例を使用すると、各行キーには、デバイス ID、指標の種類、タイムスタンプを含めることができるため、行キーは 4c410523#memusage#1423523569918 のようになります。この方法は効率的です。なぜなら、Bigtable では、新しい行を作成するのに要する時間は、新しいセルを作成するのに要する時間と大差ないからです。また、この方法を使用すると、適切な開始キーと終了キーを計算することで、特定の日付範囲から迅速にデータを読み取ることができます。

毎分数百回更新されるカウンタなど、頻繁に変化する値の場合は、データをアプリケーション層でメモリ内に保持し、新しい行を定期的に Bigtable に書き込むのが最善の方法です。

ハッシュ値。行キーをハッシュすると、Bigtable の通常の並べ替え順を利用できなくなり、クエリに最適な方法で行を格納できなくなります。同じ理由で、値をハッシュすると、Bigtable に関する問題を Key Visualizer ツールを使用してトラブルシューティングすることが困難になります。ハッシュ値ではなく、人が読める値を使用してください。

人が読める文字列ではなく、RAW バイトとして表現される値。RAW バイトは列の値には適していますが、読みやすさとトラブルシューティングを重視する場合は、行キー内の文字列値を使用してください。

特殊なユースケース

他にないデータセットでは、Bigtable に格納するスキーマを設計する際に、特別な考慮事項が必要になることがあります。このセクションでは、Bigtable データの一部の種類について、それを最善の方法で保存するためのおすすめの方法を説明します。

時間ベースのデータ

記録された時刻に基づいてデータが取得されることが多い場合は、行キーの一部としてタイムスタンプを含めます

たとえば、アプリケーションで、CPU とメモリの使用状況などのパフォーマンス関連のデータを、多くのマシン上で 1 秒ごとに記録する場合があるとします。このようなケースでは、パソコンの識別子とデータのタイムスタンプを連結したもの(例: machine_4223421#1425330757685)をデータの行キーとして使用できます。行キーは辞書順で並べ替えられていることに注意してください。

タイムスタンプを単独で、または行キーの先頭で使用しないでください。そうした場合、1 つのノードに push される連続した書き込みの原因となり、それによってホットスポットが発生します。この場合は、書き込みパターンと読み取りパターンを検討する必要があります。

最新のレコードを最初に検索することが多い場合は、行キーにリバース タイムスタンプを使用します。リバース タイムスタンプは、お使いのプログラミング言語の長整数の最大値(Java の場合は java.lang.Long.MAX_VALUE など)からタイムスタンプの値を引くことによって、反転させることができます。リバース タイムスタンプを使用すると、新しいレコードが最初にくるようにレコードが並べ替えられます。

時系列データに特化した取り扱いについては、時系列データのスキーマ設計をご覧ください。

マルチテナンシー

行キー接頭辞は、「マルチテナンシー」ユースケースに対するスケーラブルなソリューションとなります。マルチテナンシーとは、複数のクライアントのために同じデータモデルを使用して類似したデータを格納するシナリオを指します。すべてのテナントに対して 1 つのテーブルを使用することは、通常、マルチテナント データの保存およびアクセス方法として最も効率的です。

たとえば、多くの会社に代わって購入履歴を保存し、履歴を追跡するとします。会社ごとに一意の ID を行キー接頭辞として使用できます。テナントのすべてのデータは、同じテーブル内の連続した行に格納され、行キー接頭辞を使用してクエリやフィルタリングを行えます。また、ある会社がお客様でなくなり、その会社のために保存していた購入履歴データを削除する場合は、その会社の行キー接頭辞を使用している行範囲を削除すれば済みます。

たとえば、お客様 altostrat と、お客様 examplepetstore の携帯電話データを保存する場合、次のような行キーを作成できます。次に、altostrat がお客様でなくなった場合は、行キー接頭辞 altostrat が付いたすべての行を削除します。

        altostrat#phone#4c410523#20190501
        altostrat#phone#4c410523#20190502
        altostrat#tablet#a0b41f74#20190501
        examplepetstore#phone#4c410523#20190502
        examplepetstore#tablet#a6b81f79#20190501
        examplepetstore#tablet#a0b81f79#20190502

それに対して、会社ごとに別々のテーブルにデータを保存すると、パフォーマンスやスケーラビリティの問題が起こる可能性があります。さらにはインスタンスあたり 1,000 テーブルという Bigtable の上限に達してしまう可能性もあります。インスタンスの上限に達すると、Bigtable では、インスタンス内にさらにテーブルを作成することができなくなります。

プライバシー

ユースケースで必要がない限り、行キーまたは列ファミリー ID で個人情報(PII)やユーザーデータを使用しないでください。行キーと列ファミリーの値は顧客データとサービスデータの両方であり、暗号化やロギングなど、それらを使用するアプリケーションによって、プライベートデータにアクセスすべきでないユーザーにそれらの値が誤って公開される可能性があります。

サービスデータの処理方法の詳細については、Google Cloud のプライバシーに関するお知らせをご覧ください。

ドメイン名

広範なドメイン名

ドメイン名として表現可能なエンティティに関するデータを格納する場合は、行キーとしてリバース ドメイン名(com.company.productなど)を使用することを検討してください。リバース ドメイン名は、各行のデータが隣接する行と重なる傾向がある場合は特に有効です。そのような場合、Bigtable によるデータの圧縮効率が向上します。

一方、逆順でない標準のドメイン名では、関連するデータが 1 か所にまとめられない形で行が並べ替えられることがあります。これにより、圧縮効率が悪くなり、読み取り効率が悪くなる可能性があります。

このアプローチは、データが多数の異なるリバース ドメイン名間に分散している場合に、最も効果的です。

その点を説明するため、ここでは、次のドメイン名について考察します。Bigtable により辞書順に自動的に並べ替えられています。

      drive.google.com
      en.wikipedia.org
      maps.google.com

これは、google.com のすべての行に対するクエリを行うユースケースにとっては好ましくありません。対照的に、ドメイン名が逆になっている同じ行について考えます。

      com.google.drive
      com.google.maps
      org.wikipedia.en

2 番目の例では、関連する行が自動的に行の範囲として簡単に取得できるように並べ替えられています。

ほとんどないドメイン名

1 つしかないか、少数のドメイン名に対してのみ、大量のデータを格納する場合は、行キーに他の値を検討してください。そうしないと、クラスタ内の単一のノードに書き込みが push され、ホットスポットの発生を招く場合や、行が大きくなりすぎる可能性があります。

クエリの変更や不確実なクエリ

データに対して毎回同じクエリを実行するとは限らない場合、またはクエリが不明な場合、選択肢としては、行のすべてのデータを複数列ではなく 1 列に保存する方法があります。この方法では、プロトコル バッファのバイナリ形式や JSON ファイルなど、後で個々の値を抽出するのが難しくならない形式を使用します。

この場合も行キーは、必要なデータを確実に取得できるように慎重に設計されていますが、通常、各行には、その行のすべてのデータを 1 つの protobuf に含む列が 1 つだけ含まれています。

データを複数列に分散するのではなく、protobuf メッセージとしてデータを 1 列に保存することには、メリットとデメリットがあります。これには、次のようなメリットがあります。

  • 使用するデータ量が少ないため、データの保存にかかる費用が削減される。
  • 列ファミリーと列修飾子を確約しないことによって、一定の柔軟性を維持できる。
  • 読み取りアプリケーションで、テーブル スキーマを認識する必要がない。

デメリットには、次のようなものがあります。

  • protobuf メッセージは、Bigtable から読み取った後に、シリアル化解除する必要がある。
  • フィルタを使用して protobuf メッセージ内のデータを照会する選択肢がなくなる。
  • protobuf メッセージを Bigtable から読み取った後、BigQuery を使用して、そのフィールドに対する連携クエリを実行できない。

次のステップ