スキーマ更新のベスト プラクティス

このドキュメントでは、スキーマを更新するためのベスト プラクティスについて説明します。

スキーマ更新を開始する前の手順

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

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

    • 既存の列に 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 など)。
  • Spanner に既存のデータの検証を強制するステートメント(NOT NULL や長さの制約の追加など)。

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

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

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

バッチ内の文の実行順序

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

Spanner では、同じバッチのステートメントを順番に適用し、最初のエラーで停止します。あるステートメントの適用でエラーが発生した場合、そのステートメントはロールバックされます。バッチ内の以前に適用されたステートメントの結果はロールバックされません。この順序どおりのステートメントの適用は、回避できないバックフィルを必要とするステートメントを並行して実行する場合(大規模な既存のテーブルに複数のインデックスを作成するなど)、各バックフィルに時間がかかる可能性があるため、これらのステートメントを個別のバッチで送信する必要があることを意味します。一方、インデックス付きの新しいテーブルを作成する場合は、バックフィルを完全に回避するために、単一のバッチにまとめて配置する(CREATE TABLE の後に CREATE INDEX を配置する)ことをおすすめします。

Spanner によってさまざまなバッチのステートメントが組み合わされて並べ替えられることがあるため、データベースに適用される 1 つのアトミック変更の中に、複数のバッチのステートメントが混在する可能性があります。それぞれのアトミック変更内で、さまざまなバッチのステートメントが任意の順序で出現します。たとえば、あるバッチのステートメントに ALTER TABLE table_name ALTER COLUMN column_name STRING(50) が含まれ、別のバッチのステートメントには ALTER TABLE table_name ALTER COLUMN column_name STRING(20) が含まれている場合、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)を使用して、新しいリクエストを開始する前に各リクエストの完了を待機します。各リクエストが完了するまで待機すると、アプリケーションがスキーマ更新の進行状況を追跡できます。また、保留中のスキーマ更新のバックログが管理可能なサイズに保持されます。

一括読み込み

新しいテーブルにデータを一括読み込みする場合は、データの読み込み前または読み込み後にセカンダリ インデックスを作成できます。読み込み後にインデックスを作成すると、データの読み込みは速くなりますが、インデックスをバックフィルする必要があります。

最初にデータを読み込んでからインデックスを作成すると、テーブルにのみ書き込まれるため、データの取り込みが速くなります。また、後でインデックスをバックフィルすると、テーブルデータと一緒にインデックス データを書き込むよりも効率的な最適化されたバッチでインデックス データを書き込むことができます。ただし、インデックスのバックフィルには 複数のスキーマ バージョンが必要であり、制限があります。大規模な 更新のオプションで説明したように、バックフィルを必要とするインデックスは 1 つのバッチで 10 個以下にすることをおすすめします。また、1 日あたり 3 個以下にすることをおすすめします。

または、大規模な更新のオプションで説明したように、同じバッチでテーブルとインデックスを作成することもできます。これにより、インデックスのバックフィルを回避できますが、データの読み込み時に各インデックスを更新する必要があるため、データの一括読み込みは遅くなります。

どちらを選択する方がよいかは、読み込むデータの量、特定のテーブルとインデックス キー、必要なインデックスの数、同じデータベースで一括読み込みオペレーションが必要になる頻度によって異なります。目安としては、各テーブルに大量のデータを読み込む必要があり、必要なインデックスが少ない場合は、インデックスを個別に作成することをおすすめします。