スキーマを更新する

Spanner を使用すると、ダウンタイムなしでスキーマの更新を行うことができます。いくつかの方法で既存のデータベースのスキーマを更新できます。

サポートされているスキーマの更新

Spanner は、既存のデータベースに対する次のスキーマ更新をサポートしています。

  • 名前付きスキーマを追加または削除する。
  • 新しいテーブルを作成する。新しいテーブルの列は NOT NULL にできます。
  • テーブルを削除する(そのテーブル内で他のテーブルがインターリーブされておらず、セカンダリ インデックスがない場合)。
  • 外部キーを持つテーブルを作成または削除します。
  • 既存のテーブルに対して外部キーを追加または削除します。
  • 任意のテーブルに非キー列を追加する。新しい非キー列は NOT NULL にできません。
    • 任意のテーブルから非キー列を削除する(セカンダリ インデックス、外部キー、格納されている生成列、またはチェック制約で使用されていない場合)。
  • 非キー列に NOT NULL を追加する(ARRAY 列を除く)。
  • 非キー列から NOT NULL を削除する。
  • STRING 列を BYTES 列に、または BYTES 列を STRING 列に変更する。
  • PROTO 列を BYTES 列に、または BYTES 列を PROTO 列に変更する。
  • PROTO 列の proto メッセージ型を変更する。
  • ENUM 定義に新しい値を追加し、ALTER PROTO BUNDLE を使用して既存の値の名前を変更する。
  • 任意の方法で PROTO BUNDLE に定義されているメッセージを変更する。ただし、これらのメッセージの変更されたフィールドはどのテーブルの鍵としても使用されず、既存のデータは新しい制約を満たします。
  • STRING 型または BYTES 型の長さ制限を増やす、または減らす(MAX への変更を含む)(1 つまたは複数の子テーブルによって継承されている主キー列ではない場合のみ)。
  • ARRAY<STRING> 列、ARRAY<BYTES> 列、または ARRAY<PROTO> 列の長さ制限を最大許容値に増やす、または減らす。
  • 値列と主キー列の commit タイムスタンプを有効または無効にする。
  • セカンダリ インデックスを追加または削除する。
  • 既存のテーブルにチェック制約を追加または削除する。
  • 格納されている生成列を既存のテーブルに追加、または既存のテーブルから削除します。
  • 新しいオプティマイザー統計情報パッケージを作成します。
  • ビューを作成して管理する。
  • シーケンスを作成して管理する。
  • データベース ロールを作成し、権限を付与する。
  • 列のデフォルト値を設定、変更、削除する。
  • データベースのオプションを変更する(default_leader や version_retention_period など)。
  • 変更ストリームを作成して管理する。
  • ML モデルを作成して管理する。

サポートされていないスキーマの更新

Spanner は、既存のデータベースに対する次のスキーマ更新をサポートしていません。

  • テーブルまたはインデックス キーによって参照される ENUM タイプの PROTO フィールドがある場合、プロトコル列挙型から ENUM 値を削除することはできません(ENUM<> 列によって使用される列挙型からの ENUM 値の削除はサポートされています。これらの列が鍵として使用されるタイミングなど)。

スキーマ更新のパフォーマンス

Spanner のスキーマの更新には、ダウンタイムは必要ありません。DDL ステートメントのバッチを Spanner データベースに対して発行した場合、Spanner が更新を長時間実行オペレーションとして適用する間も、中断なくデータベースでの書き込みと読み取りを続けることができます。

DDL ステートメントの実行に要する時間は、更新で既存のデータの検証が必要か、データのバックフィルが必要かによって異なります。たとえば、NOT NULL アノテーションを既存の列に追加する場合、Spanner は列にすべての値を読み取って、列に NULL 値が含まれていないことを確認する必要があります。検証が必要なデータが多い場合、この手順には長い時間がかかる可能性があります。もう 1 つの例は、データベースにインデックスを追加する場合です。Spanner が既存のデータを使用してインデックスをバックフィルします。このプロセスは、インデックスの定義と対応するベーステーブルのサイズに応じて時間がかかることがあります。ただし、テーブルに新しい列を追加する場合は、検証が必要な既存のデータがないため、Spanner はより迅速に更新できます。

まとめると、Spanner による既存のデータの検証が不要なスキーマ更新は、数分で行うことができます。検証が必要なスキーマ更新にかかる時間は、検証が必要な既存のデータの量に応じて長くなりますが、データの検証は本番環境トラフィックよりも低い優先度でバックグラウンドで行われます。データの検証が必要なスキーマ更新の詳細については後述します。

ビュー定義に対して検証されたスキーマ更新

スキーマを更新する場合、Spanner は、更新によって既存のビューの定義に使用されるクエリが無効化されないかどうかを検証します。検証に成功すると、スキーマ更新は成功します。検証が成功しなかった場合、スキーマ更新は失敗します。詳細については、ビューを作成する際のベスト プラクティスをご覧ください。

データの検証が必要なスキーマ更新

スキーマ更新に伴い、既存のデータが新しい制約を満たすことの検証を要求されることがあります。スキーマ更新によってデータの検証が要求されると、Spanner は影響を受けるスキーマ エンティティに対する競合するスキーマ更新を禁止し、バックグラウンドでデータを検証します。検証に成功すると、スキーマ更新は成功します。検証が成功しなかった場合、スキーマ更新は成功しません。検証オペレーションは、長時間実行オペレーションとして実行されます。これらのオペレーションのステータスを確認して、オペレーションが成功したか失敗したかを判断できます。

たとえば、RecordLabel 列挙型と Songwriter プロトコル メッセージを使用して、次の music.proto ファイルを定義したとします。

  enum RecordLabel {
    COOL_MUSIC_INC = 0;
    PACIFIC_ENTERTAINMENT = 1;
    XYZ_RECORDS = 2;
  }

  message Songwriter {
    required string nationality   = 1;
    optional int64  year_of_birth = 2;
  }

スキーマに Songwriters テーブルを追加するには:

GoogleSQL

CREATE PROTO BUNDLE (
  googlesql.example.music.Songwriter,
  googlesql.example.music.RecordLabel,
);

CREATE TABLE Songwriters (
  Id         INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  Nickname   STRING(MAX),
  OpaqueData BYTES(MAX),
  SongWriter googlesql.example.music.Songwriter
) PRIMARY KEY (Id);

CREATE TABLE Albums (
  SongwriterId     INT64 NOT NULL,
  AlbumId          INT64 NOT NULL,
  AlbumTitle       STRING(MAX),
  Label            INT32
) PRIMARY KEY (SongwriterId, AlbumId);

次のようなスキーマ更新は許可されますが、検証が必要であり、既存データの量によっては完了するのに時間がかかる場合があります。

  • NOT NULL アノテーションを非キー列に追加する。例:

    ALTER TABLE Songwriters ALTER COLUMN Nickname STRING(MAX) NOT NULL;
    
  • 列の長さを短くする。例:

    ALTER TABLE Songwriters ALTER COLUMN FirstName STRING(10);
    
  • BYTES を STRING に変更する。例:

    ALTER TABLE Songwriters ALTER COLUMN OpaqueData STRING(MAX);
    
  • INT64/INT32 を ENUM に変更する。例:

    ALTER TABLE Albums ALTER COLUMN Label googlesql.example.music.RecordLabel;
    
  • RecordLabel 列挙型の定義から既存の値を削除する。

  • 既存の TIMESTAMP 列で commit タイムスタンプを有効にする。例:

    ALTER TABLE Albums ALTER COLUMN LastUpdateTime SET OPTIONS (allow_commit_timestamp = true);
    
  • 既存のテーブルへのチェック制約の追加。

  • 格納されている生成列を既存のテーブルに追加する。

  • 外部キーで新しいテーブルを作成する。

  • 既存のテーブルへの外部キーの追加

基になっているデータが新しい制約を満たさない場合、これらのスキーマ更新は失敗します。たとえば、ALTER TABLE Songwriters ALTER COLUMN Nickname STRING(MAX) NOT NULL ステートメントは、Nickname 列のいずれかの値が NULL の場合、既存のデータが新しい定義の NOT NULL 制約を満たさないため、失敗します。

データの検証には、数分で済むこともあれば、何時間もかかる場合もあります。データ検証に要する時間は以下のことに依存します。

  • データセットのサイズ
  • インスタンスのコンピューティング容量
  • インスタンスの負荷

スキーマ更新によっては、スキーマ更新が完了する前にデータベースに対するリクエストの動作が変わる場合があります。たとえば、NOT NULL を列に追加する場合、Spanner はほとんど瞬時に、列に NULL を使用する新しいリクエストの書き込みの拒否を開始します。新しいスキーマ更新のデータ検証が最終的に失敗する場合、古いスキーマでは受け付けられていた場合でも、書き込みがブロックされる期間が発生する可能性があります。

projects.instances.databases.operations.cancel メソッドまたは gcloud spanner operations を使用すると、実行時間の長いデータ検証オペレーションをキャンセルできます。

バッチ内のステートメントの実行順序

Google Cloud CLI、REST API、RPC API を使用する場合は、1 つ以上の CREATE、ALTER、DROP ステートメントのバッチを発行できます。

Spanner では、同じバッチのステートメントを順番に適用し、最初のエラーで停止します。あるステートメントの適用でエラーが発生した場合、そのステートメントはロールバックされます。バッチ内の以前に適用されたステートメントの結果はロールバックされません。この順序どおりのステートメントの適用は、インデックス バックフィルなど、それぞれに時間がかかる可能性のある特定のステートメントを並行して実行する場合は、それらのステートメントを個別のバッチで送信する必要があることを意味します。

Spanner によってさまざまなバッチのステートメントが組み合わされて並べ替えられることがあるため、データベースに適用される 1 つのアトミック変更の中に、複数のバッチのステートメントが混在する可能性があります。それぞれのアトミック変更内で、さまざまなバッチのステートメントが任意の順序で出現します。たとえば、あるバッチのステートメントに ALTER TABLE MyTable ALTER COLUMN MyColumn STRING(50) が含まれ、別のバッチのステートメントには ALTER TABLE MyTable ALTER COLUMN MyColumn STRING(20) が含まれている場合、Spanner によって列がこのいずれかの状態になりますが、どちらの状態になるかは指定できません。

スキーマの更新中に作成されたスキーマのバージョン

Spanner ではスキーマのバージョニングが使用されるため、大規模なデータベースへのスキーマの更新中にダウンタイムが発生しません。Spanner では、スキーマの更新処理中の読み取りをサポートするために、古いスキーマ バージョンを維持します。その後、Spanner によって、スキーマの更新を処理するために、1 つ以上の新しいバージョンのスキーマが作成されます。各バージョンには、1 回のアトミック変更でのステートメントのコレクションの結果が含まれます。

スキーマのバージョンは、必ずしも DDL ステートメントのバッチまたは個々の DDL ステートメントとで、1 対 1 で対応しているとは限りません。既存のベーステーブルのインデックス作成やデータ検証が必要な文など、一部の個々の DDL 文では、複数のスキーマ バージョンが発生します。また、いくつかの DDL 文は、1 つのバージョンでまとめてバッチ処理できます。古いスキーマ バージョンは、サーバーとストレージのリソースを大幅に消費する可能性があり、期限切れになるまで(古いバーションのデータの読み込みを提供する必要がなくなるまで)保持されます。

次の表は、Spanner でスキーマを更新するのにかかる時間を示しています。

スキーマのオペレーション 推定時間
CREATE TABLE 分
CREATE INDEX

数分から数時間(ベーステーブルがインデックスの前に作成される場合)。

数分(文がベーステーブルの CREATE TABLE 文と同時に実行される場合)。

DROP TABLE 分
DROP INDEX 分
ALTER TABLE ... ADD COLUMN 分
ALTER TABLE ... ALTER COLUMN

数分(バックグラウンド検証が必要な場合)。

数分(バックグラウンド検証が不要な場合)。

ALTER TABLE ... DROP COLUMN 分
ANALYZE

データベースのサイズに応じて数分から数時間。

データ型の変更と変更ストリーム

変更ストリームが監視する列のデータ型を変更すると、関連する変更ストリーム レコードの column_types フィールド レコードが、mods フィールド内の old_values JSON データと同様に、新しい型を反映します。

変更ストリーム レコードの mods フィールドの new_values は、常に列の現在の型と一致します。監視された列のデータ型を変更しても、その変更より前の変更ストリーム レコードに影響はありません。

BYTES から STRING へ変更する場合、Spanner はスキーマの更新の一環として列の古い値を検証します。その結果、Spanner は後続の変更ストリーム レコードを書き込むまでに、古い BYTES 型の値を文字列に安全にデコードします。

スキーマの更新に関するベスト プラクティス

以下のセクションでは、スキーマを更新するためのベスト プラクティスについて説明します。

スキーマ更新を発行する前の手順

スキーマの更新を発行する前に:

  • 変更しているデータベース内の既存のすべてのデータが、スキーマ更新が課している制約を満たしていることを確認します。一部のタイプのスキーマ更新の成功は、現在のスキーマだけでなく、データベース内のデータに依存するため、テスト データベースのスキーマ更新が成功しても、本番環境データベースのスキーマ更新が成功するとは限りません。いくつかの一般的な例を以下に示します。

    • 既存の列に NOT NULL アノテーションを追加する場合、列に既存の NULL 値が含まれないことを確認します。
    • STRING 型または BYTES 型の列に許可されている長さを短くする場合、その列の既存のすべての値が長さの制約を満たしていることを確認します。
  • スキーマ更新を行っている列、テーブル、インデックスに書き込む場合は、書き込む値が新しい制約を満たすようにします。

  • 列、テーブル、インデックスを削除する場合には、それが作成中または読み取り中でないことを確認してください。

スキーマ更新の頻度を制限する

短時間に多数のスキーマ更新を実行した場合、Spanner はキューに格納されたスキーマ更新の処理を throttle する可能性があります。これは、Spanner がスキーマのバージョンを保存するためのスペースの量を制限するためです。保持期間内に古いスキーマ バージョンが多すぎる場合は、スキーマの更新が抑制されることがあります。スキーマの変更の最大レートは、多くの要因によって異なります。そのうちの 1 つは、データベース内の列の合計数です。たとえば、2,000 列(INFORMATION_SCHEMA.COLUMNS で約 2,000 行)のデータベースでは、保持期間中に最大で 1,500 回のスキーマ変更(スキーマ変更が複数のバージョンを必要とする場合はより少ない回数)を実行できます。進行中のスキーマ更新の状態を確認するには、gcloud spanner operations list コマンドを使用して DATABASE_UPDATE_DDL 型のオペレーションでフィルタリングします。進行中のスキーマ更新をキャンセルするには、gcloud spanner operations cancel コマンドを使用して、オペレーション ID を指定します。

DDL ステートメントのバッチ方法と各バッチ内の順序は、生成されるスキーマ バージョンの数に影響します。一定期間内に実行できるスキーマ更新の数を最大化するには、スキーマ バージョンの数を最小限に抑えるバッチ処理を使用する必要があります。目安については、大規模な更新をご覧ください。

スキーマのバージョンで説明しているように、一部の DDL 文では複数のスキーマ バージョンが作成されます。これらのスキーマ バージョンは、各バッチ内でバッチ処理と順序付けを行うときに重要です。複数のスキーマ バージョンが作成されるステートメントには、主に 2 つのタイプがあります。

  • インデックス データのバックフィルを必要とするステートメント(CREATE INDEX など)
  • 既存のデータの検証が必要なステートメント(NOT NULL の追加など)

ただし、こうしたステートメントでは複数のスキーマ バージョンが常に作成されるわけではありません。Spanner は、バッチ化に依存する複数のスキーマ バージョンを使用しないように、これらのタイプのステートメントを最適化できるタイミングを検出します。たとえば、インデックスのベーステーブルに対する CREATE TABLE ステートメントと同じバッチで実行される CREATE INDEX ステートメントは、他のテーブルを介することなく、インデックス データをバックフィルする必要はありません。これは、Spanner がインデックスの作成時にベーステーブルが空であることを保証できるためです。大規模な更新のセクションでは、このプロパティを使用して多数のインデックスを効率的に作成する方法について説明します。

多数のスキーマ バージョンを作成しないように DDL ステートメントをバッチ処理できない場合は、保持期間内に単一のデータベースのスキーマに対するスキーマ更新の数を制限する必要があります。Spanner で新しいバージョンが作成される前に古いバージョンのスキーマを削除できるように、より長い期間でスキーマの更新を行います。

  • 一部のリレーショナル データベース管理システムには、本番環境のデプロイのたびに、データベースに対する長い一連のアップグレードとダウングレードのスキーマ更新を行うソフトウェア パッケージがあります。これらのタイプのプロセスは、Spanner では推奨されません。
  • Spanner は、主キーを使用してマルチテナンシー ソリューション用にデータを分割するように最適化されています。お客様ごとに個別のテーブルを使用するマルチテナンシー ソリューションでは、スキーマ更新オペレーションのバックログが大きくなり、完了に時間がかかる可能性があります。
  • 検証またはインデックスのバックフィルを必要とするスキーマ更新では、より多くのサーバー リソースが使用されます。これは、ステートメントごとに複数のバージョンのスキーマが内部的に作成されるためです。

大規模なスキーマ更新のオプション

テーブルを作成してそのテーブル上に多数のインデックスを作成する最適な方法は、すべてを同時に作成することで、1 つのスキーマ バージョンのみが作成されるようにすることです。DDL ステートメントのリストのテーブルの直後にインデックスを作成することをおすすめします。テーブルとそのインデックスは、データベースの作成時に、または DDL 文の単一の大規模なバッチで作成できます。 それぞれに多数のインデックスを持つ多数のテーブルを作成する必要がある場合は、すべてのステートメントを 1 つのバッチに含めることができます。1 つのスキーマ バージョンを使用してすべてのステートメントを同時に実行できる場合は、1 つのバッチに数千のステートメントを含めることができます。

ステートメントでインデックス データのバックフィルやデータ検証が必要な場合、単一のスキーマ バージョンでは実行できません。これは、インデックスのベーステーブルがすでに存在する場合に CREATE INDEX ステートメントで発生します(そのインデックスが前の DDL ステートメントのバッチで作成されたか、複数のスキーマ バージョンを必要とした CREATE TABLE ステートメントと CREATE INDEX ステートメントの間のバッチにステートメントがあったかのいずれかの理由で)。Spanner では、1 つのバッチでそのようなステートメントを 10 個以下にする必要があります。特にバックフィルを必要とするインデックス作成では、インデックスごとに複数のスキーマ バージョンを使用するため、1 日あたりバックフィルを行う必要がある新しいインデックスは 3 つ未満に作成することをおすすめします(このようなバッチ処理でバックフィルを回避できる場合を除き、どのようにバッチされるかに関係なく)。

たとえば、次のバッチのステートメントでは単一のスキーマ バージョンを使用します。

GoogleSQL

CREATE TABLE Singers (
SingerId   INT64 NOT NULL,
FirstName  STRING(1024),
LastName   STRING(1024),
) PRIMARY KEY (SingerId);

CREATE INDEX SingersByFirstName ON Singers(FirstName);

CREATE INDEX SingersByLastName ON Singers(LastName);

CREATE TABLE Albums (
SingerId   INT64 NOT NULL,
AlbumId    INT64 NOT NULL,
AlbumTitle STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId);

CREATE INDEX AlbumsByTitle ON Albums(AlbumTitle);

それに対して、このバッチでは多数のスキーマ バージョンが使用されます。その理由は、UnrelatedIndex ではバックフィル(ベーステーブルがすでに存在しているため)が必要で、次のインデックスもすべて(ベーステーブルと同じバッチにある場合でも)バックフィルを行う必要があります。

GoogleSQL

CREATE TABLE Singers (
SingerId   INT64 NOT NULL,
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);

CREATE INDEX UnrelatedIndex ON UnrelatedTable(UnrelatedIndexKey);

CREATE INDEX SingersByFirstName ON Singers(FirstName);

CREATE INDEX SingersByLastName ON Singers(LastName);

CREATE INDEX AlbumsByTitle ON Albums(AlbumTitle);

スキーマ バージョンを最小限に抑えるために、UnrelatedIndex の作成をバッチの最後、または別のバッチに移動することをおすすめします。

API リクエストの完了の待機

projects.instances.databases.updateDdl(REST API)リクエストまたは UpdateDatabaseDdl(RPC API)リクエストを行う場合は、それぞれ projects.instances.databases.operations.get(REST API)または GetOperation(RPC API)を使用して、新しいリクエストを開始する前に各リクエストの完了を待機します。各リクエストが完了するまで待機すると、アプリケーションがスキーマ更新の進行状況を追跡できます。また、保留中のスキーマ更新のバックログが管理可能なサイズに保持されます。

一括読み込み

テーブルを作成した後にテーブルにデータを一括して読み込む場合は、通常、データの読み込み後にインデックスを作成するほうが効率的です。複数のインデックスを追加する場合は、大規模なスキーマ更新のオプションで説明しているように、初期スキーマのすべてのテーブルとインデックスを使用してデータベースを作成するほうが効率的な場合があります。