スキーマの概要

このページでは、スキーマについて説明し、インターリーブされたテーブルを紹介します。これにより、親子関係にあるテーブルをクエリする際のクエリ パフォーマンスが向上します。

Spanner データベースには、1 つ以上のテーブルが含まれます。テーブルは行と列として構成されます。1 つ以上の列がテーブルの主キーとして定義され、各行が一意に識別されます。主キーは常にインデックスにより迅速な行を検索できます。また、1 つ以上の列にセカンダリ インデックスを定義できます。テーブルの既存の行を更新または削除する場合は、テーブルに主キーが必要です。主キー列のないテーブルに構成できる行は 1 つのみです。主キーのないテーブルを持つことができるのは、GoogleSQL 言語データベースのみです。

Spanner のデータは、厳格に型指定されます。データベースごとにスキーマを定義する必要があり、そのスキーマでは各テーブルの各列のデータ型を指定する必要があります。データ型にはスカラー型と複雑な型があります。詳しくは、GoogleSQL のデータ型PostgreSQL のデータ型をご覧ください。

親子テーブル関係

Spanner で親子関係を定義するには、テーブル インターリーブ外部キーの 2 つの方法があります。

Spanner のテーブル インターリーブは、多くの親子関係に適しています。インターリーブでは、Spanner の子行はストレージの親行と物理的に同じ場所に配置されます。コロケーションを行うと、パフォーマンスが大幅に向上します。たとえば、Customers テーブルと Invoices テーブルがあり、アプリケーションがお客様のすべての請求書を頻繁に取得する場合は、InvoicesCustomers の子テーブルとして定義できます。これは、論理的に独立した 2 つのテーブルの間にデータの局所性の関係を宣言することになります。 Spanner に Invoices の 1 つ以上の行を 1 つの Customers 行とともに格納するよう指示しています。

子テーブルを親テーブルに関連付けるには、DDL を使用して、子テーブルを親にインターリーブすることを宣言し、子テーブルの複合プライマリの最初の部分に親テーブルの主キーを追加します。インターリーブの詳細については、インターリーブされたテーブルを作成するをご覧ください。

外部キーはより一般的な親子ソリューションであり、その他のユースケースに対応します。主キー列に制限されることなく、テーブルに複数の外部キー関係(ある関係では親として、その他の関係では子として)を持たせることもできます。ただし、外部キー関係は、ストレージ レイヤ内でのテーブルのコロケーションを示唆するわけではありません。

Google では、インターリーブ テーブルと外部キーの両方ではなく、そのいずれかとして親子関係を表すことを選択することをおすすめします。外部キーとインターリーブ テーブルとの比較の詳細については、外部キーの概要をご覧ください。

主キーの選択

多くの場合、アプリケーションには主キーとして使用するのに適したフィールドがすでに存在します。たとえば、Customers テーブルには、主キーとして動作するアプリケーション提供の CustomerId があります。また、行の挿入時に、主キーを生成しなければならない場合もあります。これは通常、ビジネス上意味のない一意の整数値になります( サロゲート主キー )。

どのような場合でも、ホットスポットができないように慎重に主キーを選択する必要があります。たとえば、単調に増加する整数をキーとしてレコードを挿入すると、常にキースペースの最後に挿入されます。Spanner はキーの範囲でサーバー間にデータを分割するので、このような方法では挿入はすべて単一のサーバーに送られ、ホットスポットが作成されるため、好ましくありません。複数のサーバーに負荷を分散させ、ホットスポットを回避できる方法があります。

主キーに基づくセカンダリ インデックスの追加

状況によっては、データベースを使用する際に、主キーに基づくセカンダリ インデックスの追加が役立つ場合があります。これは特に、テーブルの主キーの逆順スキャンを必要とするクエリを頻繁に実行する場合に当てはまります。

インターリーブされたテーブルの主キー

インターリーブするには、すべてのテーブルに主キーが必要です。あるテーブルを別のテーブルのインターリーブされた子として宣言する場合は、テーブルに含まれる複合主キーに、親の主キーのすべてのコンポーネントを同じ順序で通常は1 つ以上の追加の子テーブル列を含む複合主キーが必要です。

Spanner は、主キーの値の順序で行を格納し、子の行を親の行の間に挿入します。インターリーブされたテーブルを作成するのインターリーブされた行の図をご覧ください。

まとめると、Spanner を使用すると、関連するテーブルの行を物理的に同じ場所に配置できます。この物理的なレイアウトについては、下記のスキーマの例をご覧ください。

データベースのスプリット

インターリーブされた親子関係の階層を、最大 7 層まで定義できます。つまり、7 個の独立したテーブルの行を同じ場所に配置できます。テーブルのデータのサイズが小さい場合には、1 つの Spanner サーバーでデータベースを処理できるでしょう。しかし、関連するテーブルが拡大され、個々のサーバーのリソース上限に近づくとどうなるでしょうか。Spanner は分散型データベースで、データベースのサイズが大きくなると、データはスプリットというまとまりに分割されます。個々のスプリットはそれぞれ独立した存在で、別のサーバーに割り当てられます。また、物理的に別の場所に配置される場合があります。スプリットでは連続した行の範囲を保持されます。この範囲の開始キーと終了キーは「スプリットの境界」と呼ばれます。Spanner によって、サイズや負荷に基づいてスプリットの境界を自動的に追加、削除され、それによってデータベース内のスプリット数が変えられます。

負荷に基づいた分割

読み取りホットスポットを軽減する目的で、負荷に基づいた分割を Spanner で実行する例を示します。データベースに含まれるあるテーブルの 10 行が、テーブル内の他の行よりも頻繁に読み取られるとします。Spanner は、これら 10 行のそれぞれの間にスプリット境界を追加できます。これにより、すべての行が 1 台のサーバーで読み取られてリソースを消費する代わりに、これらの各行が異なるサーバーで処理されます。

一般的に、スキーマ設計のベスト プラクティスに従う場合、Spanner は、次のようなホットスポットを軽減できます。それは、読み取りスループットが数分ごとに向上し、インスタンス内のリソースが飽和状態になる時点まで、あるいは(1 つのスプリットには単一行とそのインターリーブされた子のみが含まれるため)新しいスプリット境界を追加できないという状況になるまで向上し続けるようなホットスポットです。

スキーマの例

次のセクションのスキーマの例では、インターリーブの有無にかかわらず親テーブルと子テーブルを作成する方法と、対応するデータの物理レイアウトを示します。

親テーブルを作成する

音楽アプリケーションを作成していて、歌手データの行を格納するテーブルが必要だとします。

5 行 4 列の Singers テーブル SingerID は最初の列です。

テーブルには 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 テーブルの行を示しています。ここで、かっこ内の数字は主キーの値です。

優先するキー順序で格納されているテーブルの行の例。キー 3 とキー 4 のスプリット境界を示す点線があります。

先出の図では、Singers(3)Singers(4) によってキー付けされた行間でのスプリット境界の例も示されており、結果のスプリットのデータは異なるサーバーに割り当てられます。このテーブルが拡大すると、Singers データの行は異なる場所に格納される可能性があります。

親テーブルと子テーブルを作成する

各歌手のアルバムについての基本データを音楽アプリケーションに追加するとします。

5 行 3 列の Albums テーブル主キー列は左側にあります。

Albums の主キーは 2 つの列 SingerIdAlbumId で構成されており、各アルバムをその歌手と関連付けていることに注意してください。次のスキーマ例では、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)
);

SingersAlbums の行の物理レイアウトは図のようになります。最初に、Albums テーブルの行が連続主キーで格納され、次に Singers の行が連続主キーで格納されます。

行の物理レイアウト。主キーは左端の列に表示されます。たとえば、Albums(2,1)、Albums(2,2) などです。

上のスキーマに関する重要な注意点の 1 つは、Singers テーブルと Albums テーブルは最上位テーブルであるため、これらのテーブルの間にデータ局所性関係がSpannerに想定されないことです。データベースが拡大されると、Spanner は任意の行の間にスプリット境界を追加する可能性があります。つまり、Albums テーブルの行は Singers テーブルの行と異なるスプリットで終了する可能性があり、2 つのスプリットは互いに独立して移動することがあります。

アプリケーションのニーズによっては、Albums のデータを Singers のデータとは異なるスプリットに配置できるようにすることが適している場合があります。ただし、異なるリソース間で読み取りと更新を調整する必要があるため、パフォーマンスが低下する可能性があります。特定の歌手のすべてのアルバムに関する情報を頻繁に取得する必要がある場合は、AlbumsSingers のインターリーブされた子テーブルとして作成し、行を同じ場所に配置する必要があります。これは、主キーのディメンションに沿って 2 つのテーブルから抽出されます。次の例ではこれについてさらに詳しく説明します。

インターリーブされたテーブルの作成

インターリーブされたテーブルとは、子テーブルの行をそれに関連する親の行と一緒に物理的に格納する目的で、別のテーブルのインターリーブされた子として宣言されるテーブルです。前に説明したように、親テーブルの主キーは、子テーブルの複合主キーの最初の部分である必要があります。

音楽アプリケーションを設計しているときに、アプリが Singers 行にアクセスする際に、Albums テーブルの行に頻繁にアクセスする必要があるとします。たとえば、行 Singers(1) にアクセスする際は、行 Albums(1, 1)Albums(1, 2) にもアクセスする必要があります。この場合、SingersAlbums は強いデータ局所性関係を持つ必要があります。このようなデータ局所性関係は、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 行にホットスポットがない限り維持されます。
  • 子の行を挿入する前に、親の行が存在する必要があります。親の行は、データベースにすでに存在する場合と、同じトランザクションで子の行を挿入する前に挿入される場合があります。

行の物理レイアウト: Albums の行は Singers の行の間にインターリーブされています

インターリーブされたテーブルの階層を作成する

SingersAlbums の間の親子関係は、さらに下位のテーブルまで拡張できます。たとえば、Albums の子として Songs という名前のインターリーブされたテーブルを作成し、各アルバムのトラックリストを格納できます。

6 行 4 列の Songs テーブル左端の 3 つの列が主キーを構成します。

Songs には、階層で上位にあるテーブル(つまり、SingerIdAlbumId)のすべての主キーを含む主キーが必要です。

-- 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;

次の図は、インターリーブされた行の物理ビューを表しています。

行の物理ビュー: Songs は Albums にインターリーブされ、それらは Singers の間にインターリーブされます

この例では、歌手の数の増加に従って、Spanner は歌手の間に分割境界を追加し、歌手とそのアルバムおよび歌のデータとの間で、データの局所性を維持します。ただし、歌手行とその子行のサイズがスプリット サイズの上限を超えた場合、または子行でホットスポットが検出された場合、Spanner はスプリット境界を追加してそのホットスポットの行とその下のすべての子行を分離しようとします。

まとめると、親テーブルとそれより下位にあるすべてのテーブルにより、スキーマでのテーブルの階層が形成されます。階層内の各テーブルは論理的には独立していますが、このように物理的にインターリーブすることで、テーブルが効率的に事前結合され、関連する行にまとめてアクセスできて、ディスク アクセスが最小限になるため、パフォーマンスが向上します。

インターリーブされたテーブルと結合する

可能な限り、インターリーブされたテーブルのデータを主キーによって結合する。通常、インターリーブされた各行は、その親行と同じスプリットに物理的に保存されるため、Spanner はローカルで主キーによる結合を行い、ディスク アクセスとネットワーク トラフィックを最小限に抑えることができます。次の例では、SingersAlbums が主キー 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 はサポートされていません)。

次に示すのは、主キー列 SingerIdNOT 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 テーブルがあります。

データベース 1: Ackworth Records
SingerId FirstName LastName
1MarcRichards
2CatalinaSmith
データベース 2: Cama Records
SingerId FirstName LastName
1ユーザー BTrentor
2GabrielWright
データベース 3: Eagan Records
SingerId FirstName LastName
1BenjaminMartinez
2HannahHarris

スキーマ管理のマルチテナント

Spanner でマルチテナントを設計するもう 1 つの方法は、すべての顧客を 1 つのデータベース内の 1 つのテーブルに配置し、お客様ごとに異なる主キーの値を使用することです。たとえば、テーブルに CustomerId キー列を含めることができます。CustomerId を最初のキー列にすると、各お客様のデータに良好な局所性がもたらされます。その後、Spanner は、データベース分割を効果的に使用して、データサイズと読み込みパターンに基づいてパフォーマンスを最大化します。次の例では、すべてのお客様を対象とした単一の Singers テーブルがあります。

Spanner マルチテナント データベース
CustomerId SingerId FirstName LastName
11MarcRichards
12CatalinaSmith
21ユーザー BTrentor
22GabrielWright
31BenjaminMartinez
32HannahHarris

テナントごとに個別のデータベースを用意する必要がある場合は、以下の制約に注意してください。

  • インスタンスあたりのデータベース数とデータベースあたりのテーブルとインデックスの数には制限があります。顧客の数によっては、個別のデータベースやテーブルを持つことができない場合があります。
  • 新しいテーブルとインターリーブされていないインデックスの追加によって、長い時間がかかる場合があります。スキーマ設計が新しいテーブルとインデックスの追加に依存する場合、必要なパフォーマンスを得ることができない場合があります。

個別のデータベースを作成する場合は、各データベースの 1 週間あたりのスキーマ変更数が少なくなるようにデータベース間でテーブルを分散すると、成功することが多くなります。

アプリケーションのお客様ごとに個別のテーブルとインデックスを作成する場合は、同じデータベースにすべてのテーブルとインデックスを入れるのは避けてください。それらを多数のデータベースで分割して、多数のインデックスを作成する際のパフォーマンスの問題を緩和してください。

マルチテナンシーの他のデータ マネジメント パターンとアプリケーション設計の詳細については、Spanner でのマルチテナンシーの実装をご覧ください。