セカンダリ インデックス

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

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

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

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

  • ベーステーブルのすべてのキー列
  • インデックスに含まれるすべての列
  • インデックス定義のオプションの STORINGに指定されたすべての列

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

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

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

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

既存のテーブルに新しいインデックスが追加されると、Cloud Spanner はインデックスを自動的にバックフィルまたは入力し、インデックスに登録されているデータの最新状態を反映します。このバックフィル プロセスは Cloud Spanner で管理され、インデックスのバックフィル中に追加のリソースが使用されます。

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

  • データセットのサイズ
  • インスタンスのノードの数
  • インスタンスの負荷

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

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

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

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName)

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

CREATE INDEX SongsBySongName ON Songs(SongName)

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

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

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

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

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

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

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

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

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

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

Cloud SDK を使用すると、インデックスの作成をキャンセルできます。Cloud 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 Console または gcloud コマンドライン ツールを使用できます。

Console

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

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

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

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

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

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

gcloud

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

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

gcloud ツールにより、データベースのテーブルとインデックスを作成するためのデータ定義言語(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 ステートメントでインデックスを指定する方法と Cloud Spanner の読み取りインターフェースを使用する方法について説明します。これらのセクションの例では、MarketingBudget 列を Albums テーブルに追加し、AlbumsByAlbumTitle というインデックスを作成したと想定しています。

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);

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

SQL を使用して Cloud Spanner テーブルに対してクエリを実行する場合、Cloud Spanner では、クエリを効率化する可能性のあるインデックスが自動的に使用されます。結果として、通常、SQL クエリにインデックスを指定する必要はありません。

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

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

FROM MyTable@{FORCE_INDEX=MyTableIndex}

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

FROM MyTable@{FORCE_INDEX=_BASE_TABLE}

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

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

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

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

インデックス ディレクティブに次のいずれかの問題がある場合、Cloud 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 const& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::runtime_error(row.status().message());
    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.value() << "\n";
    } else {
      std::cout << "MarketingBudget: NULL\n";
    }
  }
  std::cout << "Read completed for [spanner_query_data_with_index]\n";
}

C#

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    var cmd = connection.CreateSelectCommand(
        "SELECT AlbumId, AlbumTitle, MarketingBudget FROM Albums@ "
        + "{FORCE_INDEX=AlbumsByAlbumTitle} "
        + $"WHERE AlbumTitle >= @startTitle "
        + $"AND AlbumTitle < @endTitle",
        new SpannerParameterCollection {
            {"startTitle", SpannerDbType.String},
            {"endTitle", SpannerDbType.String} });
    cmd.Parameters["startTitle"].Value = startTitle;
    cmd.Parameters["endTitle"].Value = endTitle;
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            var marketingBudget = reader.IsDBNull(
                reader.GetOrdinal("MarketingBudget")) ?
                "" :
                reader.GetFieldValue<string>("MarketingBudget");
            Console.WriteLine("AlbumId : "
            + reader.GetFieldValue<string>("AlbumId")
            + " AlbumTitle : "
            + reader.GetFieldValue<string>("AlbumTitle")
            + " MarketingBudget : "
            + marketingBudget);
        }
    }
}

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

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

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

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

// 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();
}

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($instanceId, $databaseId, $startTitle, $endTitle)
{
    $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(u"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

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

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

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

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

Cloud 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;

  spanner::ReadOptions read_options;
  read_options.index_name = "AlbumsByAlbumTitle";
  auto rows = client.Read("Albums", google::cloud::spanner::KeySet::All(),
                          {"AlbumId", "AlbumTitle"}, read_options);
  using RowType = std::tuple<std::int64_t, std::string>;
  for (auto const& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::runtime_error(row.status().message());
    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

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

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

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

// 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();
}

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($instanceId, $databaseId)
{
    $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 句

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

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

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

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

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

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

C++

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

  spanner::ReadOptions read_options;
  read_options.index_name = "AlbumsByAlbumTitle2";
  auto rows =
      client.Read("Albums", google::cloud::spanner::KeySet::All(),
                  {"AlbumId", "AlbumTitle", "MarketingBudget"}, read_options);
  using RowType =
      std::tuple<std::int64_t, std::string, absl::optional<std::int64_t>>;
  for (auto const& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::runtime_error(row.status().message());
    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.value() << "\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

// "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

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

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

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

// 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();
}

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($instanceId, $databaseId)
{
    $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_soring_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(u"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

NULL 値のインデックス登録

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

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

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

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

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

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

NULL 値の並べ替え順

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

NULL 値のインデックス登録の無効化

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

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

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

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

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

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

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

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

一意のインデックス

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

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

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

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

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

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);

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

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

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

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

インデックスを削除する

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

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

DROP INDEX SingersByFirstLastName;

次のステップ