スキーマの概要

このページでは、Spanner スキーマの要件、スキーマを使用して階層関係を作成する方法、スキーマ機能について説明します。また、インターリーブされたテーブルも導入され、親子関係のテーブルに対してクエリを実行する際のクエリ パフォーマンスが向上します。

スキーマは、テーブル、ビュー、インデックス、関数などのデータベース オブジェクトを含む名前空間です。スキーマを使用すると、オブジェクトを整理し、きめ細かいアクセス制御権限を適用し、名前の競合を回避できます。Spanner では、データベースごとにスキーマを定義する必要があります。

データベース テーブル内の行をさらにセグメント化し、異なる地理的リージョンに保存することもできます。詳細については、地域別パーティショニングの概要をご覧ください。

厳密に型指定されたデータ

Spanner のデータは厳密に型指定されます。データ型には、スカラー型と複合型があります。これらの詳細については、GoogleSQL のデータ型PostgreSQL のデータ型をご覧ください。

主キーを選択

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

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

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

親子テーブル関係

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

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

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

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

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

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

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

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

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

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

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

負荷に基づいた分割

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

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

名前付きスキーマ

名前付きスキーマを使用すると、類似するデータをまとめて整理できます。これにより、Google Cloud コンソールでオブジェクトをすばやく見つけ、権限を適用し、名前の競合を回避できます。

名前付きスキーマは、他のデータベース オブジェクトと同様に DDL を使用して管理されます。

Spanner の名前付きスキーマでは、完全修飾名(FQN)を使用してデータをクエリできます。FQN を使用すると、スキーマ名とオブジェクト名を組み合わせてデータベース オブジェクトを識別できます。たとえば、倉庫ビジネス ユニットに warehouse というスキーマを作成できます。このスキーマを使用するテーブルには、productordercustomer information などがあります。または、納品ビジネス ユニットに fulfillment というスキーマを作成することもできます。このスキーマには、productordercustomer information というテーブルも存在できます。最初の例では FQN は warehouse.product で、2 番目の例では FQN は fulfillment.product です。これにより、複数のオブジェクトが同じ名前を共有している場合に混乱を防ぐことができます。

CREATE SCHEMA DDL では、テーブル オブジェクトに FQN(sales.customers など)と短い名前(sales など)の両方が指定されます。

次のデータベース オブジェクトは、名前付きスキーマをサポートしています。

  • TABLE
    • CREATE
    • INTERLEAVE IN [PARENT]
    • FOREIGN KEY
    • SYNONYM
  • VIEW
  • INDEX
  • FOREIGN KEY
  • SEQUENCE

名前付きスキーマの使用の詳細については、名前付きスキーマを管理するをご覧ください。

名前付きスキーマで詳細なアクセス制御を使用する

名前付きスキーマを使用すると、スキーマ内の各オブジェクトにスキーマレベルのアクセス権を付与できます。これは、アクセス権を付与した時点で存在するスキーマ オブジェクトに適用されます。後で追加されるオブジェクトへのアクセス権を付与する必要があります。

詳細なアクセス制御では、テーブル、列、テーブル内の行など、データベース オブジェクトのグループ全体へのアクセスを制限します。

詳細については、名前付きスキーマにきめ細かいアクセス制御権限を付与するをご覧ください。

スキーマの例

このセクションのスキーマの例では、インターリーブありとインターリーブなしで親テーブルと子テーブルを作成する方法と、それに対応するデータの物理レイアウトを示します。

親テーブルを作成する

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

5 行 4 列の Singers テーブル

テーブルには 1 つの主キー列 SingerId があり(太い線の左側)、テーブルは行と列で編成されています。

このテーブルは、次の DDL で定義できます。

GoogleSQL

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 データの行は異なる場所に格納される可能性があります。

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

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

5 行 3 列の Albums テーブル

Albums の主キーは 2 つの列 SingerIdAlbumId で構成されており、各アルバムをその歌手と関連付けていることに注意してください。次のスキーマ例では、Albums テーブルと Singers テーブルをどちらもデータベース階層のルートに定義しているので、これらのテーブルは兄弟テーブルになります。

-- Schema hierarchy:
-- + Singers (sibling table of Albums)
-- + Albums (sibling table of Singers)

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

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 の行が連続主キーで格納されます。

行の物理レイアウト

上のスキーマに関する重要な注意点の 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 を作成する方法を示しています。

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;

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 テーブル

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 で結合されます。

GoogleSQL

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 が省略するため、列 Albums.SingerIdNOT NULL は使用できません。

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
1AliceTrentor
2GabrielWright
データベース 3: Eagan Records
SingerId FirstName LastName
1BenjaminMartinez
2HannahHarris

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

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

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

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

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

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

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

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