セカンダリ インデックス

Spanner データベースでは、Cloud Spanner により、テーブルの主キーごとに自動的にインデックスが作成されます。たとえば、Singers の主キーは自動的にインデックスに登録されるため、操作は必要はありません。

他の列のセカンダリ インデックスを作成することもできます。セカンダリ インデックスを列に追加すると、その列のデータをより効率的に検索できるようになります。たとえば、アルバムをタイトル別にすばやく検索する必要がある場合は、Spanner でテーブル全体をスキャンしなくてもいいように、AlbumTitle にセカンダリ インデックスを作成する必要があります。

上記の例で、読み取り / 書き込みトランザクション内で検索を行う場合、より効率的な検索では、テーブル全体をロックしたままにすることも回避します。これにより、AlbumTitle の検索範囲外のテーブルの行に対する挿入や更新が可能になります。

セカンダリ インデックスは、ルックアップを使用することで得られるメリットに加えて、Spanner でより効率的にスキャンを実行し、全テーブル スキャンではなくインデックス スキャンを行うこともできます。

Spanner では、各セカンダリ インデックスに次のデータが格納されます。

Spanner では、セカンダリ インデックスが適切なクエリに使用されるように、テーブルを経時的に分析します。

セカンダリ インデックスの追加

セカンダリ インデックスを最も効率的に追加できるタイミングは、テーブルの作成時です。テーブルとそのインデックスを同時に作成するには、新しいテーブルと新しいインデックスの DDL 文を 1 つのリクエストで Spanner に送信します。

Spanner では、データベースでトラフィックを引き続き処理しながら、既存のテーブルに新しいセカンダリ インデックスを追加することもできます。Spanner での他のスキーマの更新と同様に、既存のデータベースにインデックスを追加するときに、データベースをオフラインにする必要はありません。列またはテーブル全体をロックする必要もありません。

既存のテーブルに新しいインデックスが追加されると、Spanner はインデックスを自動的にバックフィルまたは入力し、インデックスに登録されているデータの最新状態を反映します。このバックフィル プロセスは Spanner によって管理され、このプロセスは低い優先度でノードリソースを使用してバックグラウンドで実行されます。ほとんどの場合、(ノードの追加などで)このプロセスを高速化することはできず、バックフィルはデータベースのパフォーマンスに大きな影響を与えません。

インデックスの作成には数分から数時間かかります。インデックスの作成はスキーマ更新であるため、他のスキーマ更新と同じパフォーマンス制約でバインドされます。セカンダリ インデックスを作成するために必要な時間は、次の要因によって決まります。

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

インデックスのバックフィル プロセスの進捗状況を表示するには、進捗状況のセクションをご覧ください。

セカンダリ インデックスの最初の部分として commit タイムスタンプ列を使用すると、ホットスポットが作成され、書き込みパフォーマンスが低下する可能性があるので注意してください。

スキーマでセカンダリ インデックスを定義するには、CREATE INDEX 文を使用します。次に例を示します。

データベースのすべての Singers を姓名でインデックスに登録するには、次の文を使用します。

GoogleSQL

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

PostgreSQL

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

データベースのすべての Songs のインデックスを SongName の値で作成するには、次の文を使用します。

GoogleSQL

CREATE INDEX SongsBySongName ON Songs(SongName);

PostgreSQL

CREATE INDEX SongsBySongName ON Songs(SongName);

特定の歌手の曲のみにインデックスを作成するには、INTERLEAVE IN 句を使用して、次のようにテーブル Singers のインデックスをインターリーブします。

GoogleSQL

CREATE INDEX SongsBySingerSongName ON Songs(SingerId, SongName),
    INTERLEAVE IN Singers;

PostgreSQL

CREATE INDEX SongsBySingerSongName ON Songs(SingerId, SongName)
    INTERLEAVE IN Singers;

特定のアルバムの曲のみにインデックスを作成する:

GoogleSQL

CREATE INDEX SongsBySingerAlbumSongName ON Songs(SingerId, AlbumId, SongName),
    INTERLEAVE IN Albums;

PostgreSQL

CREATE INDEX SongsBySingerAlbumSongName ON Songs(SingerId, AlbumId, SongName)
    INTERLEAVE IN Albums;

SongName の降順でインデックスに登録するには、次の文を使用します。

GoogleSQL

CREATE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, SongName DESC),
    INTERLEAVE IN Albums;

PostgreSQL

CREATE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, SongName DESC)
    INTERLEAVE IN Albums;

上記の DESC アノテーションは SongName にのみ適用されます。他のインデックス キーの降順でインデックスに登録するには、DESCSingerId DESC, AlbumId DESC というアノテーションを付けます。

また、PRIMARY_KEY は予約語であり、インデックスの名前としては使用できないことにご注意ください。これは、主キーの仕様を持つテーブルが作成される際に作成される疑似インデックスに与えられる名前です。

インターリーブされていないインデックスとインターリーブされたインデックスの選択の詳細とベスト プラクティスについては、インデックス オプション値が単調に増加または減少する列へのインターリーブされたインデックスの使用をご覧ください。

インデックスのバックフィルの進行状況を確認する

Console

  1. Spanner のナビゲーション メニューで [オペレーション] タブをクリックします。[オペレーション] ページに現在実行中のオペレーションのリストが表示されます。

  2. リストからスキーマ オペレーションを見つけます。まだ実行中の場合は、次の図に示すように、[終了時間] 列の進行状況インジケーターに、完了したオペレーションの割合が表示されます。

    98% を示す進行状況インジケーターのスクリーンショット

gcloud

gcloud spanner operations describe を使用してオペレーションの進行状況を確認します。

  1. オペレーション ID を取得します。

    gcloud spanner operations list --instance=INSTANCE-NAME \
    --database=DATABASE-NAME --type=DATABASE_UPDATE_DDL

    以下のように置き換えます。

    • INSTANCE-NAME は、Spanner インスタンス名に置き換えます。
    • DATABASE-NAME は、データベースの名前に置き換えます。

    使用上の注意:

    • 表示内容を限定するには、--filter フラグを指定します。次に例を示します。

      • --filter="metadata.name:example-db" は、特定のデータベースのオペレーションのみを一覧表示します。
      • --filter="error:*" には、失敗したバックアップ オペレーションのみが一覧表示されます。

      フィルタ構文の詳細については、gcloud topic filters をご覧ください。バックアップ オペレーションのフィルタリングについては、ListBackupOperationsRequestfilter フィールドをご覧ください。

    • --type フラグでは大文字と小文字は区別されません。

    出力は次のようになります。

    OPERATION_ID     STATEMENTS                                                                                          DONE   @TYPE
    _auto_op_123456  CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName)                                 False  UpdateDatabaseDdlMetadata
                    CREATE INDEX SongsBySingerAlbumSongName ON Songs(SingerId, AlbumId, SongName), INTERLEAVE IN Albums
    _auto_op_234567                                                                                                      True   CreateDatabaseMetadata
    
  2. gcloud spanner operations describe を実行します。

    gcloud spanner operations describe \
    --instance=INSTANCE-NAME \
    --database=DATABASE-NAME \
    projects/PROJECT-NAME/instances/INSTANCE-NAME/databases/DATABASE-NAME/operations/OPERATION_ID

    以下を置き換えます。

    • INSTANCE-NAME: Spanner インスタンス名。
    • DATABASE-NAME: Spanner データベース名。
    • PROJECT-NAME: プロジェクト名。
    • OPERATION-ID: 確認するオペレーションのオペレーション ID。

    出力の progress セクションに完了したオペレーションの割合が表示されます。出力は次のようになります。

    done: true
    ...
      progress:
      - endTime: '2021-01-22T21:58:42.912540Z'
        progressPercent: 100
        startTime: '2021-01-22T21:58:11.053996Z'
      - progressPercent: 67
        startTime: '2021-01-22T21:58:11.053996Z'
    ...
    

REST v1

オペレーション ID を取得します。

  gcloud spanner operations list --instance=INSTANCE-NAME 
--database=DATABASE-NAME --type=DATABASE_UPDATE_DDL

以下のように置き換えます。

  • INSTANCE-NAME は、Spanner インスタンス名に置き換えます。
  • DATABASE-NAME は、データベースの名前に置き換えます。

リクエストのデータを使用する前に、次のように置き換えます。

  • PROJECT-ID: プロジェクト ID。
  • INSTANCE-ID: インスタンス ID。
  • DATABASE-ID: データベース ID。
  • OPERATION-ID: オペレーション ID。

HTTP メソッドと URL:

GET https://spanner.googleapis.com/v1/projects/PROJECT-ID/instances/INSTANCE-ID/databases/DATABASE-ID/operations/OPERATION-ID

リクエストを送信するには、次のいずれかのオプションを展開します。

次のような JSON レスポンスが返されます。

{
...
    "progress": [
      {
        "progressPercent": 100,
        "startTime": "2023-05-27T00:52:27.366688Z",
        "endTime": "2023-05-27T00:52:30.184845Z"
      },
      {
        "progressPercent": 100,
        "startTime": "2023-05-27T00:52:30.184845Z",
        "endTime": "2023-05-27T00:52:40.750959Z"
      }
    ],
...
  "done": true,
  "response": {
    "@type": "type.googleapis.com/google.protobuf.Empty"
  }
}

gcloud と REST の場合、各インデックスのバックフィル ステートメントの進行状況は、progress セクションで確認できます。ステートメント配列内のステートメントごとに、進行状況配列に対応するフィールドが存在します。進行状況の配列の順序は、ステートメントの配列の順序に対応しています。利用可能になると、startTimeprogressPercentendTime の各フィールドに適宜値が入力されます。 出力には、バックフィルの進行が完了するまでの推定時間は表示されません。

オペレーションに時間がかかりすぎる場合、キャンセルできます。詳細については、インデックス作成のキャンセルをご覧ください。

インデックスのバックフィルの進捗状況を表示する場合のシナリオ

インデックスのバックフィルの進捗状況を確認しようとすると、さまざまなシナリオが生じる可能性があります。インデックスのバックフィルを必要とするインデックス作成ステートメントは、スキーマ更新オペレーションの一部です。いくつかのステートメントをスキーマ更新オペレーションの構成要素にできます。

最初のシナリオが最も簡単で、インデックス作成ステートメントがスキーマ更新オペレーションの最初のステートメントである場合です。インデックス作成ステートメントは最初のステートメントであるため、実行の順序によって最初に処理され、実行されます。すぐに、インデックス作成ステートメントの startTime フィールドにスキーマ更新オペレーションの開始時間が入力されます。次に、インデックスのバックフィルの進行状況が 0% を超えると、インデックス作成ステートメントの progressPercent フィールドに値が入力されます。最後に、ステートメントが commit されると、endTime フィールドに値が入力されます。

2 番目のシナリオは、インデックス作成ステートメントがスキーマ更新オペレーションの最初のステートメントではない場合です。実行の順序によって、前のステートメントが commit されるまで、インデックス作成ステートメントに関連するフィールドは入力されません。前述のシナリオと同様に、前のステートメントが commit されると、最初にインデックス作成ステートメントの startTime フィールドに入力され、次に progressPercent フィールドに入力されます。最後に、ステートメントの commit が終了すると、endTime フィールドに値が入力されます。

インデックスの作成をキャンセルする

Google Cloud CLI を使用してインデックスの作成をキャンセルできます。Spanner データベースのスキーマ更新オペレーションのリストを取得するには、gcloud spanner operations list コマンドを使用して --filter オプションを含めます。

gcloud spanner operations list \
    --instance=INSTANCE \
    --database=DATABASE \
    --filter="@TYPE:UpdateDatabaseDdlMetadata"

キャンセルするオペレーションの OPERATION_ID を検索してから、gcloud spanner operations cancel コマンドを使用してキャンセルします。

gcloud spanner operations cancel OPERATION_ID \
    --instance=INSTANCE \
    --database=DATABASE

既存のインデックスを表示する

データベース内の既存のインデックスに関する情報を表示するには、Google Cloud コンソール または Google Cloud CLI を使用できます。

Console

  1. Google Cloud コンソールで、Spanner の [インスタンス] ページに移動します。

    インスタンス ページに移動

  2. 確認するインスタンスの名前をクリックします。

  3. 左側のペインで、表示するデータベースをクリックし、表示するテーブルをクリックします。

  4. [インデックス] タブをクリックします。Google Cloud コンソールに、インデックスのリストが表示されます。

  5. オプション: インデックスに含まれる列など、インデックスの詳細を取得するには、インデックスの名前をクリックします。

gcloud

gcloud spanner databases ddl describe コマンドを使用します。

    gcloud spanner databases ddl describe DATABASE \
        --instance=INSTANCE

gcloud CLI により、データベースのテーブルとインデックスを作成するためのデータ定義言語(DDL)文が出力されます。CREATE INDEX 文は、既存のインデックスを記述します。次に例を示します。

    --- |-
  CREATE TABLE Singers (
    SingerId INT64 NOT NULL,
    FirstName STRING(1024),
    LastName STRING(1024),
    SingerInfo BYTES(MAX),
  ) PRIMARY KEY(SingerId)
---
  CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName)

特定のインデックスを使用したクエリ

以降のセクションでは、SQL ステートメントでインデックスを指定する方法と Spanner の読み取りインターフェースを使用する方法について説明します。これらのセクションの例では、MarketingBudget 列を Albums テーブルに追加し、AlbumsByAlbumTitle というインデックスを作成したと想定しています。

GoogleSQL

CREATE TABLE Albums (
  SingerId         INT64 NOT NULL,
  AlbumId          INT64 NOT NULL,
  AlbumTitle       STRING(MAX),
  MarketingBudget  INT64,
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle);

PostgreSQL

CREATE TABLE Albums (
  SingerId         BIGINT NOT NULL,
  AlbumId          BIGINT NOT NULL,
  AlbumTitle       VARCHAR,
  MarketingBudget  BIGINT,
  PRIMARY KEY (SingerId, AlbumId)
) INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle);

SQL ステートメントでのインデックスの指定

SQL を使用して Spanner テーブルに対してクエリを実行する場合、Spanner では、クエリを効率化する可能性のあるインデックスが自動的に使用されます。結果として、SQL クエリにインデックスを指定する必要はありません。しかし、ワークロードに重要なクエリについては、より安定したパフォーマンスのため、SQL ステートメントで FORCE_INDEX ディレクティブを使用することを Google はおすすめします。

まれに、Spanner がクエリのレイテンシを増加させるインデックスを選択することがあります。パフォーマンス低下のトラブルシューティングの手順に従い、クエリに別のインデックスを試すことが理にかなっていると確認した場合、クエリの一部としてインデックスを指定できます。

SQL ステートメントでインデックスを指定するには、FORCE_INDEX ヒントを使用してインデックス ディレクティブを指定します。インデックス ディレクティブでは、次の構文を使用します。

GoogleSQL

FROM MyTable@{FORCE_INDEX=MyTableIndex}

PostgreSQL

FROM MyTable /*@ FORCE_INDEX = MyTableIndex */

インデックスを使用せずにベーステーブルをスキャンするよう Spanner に指示するために、インデックス ディレクティブも使用できます。

GoogleSQL

FROM MyTable@{FORCE_INDEX=_BASE_TABLE}

PostgreSQL

FROM MyTable /*@ FORCE_INDEX = _BASE_TABLE */

次の例は、インデックスを指定する SQL クエリを示しています。

GoogleSQL

SELECT AlbumId, AlbumTitle, MarketingBudget
    FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}
    WHERE AlbumTitle >= "Aardvark" AND AlbumTitle < "Goo";

PostgreSQL

SELECT AlbumId, AlbumTitle, MarketingBudget
    FROM Albums /*@ FORCE_INDEX = AlbumsByAlbumTitle */
    WHERE AlbumTitle >= 'Aardvark' AND AlbumTitle < 'Goo';

インデックス ディレクティブは、Spanner のクエリ プロセッサに対して、(インデックスには格納されていないが)クエリに必要な追加の列を読み取るよう、強制する場合があります。クエリ プロセッサは、インデックスとベーステーブルを結合して、これらの列を取得します。この余分な結合を回避するには、STORING 句(GoogleSQL 言語データベース)または INCLUDE 句(PostgreSQL 言語データベース)を使用して、追加の列をインデックスに保存します。

上の例では、MarketingBudget 列はインデックスに格納されませんが、SQL クエリではこの列が選択されます。結果として、Spanner では、ベーステーブルで MarketingBudget 列を検索してからインデックスのデータと結合し、クエリ結果を返す必要があります。

インデックス ディレクティブに次のいずれかの問題がある場合、Spanner でエラーが発生します。

次の例は、インデックス AlbumsByAlbumTitle を使用して AlbumIdAlbumTitleMarketingBudget の値をフェッチするクエリを作成して実行する方法を示しています。

C++

void QueryUsingIndex(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;

  spanner::SqlStatement select(
      "SELECT AlbumId, AlbumTitle, MarketingBudget"
      " FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}"
      " WHERE AlbumTitle >= @start_title AND AlbumTitle < @end_title",
      {{"start_title", spanner::Value("Aardvark")},
       {"end_title", spanner::Value("Goo")}});
  using RowType =
      std::tuple<std::int64_t, std::string, absl::optional<std::int64_t>>;
  auto rows = client.ExecuteQuery(std::move(select));
  for (auto& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::move(row).status();
    std::cout << "AlbumId: " << std::get<0>(*row) << "\t";
    std::cout << "AlbumTitle: " << std::get<1>(*row) << "\t";
    auto marketing_budget = std::get<2>(*row);
    if (marketing_budget) {
      std::cout << "MarketingBudget: " << *marketing_budget << "\n";
    } else {
      std::cout << "MarketingBudget: NULL\n";
    }
  }
  std::cout << "Read completed for [spanner_query_data_with_index]\n";
}

C#


using Google.Cloud.Spanner.Data;
using System.Collections.Generic;
using System.Threading.Tasks;

public class QueryDataWithIndexAsyncSample
{
    public class Album
    {
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
        public long MarketingBudget { get; set; }
    }

    public async Task<List<Album>> QueryDataWithIndexAsync(string projectId, string instanceId, string databaseId,
        string startTitle, string endTitle)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";
        using var connection = new SpannerConnection(connectionString);
        using var cmd = connection.CreateSelectCommand(
            "SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@ "
            + "{FORCE_INDEX=AlbumsByAlbumTitle} "
            + $"WHERE AlbumTitle >= @startTitle "
            + $"AND AlbumTitle < @endTitle",
            new SpannerParameterCollection
            {
                { "startTitle", SpannerDbType.String, startTitle },
                { "endTitle", SpannerDbType.String, endTitle }
            });

        var albums = new List<Album>();
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            albums.Add(new Album
            {
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                AlbumTitle = reader.GetFieldValue<string>("AlbumTitle"),
                MarketingBudget = reader.IsDBNull(reader.GetOrdinal("MarketingBudget")) ? 0 : reader.GetFieldValue<long>("MarketingBudget")
            });
        }
        return albums;
    }
}

Go


import (
	"context"
	"fmt"
	"io"
	"strconv"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func queryUsingIndex(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{
		SQL: `SELECT AlbumId, AlbumTitle, MarketingBudget
			FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}
			WHERE AlbumTitle >= @start_title AND AlbumTitle < @end_title`,
		Params: map[string]interface{}{
			"start_title": "Aardvark",
			"end_title":   "Goo",
		},
	}
	iter := client.Single().Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return err
		}
		var albumID int64
		var marketingBudget spanner.NullInt64
		var albumTitle string
		if err := row.ColumnByName("AlbumId", &albumID); err != nil {
			return err
		}
		if err := row.ColumnByName("AlbumTitle", &albumTitle); err != nil {
			return err
		}
		if err := row.ColumnByName("MarketingBudget", &marketingBudget); err != nil {
			return err
		}
		budget := "NULL"
		if marketingBudget.Valid {
			budget = strconv.FormatInt(marketingBudget.Int64, 10)
		}
		fmt.Fprintf(w, "%d %s %s\n", albumID, albumTitle, budget)
	}
	return nil
}

Java

static void queryUsingIndex(DatabaseClient dbClient) {
  Statement statement =
      Statement
          // We use FORCE_INDEX hint to specify which index to use. For more details see
          // https://cloud.google.com/spanner/docs/query-syntax#from-clause
          .newBuilder(
              "SELECT AlbumId, AlbumTitle, MarketingBudget "
                  + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle} "
                  + "WHERE AlbumTitle >= @StartTitle AND AlbumTitle < @EndTitle")
          // We use @BoundParameters to help speed up frequently executed queries.
          //  For more details see https://cloud.google.com/spanner/docs/sql-best-practices
          .bind("StartTitle")
          .to("Aardvark")
          .bind("EndTitle")
          .to("Goo")
          .build();
  try (ResultSet resultSet = dbClient.singleUse().executeQuery(statement)) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %s %s\n",
          resultSet.getLong("AlbumId"),
          resultSet.getString("AlbumTitle"),
          resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"));
    }
  }
}

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const instanceId = 'my-instance';
// const databaseId = 'my-database';
// const projectId = 'my-project-id';
// const startTitle = 'Ardvark';
// const endTitle = 'Goo';

// Imports the Google Cloud Spanner client library
const {Spanner} = require('@google-cloud/spanner');

// Instantiates a client
const spanner = new Spanner({
  projectId: projectId,
});

async function queryDataWithIndex() {
  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  const query = {
    sql: `SELECT AlbumId, AlbumTitle, MarketingBudget
                FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}
                WHERE AlbumTitle >= @startTitle AND AlbumTitle <= @endTitle`,
    params: {
      startTitle: startTitle,
      endTitle: endTitle,
    },
  };

  // Queries rows from the Albums table
  try {
    const [rows] = await database.run(query);

    rows.forEach(row => {
      const json = row.toJSON();
      const marketingBudget = json.MarketingBudget
        ? json.MarketingBudget
        : null; // This value is nullable
      console.log(
        `AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}, MarketingBudget: ${marketingBudget}`
      );
    });
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    // Close the database when finished.
    database.close();
  }
}
queryDataWithIndex();

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Queries sample data from the database using SQL and an index.
 *
 * The index must exist before running this sample. You can add the index
 * by running the `add_index` sample or by running this DDL statement against
 * your database:
 *
 *     CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)
 *
 * Example:
 * ```
 * query_data_with_index($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 * @param string $startTitle The start of the title index.
 * @param string $endTitle   The end of the title index.
 */
function query_data_with_index(
    string $instanceId,
    string $databaseId,
    string $startTitle = 'Aardvark',
    string $endTitle = 'Goo'
): void {
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $parameters = [
        'startTitle' => $startTitle,
        'endTitle' => $endTitle
    ];

    $results = $database->execute(
        'SELECT AlbumId, AlbumTitle, MarketingBudget ' .
        'FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle} ' .
        'WHERE AlbumTitle >= @startTitle AND AlbumTitle < @endTitle',
        ['parameters' => $parameters]
    );

    foreach ($results as $row) {
        printf('AlbumId: %s, AlbumTitle: %s, MarketingBudget: %d' . PHP_EOL,
            $row['AlbumId'], $row['AlbumTitle'], $row['MarketingBudget']);
    }
}

Python

def query_data_with_index(
    instance_id, database_id, start_title="Aardvark", end_title="Goo"
):
    """Queries sample data from the database using SQL and an index.

    The index must exist before running this sample. You can add the index
    by running the `add_index` sample or by running this DDL statement against
    your database:

        CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)

    This sample also uses the `MarketingBudget` column. You can add the column
    by running the `add_column` sample or by running this DDL statement against
    your database:

        ALTER TABLE Albums ADD COLUMN MarketingBudget INT64

    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    params = {"start_title": start_title, "end_title": end_title}
    param_types = {
        "start_title": spanner.param_types.STRING,
        "end_title": spanner.param_types.STRING,
    }

    with database.snapshot() as snapshot:
        results = snapshot.execute_sql(
            "SELECT AlbumId, AlbumTitle, MarketingBudget "
            "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle} "
            "WHERE AlbumTitle >= @start_title AND AlbumTitle < @end_title",
            params=params,
            param_types=param_types,
        )

        for row in results:
            print("AlbumId: {}, AlbumTitle: {}, " "MarketingBudget: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"
# start_title = "An album title to start with such as 'Ardvark'"
# end_title   = "An album title to end with such as 'Goo'"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

sql_query = "SELECT AlbumId, AlbumTitle, MarketingBudget
             FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}
             WHERE AlbumTitle >= @start_title AND AlbumTitle < @end_title"

params      = { start_title: start_title, end_title: end_title }
param_types = { start_title: :STRING,     end_title: :STRING }

client.execute(sql_query, params: params, types: param_types).rows.each do |row|
  puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:MarketingBudget]}"
end

読み取りインターフェースでのインデックスの指定

Spanner に対して読み取りインターフェースを使用し、Spanner でインデックスを使用する場合、インデックスを指定する必要があります。読み取りインターフェースでインデックスが自動的に選択されることはありません。

また、インデックスには、主キーの一部である列を除いて、クエリ結果に表示されるすべてのデータを含める必要があります。この制限は、読み取りインターフェースでインデックスとベーステーブル間の結合がサポートされていないために存在します。クエリ結果に他の列を含める必要がある場合は、いくつかのオプションがあります。

  • STORINGINCLUDEを使用して、追加の列をインデックスに格納します。
  • 追加の列を含めずにクエリを実行してから、主キーを使用して、追加の列を読み取る別のクエリを送信します。

Spanner では、インデックスの値はインデックス キーの昇順で返されます。値を降順で取得するには、次の手順を行います。

  • インデックス キーに DESC というアノテーションを付けます。次に例を示します。

    CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle DESC);
    

    DESC アノテーションは、単一のインデックス キーに適用されます。インデックスに複数のキーが含まれ、クエリ結果をすべてのキーに基づいて降順で表示する必要がある場合は、各キーについて DESC アノテーションを追加します。

  • 読み取りでキー範囲が指定されている場合は、キー範囲も降順であることを確認します。つまり、開始キーの値は終了キーの値より大きい必要があります。

次の例は、インデックス AlbumsByAlbumTitle を使用して、AlbumIdAlbumTitle の値を取得する方法を示しています。

C++

void ReadDataWithIndex(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;

  auto rows =
      client.Read("Albums", google::cloud::spanner::KeySet::All(),
                  {"AlbumId", "AlbumTitle"},
                  google::cloud::Options{}.set<spanner::ReadIndexNameOption>(
                      "AlbumsByAlbumTitle"));
  using RowType = std::tuple<std::int64_t, std::string>;
  for (auto& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::move(row).status();
    std::cout << "AlbumId: " << std::get<0>(*row) << "\t";
    std::cout << "AlbumTitle: " << std::get<1>(*row) << "\n";
  }
  std::cout << "Read completed for [spanner_read_data_with_index]\n";
}

C#


using Google.Cloud.Spanner.Data;
using System.Collections.Generic;
using System.Threading.Tasks;

public class QueryDataWithIndexAsyncSample
{
    public class Album
    {
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
        public long MarketingBudget { get; set; }
    }

    public async Task<List<Album>> QueryDataWithIndexAsync(string projectId, string instanceId, string databaseId,
        string startTitle, string endTitle)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";
        using var connection = new SpannerConnection(connectionString);
        using var cmd = connection.CreateSelectCommand(
            "SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@ "
            + "{FORCE_INDEX=AlbumsByAlbumTitle} "
            + $"WHERE AlbumTitle >= @startTitle "
            + $"AND AlbumTitle < @endTitle",
            new SpannerParameterCollection
            {
                { "startTitle", SpannerDbType.String, startTitle },
                { "endTitle", SpannerDbType.String, endTitle }
            });

        var albums = new List<Album>();
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            albums.Add(new Album
            {
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                AlbumTitle = reader.GetFieldValue<string>("AlbumTitle"),
                MarketingBudget = reader.IsDBNull(reader.GetOrdinal("MarketingBudget")) ? 0 : reader.GetFieldValue<long>("MarketingBudget")
            });
        }
        return albums;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func readUsingIndex(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	iter := client.Single().ReadUsingIndex(ctx, "Albums", "AlbumsByAlbumTitle", spanner.AllKeys(),
		[]string{"AlbumId", "AlbumTitle"})
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var albumID int64
		var albumTitle string
		if err := row.Columns(&albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %s\n", albumID, albumTitle)
	}
}

Java

static void readUsingIndex(DatabaseClient dbClient) {
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .readUsingIndex(
              "Albums",
              "AlbumsByAlbumTitle",
              KeySet.all(),
              Arrays.asList("AlbumId", "AlbumTitle"))) {
    while (resultSet.next()) {
      System.out.printf("%d %s\n", resultSet.getLong(0), resultSet.getString(1));
    }
  }
}

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const instanceId = 'my-instance';
// const databaseId = 'my-database';
// const projectId = 'my-project-id';

// Imports the Google Cloud Spanner client library
const {Spanner} = require('@google-cloud/spanner');

// Instantiates a client
const spanner = new Spanner({
  projectId: projectId,
});

async function readDataWithIndex() {
  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  const albumsTable = database.table('Albums');

  const query = {
    columns: ['AlbumId', 'AlbumTitle'],
    keySet: {
      all: true,
    },
    index: 'AlbumsByAlbumTitle',
  };

  // Reads the Albums table using an index
  try {
    const [rows] = await albumsTable.read(query);

    rows.forEach(row => {
      const json = row.toJSON();
      console.log(`AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`);
    });
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    // Close the database when finished.
    database.close();
  }
}
readDataWithIndex();

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Reads sample data from the database using an index.
 *
 * The index must exist before running this sample. You can add the index
 * by running the `add_index` sample or by running this DDL statement against
 * your database:
 *
 *     CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)
 *
 * Example:
 * ```
 * read_data_with_index($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_data_with_index(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $keySet = $spanner->keySet(['all' => true]);
    $results = $database->read(
        'Albums',
        $keySet,
        ['AlbumId', 'AlbumTitle'],
        ['index' => 'AlbumsByAlbumTitle']
    );

    foreach ($results->rows() as $row) {
        printf('AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['AlbumId'], $row['AlbumTitle']);
    }
}

Python

def read_data_with_index(instance_id, database_id):
    """Reads sample data from the database using an index.

    The index must exist before running this sample. You can add the index
    by running the `add_index` sample or by running this DDL statement against
    your database:

        CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)

    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    with database.snapshot() as snapshot:
        keyset = spanner.KeySet(all_=True)
        results = snapshot.read(
            table="Albums",
            columns=("AlbumId", "AlbumTitle"),
            keyset=keyset,
            index="AlbumsByAlbumTitle",
        )

        for row in results:
            print("AlbumId: {}, AlbumTitle: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

result = client.read "Albums", [:AlbumId, :AlbumTitle],
                     index: "AlbumsByAlbumTitle"

result.rows.each do |row|
  puts "#{row[:AlbumId]} #{row[:AlbumTitle]}"
end

インデックスのみをスキャンするインデックスを作成する

必要に応じて、STORING 句(GoogleSQL 言語データベースの場合)または INCLUDE 句(PostgreSQL 言語データベースの場合)を使用してインデックスに列のコピーを保存できます。このタイプのインデックスを使用すると、クエリと読み取り呼び出しを実行する際に、次のメリットがあります(追加ストレージを使用する費用がかります)。

  • インデックスを使用し、STORING または INCLUDE 句に保存された列を選択する SQL クエリで、ベーステーブルへの余分な結合が不要になります。
  • インデックスを使用する read() 呼び出しで、STORING / INCLUDE 句で保存された列を読み取ることができます。

たとえば、MarketingBudget 列のコピーをインデックスに保存する AlbumsByAlbumTitle の代替バージョンを作成したとします(STORING または INCLUDE句は太字になっています)。

GoogleSQL

CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget);

PostgreSQL

CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) INCLUDE (MarketingBudget);

古い AlbumsByAlbumTitle インデックスを使用する場合、Spanner では、インデックスをベーステーブルと結合した後にベーステーブルからその列を取得する必要があります。新しい AlbumsByAlbumTitle2 インデックスを使用する場合、Spanner では、インデックスから直接列を読み取るため、より効率的です。

SQL の代わりに読み取りインターフェースを使用する場合、新しい AlbumsByAlbumTitle2 インデックスを使用すると、MarketingBudget 列を直接読み取ることもできます。

C++

void ReadDataWithStoringIndex(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;

  auto rows =
      client.Read("Albums", google::cloud::spanner::KeySet::All(),
                  {"AlbumId", "AlbumTitle", "MarketingBudget"},
                  google::cloud::Options{}.set<spanner::ReadIndexNameOption>(
                      "AlbumsByAlbumTitle2"));
  using RowType =
      std::tuple<std::int64_t, std::string, absl::optional<std::int64_t>>;
  for (auto& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::move(row).status();
    std::cout << "AlbumId: " << std::get<0>(*row) << "\t";
    std::cout << "AlbumTitle: " << std::get<1>(*row) << "\t";
    auto marketing_budget = std::get<2>(*row);
    if (marketing_budget) {
      std::cout << "MarketingBudget: " << *marketing_budget << "\n";
    } else {
      std::cout << "MarketingBudget: NULL\n";
    }
  }
  std::cout << "Read completed for [spanner_read_data_with_storing_index]\n";
}

C#


using Google.Cloud.Spanner.Data;
using System.Collections.Generic;
using System.Threading.Tasks;

public class QueryDataWithStoringIndexAsyncSample
{
    public class Album
    {
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
        public long? MarketingBudget { get; set; }
    }

    public async Task<List<Album>> QueryDataWithStoringIndexAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        var cmd = connection.CreateSelectCommand(
            "SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@ "
            + "{FORCE_INDEX=AlbumsByAlbumTitle2}");

        var albums = new List<Album>();
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            albums.Add(new Album
            {
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                AlbumTitle = reader.GetFieldValue<string>("AlbumTitle"),
                MarketingBudget = reader.IsDBNull(reader.GetOrdinal("MarketingBudget")) ? 0 : reader.GetFieldValue<long>("MarketingBudget")
            });
        }
        return albums;
    }
}

Go


import (
	"context"
	"fmt"
	"io"
	"strconv"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func readStoringIndex(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	iter := client.Single().ReadUsingIndex(ctx, "Albums", "AlbumsByAlbumTitle2", spanner.AllKeys(),
		[]string{"AlbumId", "AlbumTitle", "MarketingBudget"})
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var albumID int64
		var marketingBudget spanner.NullInt64
		var albumTitle string
		if err := row.Columns(&albumID, &albumTitle, &marketingBudget); err != nil {
			return err
		}
		budget := "NULL"
		if marketingBudget.Valid {
			budget = strconv.FormatInt(marketingBudget.Int64, 10)
		}
		fmt.Fprintf(w, "%d %s %s\n", albumID, albumTitle, budget)
	}
}

Java

static void readStoringIndex(DatabaseClient dbClient) {
  // We can read MarketingBudget also from the index since it stores a copy of MarketingBudget.
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .readUsingIndex(
              "Albums",
              "AlbumsByAlbumTitle2",
              KeySet.all(),
              Arrays.asList("AlbumId", "AlbumTitle", "MarketingBudget"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %s %s\n",
          resultSet.getLong(0),
          resultSet.getString(1),
          resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"));
    }
  }
}

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const instanceId = 'my-instance';
// const databaseId = 'my-database';
// const projectId = 'my-project-id';

// Imports the Google Cloud Spanner client library
const {Spanner} = require('@google-cloud/spanner');

// Instantiates a client
const spanner = new Spanner({
  projectId: projectId,
});

// "Storing" indexes store copies of the columns they index
// This speeds up queries, but takes more space compared to normal indexes
// See the link below for more information:
// https://cloud.google.com/spanner/docs/secondary-indexes#storing_clause
async function readDataWithStoringIndex() {
  // Gets a reference to a Cloud Spanner instance and database
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  const albumsTable = database.table('Albums');

  const query = {
    columns: ['AlbumId', 'AlbumTitle', 'MarketingBudget'],
    keySet: {
      all: true,
    },
    index: 'AlbumsByAlbumTitle2',
  };

  // Reads the Albums table using a storing index
  try {
    const [rows] = await albumsTable.read(query);

    rows.forEach(row => {
      const json = row.toJSON();
      let rowString = `AlbumId: ${json.AlbumId}`;
      rowString += `, AlbumTitle: ${json.AlbumTitle}`;
      if (json.MarketingBudget) {
        rowString += `, MarketingBudget: ${json.MarketingBudget}`;
      }
      console.log(rowString);
    });
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    // Close the database when finished.
    database.close();
  }
}
readDataWithStoringIndex();

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Reads sample data from the database using an index with a storing
 * clause.
 *
 * The index must exist before running this sample. You can add the index
 * by running the `add_storing_index` sample or by running this DDL statement
 * against your database:
 *
 *     CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)
 *     STORING (MarketingBudget)
 *
 * Example:
 * ```
 * read_data_with_storing_index($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_data_with_storing_index(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $keySet = $spanner->keySet(['all' => true]);
    $results = $database->read(
        'Albums',
        $keySet,
        ['AlbumId', 'AlbumTitle', 'MarketingBudget'],
        ['index' => 'AlbumsByAlbumTitle2']
    );

    foreach ($results->rows() as $row) {
        printf('AlbumId: %s, AlbumTitle: %s, MarketingBudget: %d' . PHP_EOL,
            $row['AlbumId'], $row['AlbumTitle'], $row['MarketingBudget']);
    }
}

Python

def read_data_with_storing_index(instance_id, database_id):
    """Reads sample data from the database using an index with a storing
    clause.

    The index must exist before running this sample. You can add the index
    by running the `add_scoring_index` sample or by running this DDL statement
    against your database:

        CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)
        STORING (MarketingBudget)

    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    with database.snapshot() as snapshot:
        keyset = spanner.KeySet(all_=True)
        results = snapshot.read(
            table="Albums",
            columns=("AlbumId", "AlbumTitle", "MarketingBudget"),
            keyset=keyset,
            index="AlbumsByAlbumTitle2",
        )

        for row in results:
            print("AlbumId: {}, AlbumTitle: {}, " "MarketingBudget: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

result = client.read "Albums", [:AlbumId, :AlbumTitle, :MarketingBudget],
                     index: "AlbumsByAlbumTitle2"

result.rows.each do |row|
  puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:MarketingBudget]}"
end

インデックスを変更する

ALTER INDEX ステートメントを使用して、既存のインデックスまたはドロップ列に列を追加できます。インデックスを作成する場合は、STORING 句(GoogleSQL 言語データベース)または INCLUDE 句(PostgreSQL 言語データベース)によって定義された列リストを更新できます。このステートメントを使用して、インデックス キーに対して列の追加または削除を行うことはできません。たとえば、新しいインデックス AlbumsByAlbumTitle2 を作成する代わりに、ALTER INDEX を使用して AlbumsByAlbumTitle に列を追加できます。次の例をご覧ください。

GoogleSQL

ALTER INDEX AlbumsByAlbumTitle ADD STORED COLUMN MarketingBudget

PostgreSQL

ALTER INDEX AlbumsByAlbumTitle ADD INCLUDE COLUMN MarketingBudget

既存のインデックスに新しい列を追加すると、Spanner はバックグラウンドのバックフィル プロセスを使用します。バックフィルの進行中は、インデックス内の列が読み取れないため、期待されるパフォーマンスの向上が得られない可能性があります。gcloud spanner operations コマンドを使用すると、長時間実行オペレーションを一覧表示し、そのステータスを確認できます。詳細については、オペレーションの説明をご覧ください。

オペレーションをキャンセルを使用して、実行中のオペレーションをキャンセルすることもできます。

バックフィルが完了すると、Spanner は列をインデックスに追加します。インデックスが大きくなると、インデックスを使用するクエリの速度が低下する可能性があります。

次の例は、インデックスから列を削除する方法を示しています。

GoogleSQL

ALTER INDEX AlbumsByAlbumTitle DROP STORED COLUMN MarketingBudget

PostgreSQL

ALTER INDEX AlbumsByAlbumTitle DROP INCLUDE COLUMN MarketingBudget

NULL 値のインデックス

デフォルトでは、Spanner は NULL 値をインデックスに登録します。たとえば、テーブル Singers のインデックス SingersByFirstLastName の定義を思い出してください。

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

FirstName または LastName のいずれか、あるいは両方が NULL でも、Singers のすべての行がインデックスに登録されます。

図は、NULL 除外インデックスから省略された行を示しています。

NULL 値がインデックスに登録されている場合、NULL 値を含むデータ全体に対して効率的な SQL クエリと読み取りを実行できます。たとえば、NULL FirstName を含むすべての Singers を検索するには、次の SQL クエリ文を使用します。

GoogleSQL

SELECT s.SingerId, s.FirstName, s.LastName
    FROM Singers@{FORCE_INDEX=SingersByFirstLastName} AS s
    WHERE s.FirstName IS NULL;

PostgreSQL

SELECT s.SingerId, s.FirstName, s.LastName
    FROM Singers /* @ FORCE_INDEX = SingersByFirstLastName */ AS s
    WHERE s.FirstName IS NULL;

NULL 値の並べ替え順

Spanner では、指定された型で NULL を最小値として並べ替えを行います。昇順(ASC)の列の場合、NULL 値が最初になります。降順(DESC)の列の場合、NULL 値が最後になります。

NULL 値のインデックス登録を無効にする

GoogleSQL

Null のインデックス登録を無効にするには、インデックスの定義に NULL_FILTERED キーワードを追加します。NULL_FILTERED インデックスは、大半の行に NULL 値が含まれるスパース列をインデックスに登録する場合に特に便利です。この場合、NULL_FILTERED インデックスは NULL を含む通常のインデックスよりもかなりサイズが小さくなり、効率的に維持できます。

NULL 値をインデックスに登録しない SingersByFirstLastName の代替定義は次のとおりです。

CREATE NULL_FILTERED INDEX SingersByFirstLastNameNoNulls
    ON Singers(FirstName, LastName);

NULL_FILTERED キーワードは、すべてのインデックス キー列に適用されます。列単位で NULL フィルタリングを指定することはできません。

PostgreSQL

1 つ以上のインデックス付き列に null 値を含む行を除外するには、WHERE COLUMN IS NOT NULL 述語を使用します。Null-filtered インデックスは、大半の行に NULL 値が含まれるスパース列をインデックスに登録する場合に特に便利です。この場合、NULL インデックスは を含む通常のインデックスよりもかなりサイズが小さくなり、効率的に維持できます。

NULL 値をインデックスに登録しない SingersByFirstLastName の代替定義は次のとおりです。

CREATE INDEX SingersByFirstLastNameNoNulls
    ON Singers(FirstName, LastName)
    WHERE FirstName IS NOT NULL
    AND LastName IS NOT NULL;

NULL 値を除外すると、Spanner では一部のクエリでその値を使用できなくなります。たとえば、Spanner では、次のクエリに対してこのインデックスは使用されません。これは、LastNameNULL であるすべての Singers 行がこのインデックスで省略されるため、このインデックスを使用すると、クエリで正しい行を返すことができなくなるためです。

GoogleSQL

FROM Singers@{FORCE_INDEX=SingersByFirstLastNameNoNulls}
    WHERE FirstName = "John";

PostgreSQL

FROM Singers /*@ FORCE_INDEX = SingersByFirstLastNameNoNulls */
    WHERE FirstName = 'John';

Spanner でインデックスを使用できるようにするには、インデックスから除外される行をクエリからも除外するようにクエリを書き直す必要があります。

GoogleSQL

SELECT FirstName, LastName
    FROM Singers@{FORCE_INDEX=SingersByFirstLastNameNoNulls}
    WHERE FirstName = 'John' AND LastName IS NOT NULL;

PostgreSQL

SELECT FirstName, LastName
    FROM Singers /*@ FORCE_INDEX = SingersByFirstLastNameNoNulls */
    WHERE FirstName = 'John' AND LastName IS NOT NULL;

proto フィールドのインデックス

インデックスに登録するフィールドがプリミティブ データ型または ENUM データ型を使用している限り、生成された列を使用して、PROTO 列に保存されているプロトコル バッファ内のフィールドにインデックスを付けます。

プロトコル メッセージ フィールドにインデックスを定義した場合、そのフィールドを proto スキーマから変更または削除することはできません。詳細については、proto フィールドのインデックスを含むスキーマの更新をご覧ください。

次の例は、SingerInfo proto メッセージ列を含む Singers テーブルの例です。PROTOnationality フィールドにインデックスを定義するには、保存された生成列を作成する必要があります。

GoogleSQL

CREATE PROTO BUNDLE (googlesql.example.SingerInfo, googlesql.example.SingerInfo.Residence);

CREATE TABLE Singers (
  SingerId INT64 NOT NULL,
  ...
  SingerInfo googlesql.example.SingerInfo,
  SingerNationality STRING(MAX) AS (SingerInfo.nationality) STORED
) PRIMARY KEY (SingerId);

googlesql.example.SingerInfo proto タイプの定義は次のとおりです。

GoogleSQL

package googlesql.example;

message SingerInfo {
optional string    nationality = 1;
repeated Residence residence   = 2;

  message Residence {
    required int64  start_year   = 1;
    optional int64  end_year     = 2;
    optional string city         = 3;
    optional string country      = 4;
  }
}

次に、プロトタイプの nationality フィールドにインデックスを定義します。

GoogleSQL

CREATE INDEX SingersByNationality ON Singers(SingerNationality);

次の SQL クエリは、前のインデックスを使用してデータを読み取ります。

GoogleSQL

SELECT s.SingerId, s.FirstName
FROM Singers AS s
WHERE s.SingerNationality = "English";

メモ:

  • インデックス ディレクティブを使用して、プロトコル バッファ列のフィールドのインデックスにアクセスします。
  • 繰り返しのプロトコル バッファ フィールドにインデックスを作成することはできません。

proto フィールドのインデックスを含むスキーマの更新

プロトコル メッセージ フィールドにインデックスを定義した場合、そのフィールドを proto スキーマから変更または削除することはできません。これは、インデックスを定義した後、スキーマが更新されるたびに型チェックが実行されるためです。Spanner は、インデックス定義で使用されるパス内のすべてのフィールドの型情報をキャプチャします。

一意のインデックス

インデックスは、UNIQUE として宣言できます。UNIQUE インデックスを使用すると、インデックスに登録するデータに制約が追加され、指定したインデックス キーでの重複が禁止されます。この制約は、トランザクションの commit 時に Spanner が適用します。同じキーに複数のインデックス エントリが存在するトランザクションは commit に失敗します。

テーブルの先頭に UNIQUE 以外のデータが存在する場合、UNIQUE インデックスを作成しようとすると失敗します。

UNIQUE NULL_FILTERED インデックスに関する注意事項

インデックスのキー部分の 1 つ以上が NULL の場合、UNIQUE NULL_FILTERED インデックスでインデックス キーの一意性は維持されません。

たとえば、次のテーブルとインデックスを作成したとします。

GoogleSQL

CREATE TABLE ExampleTable (
  Key1 INT64 NOT NULL,
  Key2 INT64,
  Key3 INT64,
  Col1 INT64,
) PRIMARY KEY (Key1, Key2, Key3);

CREATE UNIQUE NULL_FILTERED INDEX ExampleIndex ON ExampleTable (Key1, Key2, Col1);

PostgreSQL

CREATE TABLE ExampleTable (
  Key1 BIGINT NOT NULL,
  Key2 BIGINT,
  Key3 BIGINT,
  Col1 BIGINT,
  PRIMARY KEY (Key1, Key2, Key3)
);

CREATE UNIQUE INDEX ExampleIndex ON ExampleTable (Key1, Key2, Col1)
    WHERE Key1 IS NOT NULL
    AND Key2 IS NOT NULL
    AND Col1 IS NOT NULL;

ExampleTable の次の 2 行では、セカンダリ インデックス キー Key1Key2Col1 に同じ値が設定されています。

1, NULL, 1, 1
1, NULL, 2, 1

Key2NULL でインデックスが null-filtered であるため、インデックス ExampleIndex には行がありません。これらはインデックスに挿入されないため、(Key1, Key2, Col1) の一意性に違反していても、インデックスはこれらの値を拒否しません。

インデックスでタプル(Key1Key2Col1)の値の一意性を適用するには、テーブル定義で Key2NOT NULL というアノテーションを付けるか、 を使用せずにインデックスを作成する必要があります。

インデックスを削除する

スキーマからセカンダリ インデックスを削除するには、DROP INDEX 文を使用します。

SingersByFirstLastName という名前のインデックスを削除するには、次のようにします。

DROP INDEX SingersByFirstLastName;

迅速なスキャンのためのインデックス

Spanner で 1 つ以上の列から値をフェッチするために、(インデックス付きルックアップではなく)テーブル スキャンを実行する必要がある場合、その列にインデックスがあり、クエリで指定された順序になっていれば、よりすばやく結果を得られます。スキャンが必要なクエリを頻繁に実行する場合は、セカンダリ インデックスを作成することをおすすめします。これは、スキャンを効率的に実行するのに役立ちます。

特に、テーブルの主キーやその他のインデックスを逆順で頻繁に Spanner がスキャンする必要がある場合は、目的の順序を明確にするセカンダリ インデックスを使用することで、効率を高めることができます。

たとえば、次のクエリでは、Spanner が SongId の最小値を見つけるために Songs をスキャンする必要がありますが、常にすばやく結果が返されます。

SELECT SongId FROM Songs LIMIT 1;

SongId は、テーブルの主キーであり、すべての主キーと同様に昇順で保存されます。Spanner では、そのキーのインデックスをスキャンして最初の結果をすばやく取得できます。

ただし、特に Songs が大量のデータを保持している場合には、セカンダリ インデックスを使用しないと、次のクエリはすばやく結果を返しません。

SELECT SongId FROM Songs ORDER BY SongId DESC LIMIT 1;

SongId はテーブルの主キーですが、Spanner では、テーブル全体をスキャンしないと、列の最大値をフェッチすることができません。

次のインデックスを追加すると、このクエリの結果がすばやく返されるようになります。

CREATE INDEX SongIdDesc On Songs(SongId DESC);

適宜このインデックスを使用すると、Spanner は 2 番目のクエリの結果をより迅速に返します。

次のステップ