보조 색인

Cloud Spanner 데이터베이스에서 Cloud Spanner는 각 테이블의 기본 키 열에 대한 색인을 자동으로 생성합니다. 예를 들어 Singers의 기본 키 열은 자동으로 색인이 생성되므로 색인 생성을 위한 별도의 작업이 필요하지 않습니다.

다른 열에 대해 보조 색인을 만들 수도 있습니다. 열에 보조 색인을 추가하면 해당 열의 데이터를 더 효율적으로 조회할 수 있습니다. 예를 들어 특정 범위의 LastName 값에 대해 SingerId 값 집합을 빠르게 조회해야 하는 경우 LastName에 보조 색인을 만들면 Cloud Spanner가 전체 테이블을 스캔하지 않아도 됩니다.

위 예시에서 조회가 읽기-쓰기 트랜잭션 내에서 수행되면 보다 효율적인 조회를 통해 전체 테이블 잠금을 보유하지 않고 LastName 조회 범위 외부에 있는 행의 테이블에 삽입과 업데이트를 동시에 실행할 수 있습니다.

Cloud Spanner는 각 보조 색인에 다음 데이터를 저장합니다.

  • 기본 테이블의 모든 키 열
  • 색인에 포함된 모든 열
  • 색인 정의의 선택적 STORING에 지정된 모든 열

시간이 경과하면 Cloud Spanner는 테이블을 분석하여 보조 색인이 적절한 쿼리에 사용되는지 확인합니다.

보조 색인 추가

테이블을 만들 때 보조 색인을 추가하는 것이 가장 효율적입니다. 테이블과 테이블 색인을 동시에 만들려면 새 테이블과 새 색인에 대한 DDL 문을 Cloud Spanner에 단일 요청으로 전송합니다.

Cloud Spanner에서는 데이터베이스에서 트래픽이 계속 처리되는 동안 기존 테이블에 새로운 보조 색인을 추가할 수도 있습니다. Cloud Spanner에서 다른 스키마 변경 작업을 수행할 때와 마찬가지로 기존 데이터베이스에 색인을 추가할 때도 데이터베이스를 오프라인으로 전환할 필요가 없으며 전체 열 또는 테이블이 잠기지도 않습니다.

기존 테이블에 새 색인이 추가될 때마다 Cloud Spanner가 자동으로 색인 백필 또는 채우기를 수행하여 색인이 생성되는 데이터의 최신 뷰를 반영합니다. Cloud Spanner는 이 백필 프로세스를 자동으로 관리하며, 색인 백필 시 추가 리소스를 사용합니다.

색인을 만드는 데는 몇 분에서 여러 시간이 걸릴 수 있습니다. 색인 생성은 스키마 업데이트에 해당하므로 다른 스키마 업데이트와 동일한 성능 제약조건이 적용됩니다. 보조 색인을 만드는 데 필요한 시간은 다음과 같은 몇 가지 요인에 따라 달라집니다.

  • 데이터 세트의 크기
  • 인스턴스의 노드 수
  • 인스턴스의 부하

커밋 타임스탬프 열을 보조 색인의 첫 번째 부분으로 사용하면 핫스팟을 만들고 쓰기 성능을 줄일 수 있습니다.

스키마에 보조 색인을 정의하려면 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에만 적용됩니다. 다른 색인 키에 대해 내림차순으로 색인을 생성하려면 해당 키에도 DESC 주석을 사용합니다(예: SingerId 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용 읽기 인터페이스로 색인을 지정하는 방법을 설명합니다. 이 섹션의 예시에서는 Albums 테이블에 MarketingBudget 열을 추가하고 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 색인을 사용하여 AlbumId, AlbumTitle, MarketingBudget 값을 가져오는 쿼리를 작성하고 실행하는 방법을 보여줍니다.

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
}

자바

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

    """
    from google.cloud.spanner_v1.proto import type_pb2

    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': type_pb2.Type(code=type_pb2.STRING),
        'end_title': type_pb2.Type(code=type_pb2.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 주석은 단일 색인 키에 적용됩니다. 색인에 키가 2개 이상 포함된 경우 모든 키를 기준으로 쿼리 결과를 내림차순으로 표시하려면 각 키에 대해 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#

C#에서는 읽기 인터페이스를 사용할 수 없습니다.

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

자바

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#

C#에서는 읽기 인터페이스를 사용할 수 없습니다.

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

자바

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

Singers의 모든 행은 FirstName 또는 LastName 중 하나, 또는 둘 모두가 NULL인 경우에도 색인이 생성됩니다.

NULL 필터링 색인에서 누락된 행을 보여주는 다이어그램

NULL 값의 색인을 생성하면 NULL 값을 포함하는 데이터에 대해 효율적인 SQL 쿼리 및 읽기를 수행할 수 있습니다. 예를 들어 FirstNameNULL인 모든 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가 일부 쿼리에서 이를 사용할 수 없게 됩니다. 예를 들어 이 색인은 LastNameNULL인 모든 Singers 행을 생략하므로 Cloud Spanner는 이 쿼리에 이 색인을 사용하지 않습니다. 따라서 이 색인을 사용하면 쿼리에서 올바른 행이 반환되지 않습니다.

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 색인은 색인이 생성되는 데이터에 지정된 색인 키의 중복 항목을 금지하는 제약조건을 추가합니다. 이 제약조건은 Cloud Spanner에서 트랜잭션 커밋 시 적용됩니다. 특히 같은 키에 여러 색인 항목을 만드는 트랜잭션은 커밋되지 않습니다.

테이블에서 처음에 UNIQUE가 아닌 데이터가 있는 경우 테이블에 UNIQUE 색인을 만들려고 하면 실패합니다.

UNIQUE NULL_FILTERED 색인에 대한 참고사항

UNIQUE NULL_FILTERED 색인은 색인의 키 부분 중 적어도 하나가 NULL이면 색인 키 고유성을 적용하지 않습니다.

예를 들어 다음과 같은 테이블과 색인을 만들었다고 가정해보겠습니다.

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의 다음 두 행은 보조 색인 키 Key1, Key2, Col1에 같은 값이 있습니다.

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

Key2NULL이고 색인이 NULL_FILTERED이므로 ExampleIndex 색인에는 해당 행이 표시되지 않습니다. 해당 행이 색인에 삽입되지 않으므로 이 색인은 (Key1, Key2, Col1)의 고유성 위반에 대해 행을 거부하지 않습니다.

색인이 튜플(Key1, Key2, Col1)의 값 고유성을 적용하도록 하려면 테이블 정의에서 Key2NOT NULL 주석을 추가하거나 NULL_FILTERED 없이 색인을 만들어야 합니다.

색인 삭제

스키마에서 보조 색인을 삭제하려면 DROP INDEX 문을 사용합니다.

SingersByFirstLastName이라는 색인을 삭제하려면 다음을 실행합니다.

DROP INDEX SingersByFirstLastName;

다음 단계