このページでは、スキーマについて説明し、インターリーブされたテーブルを紹介します。これにより、親子関係にあるテーブルをクエリする際のクエリ パフォーマンスが向上します。
Spanner データベースには、1 つ以上のテーブルが含まれます。テーブルは行と列として構成されます。1 つ以上の列がテーブルの主キーとして定義され、各行が一意に識別されます。主キーは常にインデックスにより迅速な行を検索できます。また、1 つ以上の列にセカンダリ インデックスを定義できます。テーブルの既存の行を更新または削除する場合は、テーブルに主キーが必要です。主キー列のないテーブルに構成できる行は 1 つのみです。主キーのないテーブルを持つことができるのは、GoogleSQL 言語データベースのみです。
Spanner のデータは、厳格に型指定されます。データベースごとにスキーマを定義する必要があり、そのスキーマでは各テーブルの各列のデータ型を指定する必要があります。データ型にはスカラー型と複雑な型があります。詳しくは、GoogleSQL のデータ型と PostgreSQL のデータ型をご覧ください。
親子テーブル関係
Spanner で親子関係を定義するには、テーブル インターリーブと外部キーの 2 つの方法があります。
Spanner のテーブル インターリーブは、多くの親子関係に適しています。インターリーブでは、Spanner の子行はストレージの親行と物理的に同じ場所に配置されます。コロケーションを行うと、パフォーマンスが大幅に向上します。たとえば、Customers
テーブルと Invoices
テーブルがあり、アプリケーションがお客様のすべての請求書を頻繁に取得する場合は、Invoices
を Customers
の子テーブルとして定義できます。これは、論理的に独立した 2 つのテーブルの間にデータの局所性の関係を宣言することになります。
Spanner に Invoices
の 1 つ以上の行を 1 つの Customers
行とともに格納するよう指示しています。
子テーブルを親テーブルに関連付けるには、DDL を使用して、子テーブルを親にインターリーブすることを宣言し、子テーブルの複合プライマリの最初の部分に親テーブルの主キーを追加します。インターリーブの詳細については、インターリーブされたテーブルを作成するをご覧ください。
外部キーはより一般的な親子ソリューションであり、その他のユースケースに対応します。主キー列に制限されることなく、テーブルに複数の外部キー関係(ある関係では親として、その他の関係では子として)を持たせることもできます。ただし、外部キー関係は、ストレージ レイヤ内でのテーブルのコロケーションを示唆するわけではありません。
Google では、インターリーブ テーブルと外部キーの両方ではなく、そのいずれかとして親子関係を表すことを選択することをおすすめします。外部キーとインターリーブ テーブルとの比較の詳細については、外部キーの概要をご覧ください。
主キーの選択
多くの場合、アプリケーションには主キーとして使用するのに適したフィールドがすでに存在します。たとえば、Customers
テーブルには、主キーとして動作するアプリケーション提供の CustomerId
があります。また、行の挿入時に、主キーを生成しなければならない場合もあります。これは通常、ビジネス上意味のない一意の整数値になります( サロゲート主キー )。
どのような場合でも、ホットスポットができないように慎重に主キーを選択する必要があります。たとえば、単調に増加する整数をキーとしてレコードを挿入すると、常にキースペースの最後に挿入されます。Spanner はキーの範囲でサーバー間にデータを分割するので、このような方法では挿入はすべて単一のサーバーに送られ、ホットスポットが作成されるため、好ましくありません。複数のサーバーに負荷を分散させ、ホットスポットを回避できる方法があります。
- キーをハッシュして列に格納する。主キーとしてハッシュ列を使用します(またはハッシュ列と一意のキー列を一緒に使用します)。
- 主キーの列の順序を入れ替える。
- Universally Unique Identifier(UUID)を使用する。上位ビットのランダム値が使用されるため、バージョン 4 の UUID をおすすめします。上位ビットにタイムスタンプを格納する UUID アルゴリズム(バージョン 1 UUID など)は使用しないでください。
- 連続した値をビット反転する。
主キーに基づくセカンダリ インデックスの追加
状況によっては、データベースを使用する際に、主キーに基づくセカンダリ インデックスの追加が役立つ場合があります。これは特に、テーブルの主キーの逆順スキャンを必要とするクエリを頻繁に実行する場合に当てはまります。
インターリーブされたテーブルの主キー
インターリーブするには、すべてのテーブルに主キーが必要です。あるテーブルを別のテーブルのインターリーブされた子として宣言する場合は、テーブルに含まれる複合主キーに、親の主キーのすべてのコンポーネントを同じ順序で通常は1 つ以上の追加の子テーブル列を含む複合主キーが必要です。
Spanner は、主キーの値の順序で行を格納し、子の行を親の行の間に挿入します。インターリーブされたテーブルを作成するのインターリーブされた行の図をご覧ください。
まとめると、Spanner を使用すると、関連するテーブルの行を物理的に同じ場所に配置できます。この物理的なレイアウトについては、下記のスキーマの例をご覧ください。
データベースのスプリット
インターリーブされた親子関係の階層を、最大 7 層まで定義できます。つまり、7 個の独立したテーブルの行を同じ場所に配置できます。テーブルのデータのサイズが小さい場合には、1 つの Spanner サーバーでデータベースを処理できるでしょう。しかし、関連するテーブルが拡大され、個々のサーバーのリソース上限に近づくとどうなるでしょうか。Spanner は分散型データベースで、データベースのサイズが大きくなると、データはスプリットというまとまりに分割されます。個々のスプリットはそれぞれ独立した存在で、別のサーバーに割り当てられます。また、物理的に別の場所に配置される場合があります。スプリットでは連続した行の範囲を保持されます。この範囲の開始キーと終了キーは「スプリットの境界」と呼ばれます。Spanner によって、サイズや負荷に基づいてスプリットの境界を自動的に追加、削除され、それによってデータベース内のスプリット数が変えられます。
負荷に基づいた分割
読み取りホットスポットを軽減する目的で、負荷に基づいた分割を Spanner で実行する例を示します。データベースに含まれるあるテーブルの 10 行が、テーブル内の他の行よりも頻繁に読み取られるとします。Spanner は、これら 10 行のそれぞれの間にスプリット境界を追加できます。これにより、すべての行が 1 台のサーバーで読み取られてリソースを消費する代わりに、これらの各行が異なるサーバーで処理されます。
一般的に、スキーマ設計のベスト プラクティスに従う場合、Spanner は、次のようなホットスポットを軽減できます。それは、読み取りスループットが数分ごとに向上し、インスタンス内のリソースが飽和状態になる時点まで、あるいは(1 つのスプリットには単一行とそのインターリーブされた子のみが含まれるため)新しいスプリット境界を追加できないという状況になるまで向上し続けるようなホットスポットです。
スキーマの例
次のセクションのスキーマの例では、インターリーブの有無にかかわらず親テーブルと子テーブルを作成する方法と、対応するデータの物理レイアウトを示します。
親テーブルを作成する
音楽アプリケーションを作成していて、歌手データの行を格納するテーブルが必要だとします。
テーブルには 1 つの主キー列 SingerId
があり(太い線の左側)、テーブルは行、列、値で編成されています。
このテーブルは、Spanner のスキーマで次のように定義できます。
Google SQL
CREATE TABLE Singers ( SingerId INT64 NOT NULL, FirstName STRING(1024), LastName STRING(1024), SingerInfo BYTES(MAX), ) PRIMARY KEY (SingerId);
PostgreSQL
CREATE TABLE singers ( singer_id BIGINT PRIMARY KEY, first_name VARCHAR(1024), last_name VARCHAR(1024), singer_info BYTEA );
このスキーマの例については、次の点に注意してください。
Singers
はデータベース階層のルートにあるテーブルです(別のテーブルの子として定義されていないため)。- GoogleSQL 言語データベースの場合、通常、主キー列には
NOT NULL
というアノテーションを指定します(ただし、キー列でNULL
値を許容する場合は、このアノテーションを省略できます。詳細については、キー列をご覧ください。 - 主キーに含まれない列は非キー列と呼ばれ、必要に応じて
NOT NULL
アノテーションを指定できます。 - GoogleSQL で
STRING
型またはBYTES
型を使用する列では、長さを定義する必要があります。これは、フィールドに格納できる Unicode 文字の最大数を表しますPostgreSQL のvarchar
型とcharacter varying
型では、長さの指定は省略可能です。詳細については、GoogleSQL 言語データベースのスカラーデータ型と PostgreSQL 言語データベースの PostgreSQL データ型をご覧ください。
Singers
テーブルの行の物理レイアウトはどのようになるでしょう。 次の図は、主キー("Singers(1)"、"Singers(2)" など)によって保存された Singers
テーブルの行を示しています。ここで、かっこ内の数字は主キーの値です。
先出の図では、Singers(3)
と Singers(4)
によってキー付けされた行間でのスプリット境界の例も示されており、結果のスプリットのデータは異なるサーバーに割り当てられます。このテーブルが拡大すると、Singers
データの行は異なる場所に格納される可能性があります。
親テーブルと子テーブルを作成する
各歌手のアルバムについての基本データを音楽アプリケーションに追加するとします。
Albums
の主キーは 2 つの列 SingerId
と AlbumId
で構成されており、各アルバムをその歌手と関連付けていることに注意してください。次のスキーマ例では、Albums
テーブルと Singers
テーブルをどちらもデータベース階層のルートに定義しているので、これらのテーブルは兄弟テーブルになります。
-- Schema hierarchy: -- + Singers (sibling table of Albums) -- + Albums (sibling table of Singers)
Google SQL
CREATE TABLE Singers ( SingerId INT64 NOT NULL, FirstName STRING(1024), LastName STRING(1024), SingerInfo BYTES(MAX), ) PRIMARY KEY (SingerId); CREATE TABLE Albums ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, AlbumTitle STRING(MAX), ) PRIMARY KEY (SingerId, AlbumId);
PostgreSQL
CREATE TABLE singers ( singer_id BIGINT PRIMARY KEY, first_name VARCHAR(1024), last_name VARCHAR(1024), singer_info BYTEA ); CREATE TABLE albums ( singer_id BIGINT, album_id BIGINT, album_title VARCHAR, PRIMARY KEY (singer_id, album_id) );
Singers
と Albums
の行の物理レイアウトは図のようになります。最初に、Albums
テーブルの行が連続主キーで格納され、次に Singers
の行が連続主キーで格納されます。
上のスキーマに関する重要な注意点の 1 つは、Singers
テーブルと Albums
テーブルは最上位テーブルであるため、これらのテーブルの間にデータ局所性関係がSpannerに想定されないことです。データベースが拡大されると、Spanner は任意の行の間にスプリット境界を追加する可能性があります。つまり、Albums
テーブルの行は Singers
テーブルの行と異なるスプリットで終了する可能性があり、2 つのスプリットは互いに独立して移動することがあります。
アプリケーションのニーズによっては、Albums
のデータを Singers
のデータとは異なるスプリットに配置できるようにすることが適している場合があります。ただし、異なるリソース間で読み取りと更新を調整する必要があるため、パフォーマンスが低下する可能性があります。特定の歌手のすべてのアルバムに関する情報を頻繁に取得する必要がある場合は、Albums
を Singers
のインターリーブされた子テーブルとして作成し、行を同じ場所に配置する必要があります。これは、主キーのディメンションに沿って 2 つのテーブルから抽出されます。次の例ではこれについてさらに詳しく説明します。
インターリーブされたテーブルの作成
インターリーブされたテーブルとは、子テーブルの行をそれに関連する親の行と一緒に物理的に格納する目的で、別のテーブルのインターリーブされた子として宣言されるテーブルです。前に説明したように、親テーブルの主キーは、子テーブルの複合主キーの最初の部分である必要があります。
音楽アプリケーションを設計しているときに、アプリが Singers
行にアクセスする際に、Albums
テーブルの行に頻繁にアクセスする必要があるとします。たとえば、行 Singers(1)
にアクセスする際は、行 Albums(1, 1)
と Albums(1, 2)
にもアクセスする必要があります。この場合、Singers
と Albums
は強いデータ局所性関係を持つ必要があります。このようなデータ局所性関係は、Singers
のインターリーブされた子テーブルとして Albums
を作成することにより宣言できます。
-- Schema hierarchy: -- + Singers -- + Albums (interleaved table, child table of Singers)
次のスキーマの太字の行は、Singers
のインターリーブされたテーブルとして Albums
を作成する方法を示します。
Google SQL
CREATE TABLE Singers ( SingerId INT64 NOT NULL, FirstName STRING(1024), LastName STRING(1024), SingerInfo BYTES(MAX), ) PRIMARY KEY (SingerId); CREATE TABLE Albums ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, AlbumTitle STRING(MAX), ) PRIMARY KEY (SingerId, AlbumId), INTERLEAVE IN PARENT Singers ON DELETE CASCADE;
PostgreSQL
CREATE TABLE singers ( singer_id BIGINT PRIMARY KEY, first_name VARCHAR(1024), last_name VARCHAR(1024), singer_info BYTEA ); CREATE TABLE albums ( singer_id BIGINT, album_id BIGINT, album_title VARCHAR, PRIMARY KEY (singer_id, album_id) ) INTERLEAVE IN PARENT singers ON DELETE CASCADE;
このスキーマについては次の点に注意してください。
- 子テーブル
Albums
の主キーの最初の部分であるSingerId
は、親テーブルSingers
の主キーでもあります。 ON DELETE CASCADE
アノテーションは、親テーブルの行が削除された場合、その子行も自動的に削除されることを示します。子テーブルにこのアノテーションがないか、アノテーションがON DELETE NO ACTION
の場合、親の行を削除するためには先に子の行を削除する必要があります。- インターリーブされた行は、最初に親テーブルの行で並べ替えられた後、親の主キーを共有する子テーブルの連続行によって並べ替えられます。例えば、"Singers(1)"、"Albums(1, 1)"、"Albums(1, 2)" のようになります。
- 各歌手とそのアルバムデータの間のデータ局所性関係は、このデータベースが分割されても、
Singers
行とそのすべてのAlbums
行のサイズが分割サイズ制限を超えず、Albums
行にホットスポットがない限り維持されます。 - 子の行を挿入する前に、親の行が存在する必要があります。親の行は、データベースにすでに存在する場合と、同じトランザクションで子の行を挿入する前に挿入される場合があります。
インターリーブされたテーブルの階層を作成する
Singers
と Albums
の間の親子関係は、さらに下位のテーブルまで拡張できます。たとえば、Albums
の子として Songs
という名前のインターリーブされたテーブルを作成し、各アルバムのトラックリストを格納できます。
Songs
には、階層で上位にあるテーブル(つまり、SingerId
と AlbumId
)のすべての主キーを含む主キーが必要です。
-- Schema hierarchy: -- + Singers -- + Albums (interleaved table, child table of Singers) -- + Songs (interleaved table, child table of Albums)
GoogleSQL
CREATE TABLE Singers ( SingerId INT64 NOT NULL, FirstName STRING(1024), LastName STRING(1024), SingerInfo BYTES(MAX), ) PRIMARY KEY (SingerId); CREATE TABLE Albums ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, AlbumTitle STRING(MAX), ) PRIMARY KEY (SingerId, AlbumId), INTERLEAVE IN PARENT Singers ON DELETE CASCADE; CREATE TABLE Songs ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, TrackId INT64 NOT NULL, SongName STRING(MAX), ) PRIMARY KEY (SingerId, AlbumId, TrackId), INTERLEAVE IN PARENT Albums ON DELETE CASCADE;
PostgreSQL
CREATE TABLE singers ( singer_id BIGINT PRIMARY KEY, first_name VARCHAR(1024), last_name VARCHAR(1024), singer_info BYTEA ); CREATE TABLE albums ( singer_id BIGINT, album_id BIGINT, album_title VARCHAR, PRIMARY KEY (singer_id, album_id) ) INTERLEAVE IN PARENT singers ON DELETE CASCADE; CREATE TABLE songs ( singer_id BIGINT, album_id BIGINT, track_id BIGINT, song_name VARCHAR, PRIMARY KEY (singer_id, album_id, track_id) ) INTERLEAVE IN PARENT albums ON DELETE CASCADE;
次の図は、インターリーブされた行の物理ビューを表しています。
この例では、歌手の数の増加に従って、Spanner は歌手の間に分割境界を追加し、歌手とそのアルバムおよび歌のデータとの間で、データの局所性を維持します。ただし、歌手行とその子行のサイズがスプリット サイズの上限を超えた場合、または子行でホットスポットが検出された場合、Spanner はスプリット境界を追加してそのホットスポットの行とその下のすべての子行を分離しようとします。
まとめると、親テーブルとそれより下位にあるすべてのテーブルにより、スキーマでのテーブルの階層が形成されます。階層内の各テーブルは論理的には独立していますが、このように物理的にインターリーブすることで、テーブルが効率的に事前結合され、関連する行にまとめてアクセスできて、ディスク アクセスが最小限になるため、パフォーマンスが向上します。
インターリーブされたテーブルと結合する
可能な限り、インターリーブされたテーブルのデータを主キーによって結合する。通常、インターリーブされた各行は、その親行と同じスプリットに物理的に保存されるため、Spanner はローカルで主キーによる結合を行い、ディスク アクセスとネットワーク トラフィックを最小限に抑えることができます。次の例では、Singers
と Albums
が主キー SingerId
で結合されます。
Google SQL
SELECT s.FirstName, a.AlbumTitle FROM Singers AS s JOIN Albums AS a ON s.SingerId = a.SingerId;
PostgreSQL
SELECT s.first_name, a.album_title FROM singers AS s JOIN albums AS a ON s.singer_id = a.singer_id;
キー列
このセクションでは、キー列に関する注意事項について説明します。
テーブルキーの変更
テーブルのキーは変更できません。既存のテーブルにキー列を追加したり、既存のテーブルからキー列を削除したりすることはできません。
主キーに NULL を格納する
GoogleSQL では、主キー列に NULL を格納する必要がある場合は、スキーマでその列に NOT NULL
句を指定しないようにします(PostgreSQL 言語データベースでは、主キー列の NULL はサポートされていません)。
次に示すのは、主キー列 SingerId
で NOT NULL
句を省略した例です。SingerId
は主キーであるため、その列に NULL
を格納する行は 1 つだけです。
CREATE TABLE Singers ( SingerId INT64, FirstName STRING(1024), LastName STRING(1024), ) PRIMARY KEY (SingerId);
主キー列の NULL 値可能プロパティは、親テーブルの宣言と子テーブルの宣言の間で一致している必要があります。この例では、Singers.SingerId
では NOT NULL
が省略されているため、列 Albums.SingerId
では許可できません。
CREATE TABLE Singers ( SingerId INT64, FirstName STRING(1024), LastName STRING(1024), ) PRIMARY KEY (SingerId); CREATE TABLE Albums ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, AlbumTitle STRING(MAX), ) PRIMARY KEY (SingerId, AlbumId), INTERLEAVE IN PARENT Singers ON DELETE CASCADE;
許可されない型
次の列の型は ARRAY
にできません。
- テーブルのキー列。
- インデックスのキー列。
マルチテナントのための設計
異なるお客様に属するデータを格納する場合は、マルチテナンシーを提供することをおすすめします。たとえば、音楽サービスでは、個々のレコードラベルのデータを個別に保存する必要がある場合があります。
従来のマルチテナンシー
マルチテナンシーを設計する従来の方法は、お客様ごとに個別のデータベースを作成することです。この例では、各データベースに独自の Singers
テーブルがあります。
SingerId | FirstName | LastName |
---|---|---|
1 | Marc | Richards |
2 | Catalina | Smith |
SingerId | FirstName | LastName |
---|---|---|
1 | ユーザー B | Trentor |
2 | Gabriel | Wright |
SingerId | FirstName | LastName |
---|---|---|
1 | Benjamin | Martinez |
2 | Hannah | Harris |
スキーマ管理のマルチテナント
Spanner でマルチテナントを設計するもう 1 つの方法は、すべての顧客を 1 つのデータベース内の 1 つのテーブルに配置し、お客様ごとに異なる主キーの値を使用することです。たとえば、テーブルに CustomerId
キー列を含めることができます。CustomerId
を最初のキー列にすると、各お客様のデータに良好な局所性がもたらされます。その後、Spanner は、データベース分割を効果的に使用して、データサイズと読み込みパターンに基づいてパフォーマンスを最大化します。次の例では、すべてのお客様を対象とした単一の Singers
テーブルがあります。
CustomerId | SingerId | FirstName | LastName |
---|---|---|---|
1 | 1 | Marc | Richards |
1 | 2 | Catalina | Smith |
2 | 1 | ユーザー B | Trentor |
2 | 2 | Gabriel | Wright |
3 | 1 | Benjamin | Martinez |
3 | 2 | Hannah | Harris |
テナントごとに個別のデータベースを用意する必要がある場合は、以下の制約に注意してください。
- インスタンスあたりのデータベース数とデータベースあたりのテーブルとインデックスの数には制限があります。顧客の数によっては、個別のデータベースやテーブルを持つことができない場合があります。
- 新しいテーブルとインターリーブされていないインデックスの追加によって、長い時間がかかる場合があります。スキーマ設計が新しいテーブルとインデックスの追加に依存する場合、必要なパフォーマンスを得ることができない場合があります。
個別のデータベースを作成する場合は、各データベースの 1 週間あたりのスキーマ変更数が少なくなるようにデータベース間でテーブルを分散すると、成功することが多くなります。
アプリケーションのお客様ごとに個別のテーブルとインデックスを作成する場合は、同じデータベースにすべてのテーブルとインデックスを入れるのは避けてください。それらを多数のデータベースで分割して、多数のインデックスを作成する際のパフォーマンスの問題を緩和してください。
マルチテナンシーの他のデータ マネジメント パターンとアプリケーション設計の詳細については、Spanner でのマルチテナンシーの実装をご覧ください。