Spanner を使用すると、ダウンタイムなしでスキーマの更新を行うことができます。いくつかの方法で既存のデータベースのスキーマを更新できます。
Google Cloud Console の場合
[Spanner Studio] ページで
ALTER TABLE
コマンドを送信します。[Spanner Studio] ページにアクセスするには、[データベースの概要] ページまたは [テーブルの概要] ページで [Spanner Studio] をクリックします。
gcloud spanner
コマンドライン ツールの使用gcloud spanner databases ddl update
コマンドを使用してALTER TABLE
コマンドを送信します。クライアント ライブラリの使用
projects.instances.databases.updateDdl
REST API の使用UpdateDatabaseDdl
RPC API の使用
サポートされているスキーマの更新
Spanner は、既存のデータベースに対する次のスキーマ更新をサポートしています。
- 名前付きスキーマを追加または削除します。
- 新しいテーブルを作成する 新しいテーブルの列は
NOT NULL
にできます。 - テーブルを削除する(そのテーブル内で他のテーブルがインターリーブされておらず、セカンダリ インデックスがない場合)。
- 外部キーを持つテーブルを作成または削除します。
- 既存のテーブルに対して外部キーを追加または削除します。
- 任意のテーブルに非キー列を追加する。新しい非キー列は
NOT NULL
にできません。 - 任意のテーブルから非キー列を削除する(セカンダリ インデックス、外部キー、格納されている生成列、またはチェック制約で使用されていない場合)。
- 非キー列に
NOT NULL
を追加する(ARRAY
列を除く)。 - 非キー列から
NOT NULL
を削除する。 STRING
列をBYTES
列に、またはBYTES
列をSTRING
列に変更する。PROTO
列をBYTES
列に、またはBYTES
列を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 |
数分から数時間(ベーステーブルがインデックスの前に作成される場合)。 数分(文がベーステーブルの |
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)を使用して、新しいリクエストを開始する前に各リクエストの完了を待機します。各リクエストが完了するまで待機すると、アプリケーションがスキーマ更新の進行状況を追跡できます。また、保留中のスキーマ更新のバックログが管理可能なサイズに保持されます。
一括読み込み
テーブルを作成した後にテーブルにデータを一括して読み込む場合は、通常、データの読み込み後にインデックスを作成する方が効率的です。複数のインデックスを追加する場合は、大規模なスキーマ更新のオプションで説明しているように、初期スキーマのすべてのテーブルとインデックスを使用してデータベースを作成するほうが効率的な場合があります。