このページでは、Spanner スキーマの要件、スキーマを使用して階層関係を作成する方法、スキーマ機能について説明します。また、インターリーブされたテーブルも導入され、親子関係のテーブルに対してクエリを実行する際のクエリ パフォーマンスが向上します。
スキーマは、テーブル、ビュー、インデックス、関数などのデータベース オブジェクトを含む名前空間です。スキーマを使用すると、オブジェクトを整理し、きめ細かいアクセス制御権限を適用し、名前の競合を回避できます。Spanner では、データベースごとにスキーマを定義する必要があります。
また、データベース テーブル内の行を、異なる地理的リージョン間でさらにセグメント化して保存することもできます。詳細については、地域別パーティション分割の概要をご覧ください。
厳密に型指定されたデータ
Spanner のデータは厳密に型指定されます。データ型には、スカラー型と複合型があります。これらの詳細については、GoogleSQL のデータ型と PostgreSQL のデータ型をご覧ください。
主キーを選択
Spanner データベースには、1 つ以上のテーブルを含めることができます。テーブルは行と列で構成されています。テーブル スキーマでは、各行を一意に識別するテーブルの主キーとして 1 つ以上のテーブル列を定義します。主キーは常にインデックスに登録され、行の迅速な検索が可能です。テーブルの既存の行を更新または削除する場合は、テーブルに主キーが必要です。主キー列のないテーブルに構成できる行は 1 つのみです。主キーのないテーブルを構成できるのは、GoogleSQL 言語データベースのみです。
多くの場合、アプリケーションには主キーとして使用するのに適したフィールドがすでに存在します。たとえば、Customers
テーブルについては、主キーとして適切に機能しアプリケーションが指定する CustomerId
が存在する場合があります。また、行を挿入するときに主キーを生成することが必要な場合があります。これは通常、ビジネス上意味のない一意の整数値になります(サロゲート主キー)。
どのような場合でも、ホットスポットができないように慎重に主キーを選択する必要があります。たとえば、単調に増加する整数をキーとしてレコードを挿入すると、常にキースペースの最後に挿入されます。Spanner はキーの範囲でサーバー間にデータを分割するので、このような方法では挿入はすべて単一のサーバーに送られ、ホットスポットが作成されるため、好ましくありません。複数のサーバーに負荷を分散させ、ホットスポットを回避できる方法があります。
- キーをハッシュして列に格納する。主キーとしてハッシュ列を使用します(またはハッシュ列と一意のキー列を一緒に使用します)。
- 主キーの列の順序を入れ替える。
- Universally Unique Identifier(UUID)を使用する。上位ビットのランダム値が使用されるため、バージョン 4 の UUID をおすすめします。上位ビットにタイムスタンプを格納する UUID アルゴリズム(バージョン 1 UUID など)は使用しないでください。
- 連続した値をビット反転する。
親子テーブル関係
Spanner で親子関係を定義するには、テーブル インターリーブと外部キーの 2 つの方法があります。
親子関係が多い場合は、Spanner のテーブル インターリーブが適しています。インターリーブを使用すると、Spanner はストレージ内の子行と親行を物理的に配置します。コロケーションを行うと、パフォーマンスが大幅に向上します。たとえば、Customers
テーブルと Invoices
テーブルがあり、アプリケーションがお客様のすべての請求書を頻繁に取得する場合は、Invoices
を Customers
のインターリーブされた子テーブルとして定義できます。これにより、独立した 2 つのテーブル間にデータの局所性の関係を宣言することになります。Spanner に、Invoices
の 1 つ以上の行を 1 つの Customers
行と格納するよう指示しています。
子テーブルを親テーブルに関連付けるには、DDL を使用して、子テーブルを親にインターリーブすることを宣言し、子テーブルの複合主キーの最初の部分に親テーブルの主キーを追加します。インターリーブの詳細については、このページの後半のインターリーブされたテーブルを作成するをご覧ください。
外部キーはより一般的な親子ソリューションであり、その他のユースケースに対応します。主キー列に制限されることなく、テーブルに複数の外部キー関係(ある関係では親として、その他の関係では子として)を持たせることもできます。ただし、外部キー関係は、ストレージ レイヤ内でのテーブルのコロケーションを示唆するわけではありません。
Google では、インターリーブ テーブルと外部キーの両方ではなく、そのいずれかとして親子関係を表すことを選択することをおすすめします。外部キーとインターリーブ テーブルとの比較の詳細については、外部キーの概要をご覧ください。
インターリーブされたテーブルの主キー
インターリーブするには、すべてのテーブルに主キーが必要です。あるテーブルを別のテーブルのインターリーブされた子として宣言する場合、テーブルには親の主キーのすべてのコンポーネントが同じ順序で含まれる複合主キーと、通常は 1 つ以上の追加の子テーブル列が必要です。
Spanner は、主キーの値の順序で行を格納し、子の行を親の行の間に挿入します。インターリーブされた行の図については、このページの後半にあるインターリーブされたテーブルを作成するをご覧ください。
まとめると、Spanner を使用すると、関連するテーブルの行を物理的に同じ場所に配置できます。この物理的なレイアウトについては、スキーマの例をご覧ください。
データベースのスプリット
インターリーブされた親子関係の階層を最大 7 層まで定義できます。つまり、7 つの独立したテーブルの行を同じ場所に配置できます。テーブルのデータのサイズが小さい場合には、単一の Spanner サーバーでデータベースを処理できるでしょう。しかし、関連するテーブルが拡大され、個々のサーバーのリソース上限に近づくとどうなるでしょうか。Spanner は分散型データベースで、データベースのサイズが大きくなると、データはスプリットというまとまりに分割されます。個々のスプリットはそれぞれ独立した存在で、別のサーバーに割り当てられます。また、物理的に別の場所に配置される場合があります。スプリットでは連続した行の範囲が保持されます。この範囲の開始キーと終了キーは「スプリットの境界」と呼ばれます。Spanner によって、サイズや負荷に基づいてスプリットの境界を自動的に追加、削除され、それによってデータベース内のスプリット数が変えられます。
負荷に基づいた分割
読み取りホットスポットを軽減する目的で、負荷に基づいた分割を Spanner で実行する例を示します。データベースに含まれるあるテーブルの 10 行が、テーブル内の他の行よりも頻繁に読み取られるとします。Spanner は、これら 10 行のそれぞれの間にスプリット境界を追加できます。これにより、すべての行が 1 台のサーバーで読み取られてリソースを消費する代わりに、これらの各行が異なるサーバーで処理されます。
一般的に、スキーマ設計のベスト プラクティスに従う場合、Spanner は、次のようなホットスポットを軽減できます。それは、読み取りスループットが数分ごとに向上し、インスタンス内のリソースが飽和状態になる時点まで、あるいは(1 つのスプリットには単一行とそのインターリーブされた子のみが含まれるため)新しいスプリット境界を追加できないという状況になるまで向上し続けるようなホットスポットです。
名前付きスキーマ
名前付きスキーマを使用すると、類似するデータをまとめて整理できます。これにより、Google Cloud コンソールでオブジェクトをすばやく見つけ、権限を適用し、名前の競合を回避できます。
名前付きスキーマは、他のデータベース オブジェクトと同様に DDL を使用して管理されます。
Spanner の名前付きスキーマでは、完全修飾名(FQN)を使用してデータをクエリできます。FQN を使用すると、スキーマ名とオブジェクト名を組み合わせてデータベース オブジェクトを識別できます。たとえば、倉庫ビジネス ユニットに warehouse
というスキーマを作成できます。このスキーマを使用するテーブルには、product
、order
、customer information
などがあります。または、フルフィルメント ビジネス ユニットに fulfillment
というスキーマを作成することもできます。このスキーマには、product
、order
、customer
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
名前付きスキーマの使用の詳細については、名前付きスキーマを管理するをご覧ください。
名前付きスキーマできめ細かいアクセス制御を使用する
名前付きスキーマを使用すると、スキーマ内の各オブジェクトにスキーマレベルのアクセス権を付与できます。これは、アクセス権を付与した時点で存在するスキーマ オブジェクトに適用されます。後で追加されるオブジェクトへのアクセス権を付与する必要があります。
きめ細かいアクセス制御では、テーブル、列、テーブル内の行など、データベース オブジェクトのグループ全体へのアクセスを制限します。
詳細については、名前付きスキーマにきめ細かいアクセス制御権限を付与するをご覧ください。
スキーマの例
このセクションのスキーマの例では、インターリーブありとインターリーブなしで親テーブルと子テーブルを作成する方法と、それに対応するデータの物理レイアウトを示します。
親テーブルを作成する
音楽アプリケーションを作成していて、歌手データの行を格納するテーブルが必要だとします。
テーブルには 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
データの行は異なる場所に格納される可能性があります。
親テーブルと子テーブルを作成する
各歌手のアルバムについての基本データを音楽アプリケーションに追加するとします。
Albums
の主キーは 2 つの列 SingerId
と AlbumId
で構成されており、各アルバムをその歌手と関連付けていることに注意してください。次のスキーマ例では、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) );
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
を作成する方法を示しています。
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
行にホットスポットがない限り維持されます。 - 子の行を挿入する前に、親の行が存在する必要があります。親の行は、データベースにすでに存在する場合と、同じトランザクションで子の行を挿入する前に挿入される場合があります。
インターリーブされたテーブルの階層を作成する
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
で結合されます。
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 をサポートしていません。)
次に示すのは、主キー列 SingerId
で NOT NULL
句を省略した例です。SingerId
は主キーであるため、その列に NULL
を格納する行は 1 つだけです。
CREATE TABLE Singers ( SingerId INT64, FirstName STRING(1024), LastName STRING(1024), ) PRIMARY KEY (SingerId);
主キー列の NULL 値可能プロパティは、親テーブルの宣言と子テーブルの宣言の間で一致している必要があります。この例では、Singers.SingerId
が省略するため、列 Albums.SingerId
の NOT 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
テーブルがあります。
SingerId | FirstName | LastName |
---|---|---|
1 | Marc | Richards |
2 | Catalina | Smith |
SingerId | FirstName | LastName |
---|---|---|
1 | Alice | 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 | Alice | Trentor |
2 | 2 | Gabriel | Wright |
3 | 1 | Benjamin | Martinez |
3 | 2 | Hannah | Harris |
テナントごとに個別のデータベースを用意する必要がある場合は、以下の制約に注意してください。
- インスタンスあたりのデータベース数とデータベースあたりのテーブルとインデックスの数には制限があります。顧客の数によっては、個別のデータベースやテーブルを持つことができない場合があります。
- 新しいテーブルとインターリーブされていないインデックスの追加によって、長い時間がかかる場合があります。スキーマ設計が新しいテーブルとインデックスの追加に依存する場合、必要なパフォーマンスを得ることができない場合があります。
個別のデータベースを作成する場合は、各データベースの 1 週間あたりのスキーマ変更数が少なくなるようにデータベース間でテーブルを分散すると、成功することが多くなります。
アプリケーションのお客様ごとに個別のテーブルとインデックスを作成する場合は、同じデータベースにすべてのテーブルとインデックスを入れるのは避けてください。それらを多数のデータベースで分割して、多数のインデックスを作成する際のパフォーマンスの問題を緩和してください。
マルチテナンシーの他のデータ マネジメント パターンとアプリケーション設計の詳細については、Spanner でのマルチテナンシーの実装をご覧ください。