커밋 타임스탬프

여기서는 Cloud Spanner로 수행하는 모든 삽입 및 업데이트 작업 시 커밋 타임스탬프를 기록하는 방법에 대해 설명합니다. 이 기능을 사용하려면 TIMESTAMP 열에서 allow_commit_timestamp 옵션을 설정한 다음, 각 트랜잭션 과정에서 타임스탬프를 씁니다.

개요

TrueTime 기술에 기반을 둔 커밋 타임스탬프는 데이터베이스에서 트랜잭션이 커밋되는 시간입니다. allow_commit_timestamp 열 옵션을 사용하여 커밋 타임스탬프를 열에 원자적으로 저장할 수 있습니다. 테이블에 저장된 커밋 타임스탬프를 사용하여 변형의 정확한 순서를 결정하고 변경 로그와 같은 기능을 빌드할 수 있습니다.

데이터베이스에 커밋 타임스탬프를 삽입하려면 다음 단계를 완료하세요.

  1. 스키마 정의에서 열 옵션 allow_commit_timestamptrue로 설정하여 TIMESTAMP 유형의 열 만들기를 수행합니다. 예를 들면 다음과 같습니다.

    CREATE TABLE Performances (
        ...
        LastUpdateTime  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
        ...
    ) PRIMARY KEY (...);
    
  2. DML을 사용하여 삽입 또는 업데이트를 수행하는 경우 PENDING_COMMIT_TIMESTAMP 함수를 사용하여 커밋 타임스탬프를 씁니다.

    변형이 포함된 삽입 또는 업데이트를 수행하는 경우 커밋 타임스탬프 열에 삽입 또는 업데이트 시 spanner.commit_timestamp() 자리표시자 문자열(또는 클라이언트 라이브러리 상수)을 사용합니다.

Cloud Spanner가 이러한 자리표시자를 열 값으로 사용해서 트랜잭션을 커밋하면 실제 커밋 타임스탬프가 지정된 열에 기록됩니다(예: LastUpdateTime 열). 그런 다음 이 열 값을 사용해서 업데이트 기록을 테이블에 만들 수 있습니다.

커밋 타임스탬프 값은 고유한 값이 아닐 수도 있습니다. 중복되지 않는 필드 세트에 기록하는 트랜잭션은 동일한 타임스탬프를 가질 수 있습니다. 중복된 필드 세트에 기록하는 트랜잭션은 고유한 타임스탬프를 가집니다.

Cloud Spanner 커밋 타임스탬프는 마이크로초 단위로 기록되며 TIMESTAMP 열에 저장될 때는 나노초로 변환됩니다.

커밋 타임스탬프 열 생성 및 삭제

allow_commit_timestamp 열 옵션을 사용하여 커밋 타임스탬프에 대한 지원을 추가하고 삭제할 수 있습니다.

  • 새 테이블 만들기를 수행할 때 열이 커밋 타임스탬프를 지원하도록 지정합니다.
  • 기존 테이블을 수정하는 경우에는 다음을 수행합니다.
    • 커밋 타임스탬프를 지원하는 새 열을 추가합니다.
    • 커밋 타임스탬프를 지원하도록 기존 TIMESTAMP 열을 수정합니다.
    • 커밋 타임스탬프 지원을 삭제하도록 기존 TIMESTAMP 열을 수정합니다.

키와 색인

커밋 타임스탬프 열을 기본 키 열이나 키 열이 아닌 열로 사용할 수 있습니다. 기본 키는 ASC 또는 DESC로 정의될 수 있습니다.

  • ASC(기본값) - 오름차순 키는 특정 시간부터 쿼리에 답변하는 데 적합합니다.
  • DESC - 내림차순 키는 최신 행을 테이블의 상단에 유지합니다. 따라서 최신 레코드에 빨리 액세스할 수 있습니다.

allow_commit_timestamp 옵션은 상위 테이블과 하위 테이블의 모든 기본 키에서 일관되어야 합니다. 이 옵션이 모든 기본 키에서 일관되지 않으면 Cloud Spanner가 오류를 반환합니다. 이 옵션이 일관되지 않아도 되는 유일한 시간은 스키마를 생성하거나 업데이트할 때입니다.

다음 시나리오에서 커밋 타임스탬프를 사용하면 데이터 성능을 저하시키는 핫스팟이 생성됩니다.

  • 테이블 기본 키의 첫 번째 부분으로 타임스탬프 열을 커밋합니다.

    CREATE TABLE Users (
      LastAccess TIMESTAMP NOT NULL,
      UserId     INT64 NOT NULL,
      ...
    ) PRIMARY KEY (LastAccess, UserId);
    
  • 보조 색인 기본 키의 첫 번째 부분은 다음과 같습니다.

    CREATE INDEX UsersByLastAccess ON Users(LastAccess)
    

    또는

    CREATE INDEX UsersByLastAccessAndName ON Users(LastAccess, FirstName)
    

핫스팟은 낮은 쓰기 속도에서도 데이터 성능을 저하시킵니다. 색인이 생성되지 않은 키 열이 아닌 열에 커밋 타임스탬프를 사용 설정해도 성능 오버헤드가 발생하지 않습니다.

커밋 타임스탬프 열 만들기

다음은 커밋 타임스탬프를 지원하는 열로 테이블을 만드는 DDL입니다.

CREATE TABLE Performances (
    SingerId        INT64 NOT NULL,
    VenueId         INT64 NOT NULL,
    EventDate       Date,
    Revenue         INT64,
    LastUpdateTime  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (SingerId, VenueId, EventDate),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE)

옵션을 추가하면 타임스탬프 열이 다음과 같이 바뀝니다.

  • 삽입 및 업데이트 시 spanner.commit_timestamp() 자리표시자 문자열(또는 클라이언트 라이브러리에서 제공하는 상수)을 사용할 수 있습니다.
  • 열은 과거의 값만 포함할 수 있습니다. 자세한 내용은 타임스탬프에 고유한 값 지정을 참조하세요.

allow_commit_timestamp 옵션은 대소문자를 구분합니다.

기존 테이블에 커밋 타임스탬프 열 추가하기

기존 테이블에 커밋 타임스탬프 열을 추가하려면 ALTER TABLE 문을 사용합니다. 예를 들어 LastUpdateTime 열을 Performances 테이블에 추가하려면 다음 문을 사용합니다.

ALTER TABLE Performances ADD COLUMN LastUpdateTime TIMESTAMP
    NOT NULL OPTIONS (allow_commit_timestamp=true)

타임스탬프 열을 커밋 타임스탬프 열로 변환하기

기존 타임스탬프 열을 커밋 타임스탬프 열로 변환할 수 있으나 이렇게 하려면 Cloud Spanner가 기존 타임스탬프 값이 과거의 값임을 확인해야 합니다. 예를 들면 다음과 같습니다.

ALTER TABLE Performances ALTER COLUMN LastUpdateTime
    SET OPTIONS (allow_commit_timestamp=true)

SET OPTIONS를 포함하는 ALTER TABLE 문에서 특정 열의 데이터 형식이나 NULL 주석을 변경할 수 없습니다. 자세한 내용은 데이터 정의 언어를 참조하세요.

커밋 타임스탬프 옵션 제거하기

열에서 커밋 타임스탬프 지원을 제거하려면 ALTER TABLE 문에서 allow_commit_timestamp=null 옵션을 사용합니다. 커밋 타임스탬프 동작은 제거되지만 해당 열은 여전히 타임스탬프입니다. 이 옵션을 변경해도 해당 열의 다른 특성(예: 유형 또는 null 가능성(NOT NULL))은 바뀌지 않습니다.

ALTER TABLE Performances ALTER COLUMN LastUpdateTime
    SET OPTIONS (allow_commit_timestamp=null)

DML 문을 사용하여 커밋 타임스탬프 쓰기

PENDING_COMMIT_TIMESTAMP 함수를 사용하여 DML 문에서 커밋 타임스탬프를 씁니다. Cloud Spanner는 트랜잭션이 커밋될 때 커밋 타임스탬프를 선택합니다.

다음 DML 문은 Singers 테이블의 LastUpdated 열을 커밋 타임스탬프로 업데이트합니다.

UPDATE Performances SET LastUpdated = PENDING_COMMIT_TIMESTAMP()
   WHERE SingerId=1 AND VenueId=2 AND EventDate="2015-10-21"

변형을 사용하여 행 삽입

행을 삽입할 경우, Cloud Spanner는 사용자가 열 목록에 해당 열을 포함시키고 spanner.commit_timestamp() 자리표시자 문자열(또는 클라이언트 라이브러리 상수)을 해당 값으로 전달할 경우에만 커밋 타임스탬프 값을 기록합니다. 예를 들면 다음과 같습니다.

C++

void InsertDataWithTimestamp(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto commit_result = client.Commit(spanner::Mutations{
      spanner::InsertOrUpdateMutationBuilder(
          "Performances",
          {"SingerId", "VenueId", "EventDate", "Revenue", "LastUpdateTime"})
          .EmplaceRow(1, 4, absl::CivilDay(2017, 10, 5), 11000,
                      spanner::CommitTimestamp{})
          .EmplaceRow(1, 19, absl::CivilDay(2017, 11, 2), 15000,
                      spanner::CommitTimestamp{})
          .EmplaceRow(2, 42, absl::CivilDay(2017, 12, 23), 7000,
                      spanner::CommitTimestamp{})
          .Build()});
  if (!commit_result) {
    throw std::runtime_error(commit_result.status().message());
  }
  std::cout
      << "Update was successful [spanner_insert_data_with_timestamp_column]\n";
}

C#

string connectionString =
$"Data Source=projects/{projectId}/instances/{instanceId}"
+ $"/databases/{databaseId}";
List<Performance> performances = new List<Performance> {
    new Performance {SingerId = 1, VenueId = 4, EventDate = DateTime.Parse("2017-10-05"),
        Revenue = 11000},
    new Performance {SingerId = 1, VenueId = 19, EventDate = DateTime.Parse("2017-11-02"),
        Revenue = 15000},
    new Performance {SingerId = 2, VenueId = 42, EventDate = DateTime.Parse("2017-12-23"),
        Revenue = 7000},
};
// Create connection to Cloud Spanner.
using (var connection = new SpannerConnection(connectionString))
{
    await connection.OpenAsync();
    // Insert rows into the Performances table.
    var cmd = connection.CreateInsertCommand("Performances",
        new SpannerParameterCollection {
            {"SingerId", SpannerDbType.Int64},
            {"VenueId", SpannerDbType.Int64},
            {"EventDate", SpannerDbType.Date},
            {"Revenue", SpannerDbType.Int64},
            {"LastUpdateTime", SpannerDbType.Timestamp},
    });
    await Task.WhenAll(performances.Select(performance =>
    {
        cmd.Parameters["SingerId"].Value = performance.SingerId;
        cmd.Parameters["VenueId"].Value = performance.VenueId;
        cmd.Parameters["EventDate"].Value = performance.EventDate;
        cmd.Parameters["Revenue"].Value = performance.Revenue;
        cmd.Parameters["LastUpdateTime"].Value = SpannerParameter.CommitTimestamp;
        return cmd.ExecuteNonQueryAsync();
    }));
    Console.WriteLine("Inserted data.");
}

Go


import (
	"context"

	"cloud.google.com/go/spanner"
)

func writeWithTimestamp(db string) error {
	ctx := context.Background()

	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	performanceColumns := []string{"SingerId", "VenueId", "EventDate", "Revenue", "LastUpdateTime"}
	m := []*spanner.Mutation{
		spanner.InsertOrUpdate("Performances", performanceColumns, []interface{}{1, 4, "2017-10-05", 11000, spanner.CommitTimestamp}),
		spanner.InsertOrUpdate("Performances", performanceColumns, []interface{}{1, 19, "2017-11-02", 15000, spanner.CommitTimestamp}),
		spanner.InsertOrUpdate("Performances", performanceColumns, []interface{}{2, 42, "2017-12-23", 7000, spanner.CommitTimestamp}),
	}
	_, err = client.Apply(ctx, m)
	return err
}

자바

static final List<Performance> PERFORMANCES =
    Arrays.asList(
        new Performance(1, 4, "2017-10-05", 11000),
        new Performance(1, 19, "2017-11-02", 15000),
        new Performance(2, 42, "2017-12-23", 7000));
static void writeExampleDataWithTimestamp(DatabaseClient dbClient) {
  List<Mutation> mutations = new ArrayList<>();
  for (Performance performance : PERFORMANCES) {
    mutations.add(
        Mutation.newInsertBuilder("Performances")
            .set("SingerId")
            .to(performance.singerId)
            .set("VenueId")
            .to(performance.venueId)
            .set("EventDate")
            .to(performance.eventDate)
            .set("Revenue")
            .to(performance.revenue)
            .set("LastUpdateTime")
            .to(Value.COMMIT_TIMESTAMP)
            .build());
  }
  dbClient.write(mutations);
}

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

// Instantiate Spanner table objects
const performancesTable = database.table('Performances');

const data = [
  {
    SingerId: '1',
    VenueId: '4',
    EventDate: '2017-10-05',
    Revenue: '11000',
    LastUpdateTime: 'spanner.commit_timestamp()',
  },
  {
    SingerId: '1',
    VenueId: '19',
    EventDate: '2017-11-02',
    Revenue: '15000',
    LastUpdateTime: 'spanner.commit_timestamp()',
  },
  {
    SingerId: '2',
    VenueId: '42',
    EventDate: '2017-12-23',
    Revenue: '7000',
    LastUpdateTime: 'spanner.commit_timestamp()',
  },
];

// Inserts rows into the Singers table
// Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so
// they must be converted to strings before being inserted as INT64s
try {
  await performancesTable.insert(data);
  console.log('Inserted data.');
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Inserts sample data into a table with a commit timestamp column.
 *
 * The database and table must already exist and can be created using
 * `create_table_with_timestamp_column`.
 * Example:
 * ```
 * insert_data_with_timestamp_column($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function insert_data_with_timestamp_column($instanceId, $databaseId)
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $operation = $database->transaction(['singleUse' => true])
        ->insertBatch('Performances', [
            ['SingerId' => 1, 'VenueId' => 4, 'EventDate' => '2017-10-05', 'Revenue' => 11000, 'LastUpdateTime' => $spanner->commitTimestamp()],
            ['SingerId' => 1, 'VenueId' => 19, 'EventDate' => '2017-11-02', 'Revenue' => 15000, 'LastUpdateTime' => $spanner->commitTimestamp()],
            ['SingerId' => 2, 'VenueId' => 42, 'EventDate' => '2017-12-23', 'Revenue' => 7000, 'LastUpdateTime' => $spanner->commitTimestamp()],
        ])
        ->commit();

    print('Inserted data.' . PHP_EOL);
}

Python

def insert_data_with_timestamp(instance_id, database_id):
    """Inserts data with a COMMIT_TIMESTAMP field into a table. """

    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)

    database = instance.database(database_id)

    with database.batch() as batch:
        batch.insert(
            table='Performances',
            columns=(
                'SingerId', 'VenueId', 'EventDate',
                'Revenue', 'LastUpdateTime',),
            values=[
                (1, 4, "2017-10-05", 11000, spanner.COMMIT_TIMESTAMP),
                (1, 19, "2017-11-02", 15000, spanner.COMMIT_TIMESTAMP),
                (2, 42, "2017-12-23", 7000, spanner.COMMIT_TIMESTAMP)])

    print('Inserted data.')

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

# Get commit_timestamp
commit_timestamp = client.commit_timestamp

client.commit do |c|
  c.insert "Performances", [
    { SingerId: 1, VenueId: 4, EventDate: "2017-10-05", Revenue: 11_000, LastUpdateTime: commit_timestamp },
    { SingerId: 1, VenueId: 19, EventDate: "2017-11-02", Revenue: 15_000, LastUpdateTime: commit_timestamp },
    { SingerId: 2, VenueId: 42, EventDate: "2017-12-23", Revenue: 7000, LastUpdateTime: commit_timestamp }
  ]
end

puts "Inserted data"

커밋 타임스탬프는 allow_commit_timestamp=true 옵션 주석이 달린 열에만 기록될 수 있습니다.

여러 테이블의 행에 변형이 있는 경우 각 테이블의 커밋 타임스탬프 열에 spanner.commit_timestamp()(또는 클라이언트 라이브러리 상수)를 지정해야 합니다.

변형을 사용하여 행 업데이트

행을 업데이트할 경우, Cloud Spanner는 사용자가 열 목록에 해당 열을 포함시키고 spanner.commit_timestamp() 자리표시자 문자열(또는 클라이언트 라이브러리 상수)을 해당 값으로 전달할 경우에만 커밋 타임스탬프 값을 기록합니다. 행의 기본 키는 업데이트할 수 없습니다. 기본 키를 업데이트하려면 기존 행을 삭제하고 새 행을 만듭니다.

예를 들어 이름이 LastUpdateTime인 커밋 타임스탬프 열을 업데이트하려면 다음을 수행합니다.

C++

void UpdateDataWithTimestamp(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto commit_result = client.Commit(spanner::Mutations{
      spanner::UpdateMutationBuilder(
          "Albums",
          {"SingerId", "AlbumId", "MarketingBudget", "LastUpdateTime"})
          .EmplaceRow(1, 1, 1000000, spanner::CommitTimestamp{})
          .EmplaceRow(2, 2, 750000, spanner::CommitTimestamp{})
          .Build()});
  if (!commit_result) {
    throw std::runtime_error(commit_result.status().message());
  }
  std::cout
      << "Update was successful [spanner_update_data_with_timestamp_column]\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.CreateUpdateCommand("Albums",
        new SpannerParameterCollection {
            {"SingerId", SpannerDbType.Int64},
            {"AlbumId", SpannerDbType.Int64},
            {"MarketingBudget", SpannerDbType.Int64},
            {"LastUpdateTime", SpannerDbType.Timestamp},
        });
    var cmdLookup =
        connection.CreateSelectCommand("SELECT * FROM Albums");
    using (var reader = await cmdLookup.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            if (reader.GetFieldValue<int>("SingerId") == 1
                && reader.GetFieldValue<int>("AlbumId") == 1)
            {
                cmd.Parameters["SingerId"].Value =
                    reader.GetFieldValue<int>("SingerId");
                cmd.Parameters["AlbumId"].Value =
                    reader.GetFieldValue<int>("AlbumId");
                cmd.Parameters["MarketingBudget"].Value = 1000000;
                cmd.Parameters["LastUpdateTime"].Value =
                    SpannerParameter.CommitTimestamp;
                await cmd.ExecuteNonQueryAsync();
            }
            if (reader.GetInt64(0) == 2 && reader.GetInt64(1) == 2)
            {
                cmd.Parameters["SingerId"].Value =
                    reader.GetFieldValue<int>("SingerId");
                cmd.Parameters["AlbumId"].Value =
                    reader.GetFieldValue<int>("AlbumId");
                cmd.Parameters["MarketingBudget"].Value = 750000;
                cmd.Parameters["LastUpdateTime"].Value =
                    SpannerParameter.CommitTimestamp;
                await cmd.ExecuteNonQueryAsync();
            }
        }
    }
}
Console.WriteLine("Updated data.");

Go


import (
	"context"
	"io"

	"cloud.google.com/go/spanner"
)

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

	cols := []string{"SingerId", "AlbumId", "MarketingBudget", "LastUpdateTime"}
	_, err = client.Apply(ctx, []*spanner.Mutation{
		spanner.Update("Albums", cols, []interface{}{1, 1, 1000000, spanner.CommitTimestamp}),
		spanner.Update("Albums", cols, []interface{}{2, 2, 750000, spanner.CommitTimestamp}),
	})
	return err
}

자바

static void updateWithTimestamp(DatabaseClient dbClient) {
  // Mutation can be used to update/insert/delete a single row in a table. Here we use
  // newUpdateBuilder to create update mutations.
  List<Mutation> mutations =
      Arrays.asList(
          Mutation.newUpdateBuilder("Albums")
              .set("SingerId")
              .to(1)
              .set("AlbumId")
              .to(1)
              .set("MarketingBudget")
              .to(1000000)
              .set("LastUpdateTime")
              .to(Value.COMMIT_TIMESTAMP)
              .build(),
          Mutation.newUpdateBuilder("Albums")
              .set("SingerId")
              .to(2)
              .set("AlbumId")
              .to(2)
              .set("MarketingBudget")
              .to(750000)
              .set("LastUpdateTime")
              .to(Value.COMMIT_TIMESTAMP)
              .build());
  // This writes all the mutations to Cloud Spanner atomically.
  dbClient.write(mutations);
}

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

// Update a row in the Albums table
// Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so they
// must be converted to strings before being inserted as INT64s
const albumsTable = database.table('Albums');

const data = [
  {
    SingerId: '1',
    AlbumId: '1',
    MarketingBudget: '1000000',
    LastUpdateTime: 'spanner.commit_timestamp()',
  },
  {
    SingerId: '2',
    AlbumId: '2',
    MarketingBudget: '750000',
    LastUpdateTime: 'spanner.commit_timestamp()',
  },
];

try {
  await albumsTable.update(data);
  console.log('Updated data.');
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Updates sample data in a table with a commit timestamp column.
 *
 * Before executing this method, a new column MarketingBudget has to be added to the Albums
 * table by applying the DDL statement "ALTER TABLE Albums ADD COLUMN MarketingBudget INT64".
 *
 * In addition this update expects the LastUpdateTime column added by applying the DDL statement
 * "ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP OPTIONS (allow_commit_timestamp=true)"
 *
 * Example:
 * ```
 * update_data_with_timestamp_column($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function update_data_with_timestamp_column($instanceId, $databaseId)
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $operation = $database->transaction(['singleUse' => true])
        ->updateBatch('Albums', [
            ['SingerId' => 1, 'AlbumId' => 1, 'MarketingBudget' => 1000000, 'LastUpdateTime' => $spanner->commitTimestamp()],
            ['SingerId' => 2, 'AlbumId' => 2, 'MarketingBudget' => 750000, 'LastUpdateTime' => $spanner->commitTimestamp()],
        ])
        ->commit();

    print('Updated data.' . PHP_EOL);
}

Python

def update_data_with_timestamp(instance_id, database_id):
    """Updates Performances tables in the database with the COMMIT_TIMESTAMP
    column.

    This updates the `MarketingBudget` column which must be created before
    running this sample. 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

    In addition this update expects the LastUpdateTime column added by
    applying this DDL statement against your database:

        ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP
        OPTIONS(allow_commit_timestamp=true)
    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)

    database = instance.database(database_id)

    with database.batch() as batch:
        batch.update(
            table='Albums',
            columns=(
                'SingerId', 'AlbumId', 'MarketingBudget', 'LastUpdateTime'),
            values=[
                (1, 1, 1000000, spanner.COMMIT_TIMESTAMP),
                (2, 2, 750000, spanner.COMMIT_TIMESTAMP)])

    print('Updated data.')

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

commit_timestamp = client.commit_timestamp

client.commit do |c|
  c.update "Albums", [
    { SingerId: 1, AlbumId: 1, MarketingBudget: 100_000, LastUpdateTime: commit_timestamp },
    { SingerId: 2, AlbumId: 2, MarketingBudget: 750_000, LastUpdateTime: commit_timestamp }
  ]
end

puts "Updated data"

커밋 타임스탬프는 allow_commit_timestamp=true 옵션 주석이 달린 열에만 기록될 수 있습니다.

여러 테이블의 행에 변형이 있는 경우 각 테이블의 커밋 타임스탬프 열에 spanner.commit_timestamp()(또는 클라이언트 라이브러리 상수)를 지정해야 합니다.

커밋 타임스탬프 열 쿼리

다음의 예는 테이블의 커밋 타임스탬프 열을 쿼리합니다.

C++

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

  spanner::SqlStatement select(
      "SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime"
      "  FROM Albums"
      " ORDER BY LastUpdateTime DESC");
  using RowType =
      std::tuple<std::int64_t, std::int64_t, absl::optional<std::int64_t>,
                 absl::optional<spanner::Timestamp>>;

  auto rows = client.ExecuteQuery(select);
  for (auto const& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::runtime_error(row.status().message());
    std::cout << std::get<0>(*row) << " " << std::get<1>(*row);
    auto marketing_budget = std::get<2>(*row);
    if (!marketing_budget) {
      std::cout << " NULL";
    } else {
      std::cout << ' ' << *marketing_budget;
    }
    auto last_update_time = std::get<3>(*row);
    if (!last_update_time) {
      std::cout << " NULL";
    } else {
      std::cout << ' ' << *last_update_time;
    }
    std::cout << "\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 SingerId, AlbumId, "
            + "MarketingBudget, LastUpdateTime FROM Albums");
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            string budget = string.Empty;
            if (reader["MarketingBudget"] != DBNull.Value)
            {
                budget = reader.GetFieldValue<string>("MarketingBudget");
            }
            string timestamp = string.Empty;
            if (reader["LastUpdateTime"] != DBNull.Value)
            {
                timestamp = reader.GetFieldValue<string>("LastUpdateTime");
            }
            Console.WriteLine("SingerId : "
            + reader.GetFieldValue<string>("SingerId")
            + " AlbumId : "
            + reader.GetFieldValue<string>("AlbumId")
            + $" MarketingBudget : {budget}"
            + $" LastUpdateTime : {timestamp}");
        }
    }
}

Go


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

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

func queryWithTimestamp(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 SingerId, AlbumId, MarketingBudget, LastUpdateTime
				FROM Albums ORDER BY LastUpdateTime DESC`}
	iter := client.Single().Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var singerID, albumID int64
		var marketingBudget spanner.NullInt64
		var lastUpdateTime spanner.NullTime
		if err := row.ColumnByName("SingerId", &singerID); err != nil {
			return err
		}
		if err := row.ColumnByName("AlbumId", &albumID); 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)
		}
		if err := row.ColumnByName("LastUpdateTime", &lastUpdateTime); err != nil {
			return err
		}
		timestamp := "NULL"
		if lastUpdateTime.Valid {
			timestamp = lastUpdateTime.String()
		}
		fmt.Fprintf(w, "%d %d %s %s\n", singerID, albumID, budget, timestamp)
	}
}

자바

static void queryMarketingBudgetWithTimestamp(DatabaseClient dbClient) {
  // Rows without an explicit value for MarketingBudget will have a MarketingBudget equal to
  // null. A try-with-resource block is used to automatically release resources held by
  // ResultSet.
  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .executeQuery(
              Statement.of(
                  "SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime FROM Albums"
                      + " ORDER BY LastUpdateTime DESC"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s %s\n",
          resultSet.getLong("SingerId"),
          resultSet.getLong("AlbumId"),
          // We check that the value is non null. ResultSet getters can only be used to retrieve
          // non null values.
          resultSet.isNull("MarketingBudget") ? "NULL" : resultSet.getLong("MarketingBudget"),
          resultSet.isNull("LastUpdateTime") ? "NULL" : resultSet.getTimestamp("LastUpdateTime"));
    }
  }
}

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 query = {
  sql: `SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime
          FROM Albums ORDER BY LastUpdateTime DESC`,
};

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

  rows.forEach(row => {
    const json = row.toJSON();

    console.log(
      `SingerId: ${json.SingerId}, AlbumId: ${
        json.AlbumId
      }, MarketingBudget: ${
        json.MarketingBudget ? json.MarketingBudget : null
      }, LastUpdateTime: ${json.LastUpdateTime}`
    );
  });
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Queries sample data from a database with a commit timestamp column.
 *
 * This sample 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
 *
 * This sample also uses the 'LastUpdateTime' commit timestamp column. You can
 * add the column by running the `add_timestamp_column` sample or by running
 * this DDL statement against your database:
 *
 * 		ALTER TABLE Albums ADD COLUMN LastUpdateTime TIMESTAMP OPTIONS (allow_commit_timestamp=true)
 *
 * Example:
 * ```
 * query_data_with_timestamp_column($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function query_data_with_timestamp_column($instanceId, $databaseId)
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $results = $database->execute(
        'SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime ' .
        ' FROM Albums ORDER BY LastUpdateTime DESC'
    );

    foreach ($results as $row) {
        if ($row['MarketingBudget'] == null) {
            $row['MarketingBudget'] = 'NULL';
        }
        if ($row['LastUpdateTime'] == null) {
            $row['LastUpdateTime'] = 'NULL';
        }
        printf('SingerId: %s, AlbumId: %s, MarketingBudget: %s, LastUpdateTime: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['MarketingBudget'], $row['LastUpdateTime']);
    }
}

Python

def query_data_with_timestamp(instance_id, database_id):
    """Queries sample data from the database using SQL.

    This updates the `LastUpdateTime` column which must be created before
    running this sample. You can add the column by running the
    `add_timestamp_column` sample or by running this DDL statement
    against your database:

        ALTER TABLE Performances ADD COLUMN LastUpdateTime TIMESTAMP
        OPTIONS (allow_commit_timestamp=true)

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

    database = instance.database(database_id)

    with database.snapshot() as snapshot:
        results = snapshot.execute_sql(
            'SELECT SingerId, AlbumId, MarketingBudget FROM Albums '
            'ORDER BY LastUpdateTime DESC')

    for row in results:
        print(u'SingerId: {}, AlbumId: {}, 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

client.execute("SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime
                FROM Albums ORDER BY LastUpdateTime DESC").rows.each do |row|
  puts "#{row[:SingerId]} #{row[:AlbumId]} #{row[:MarketingBudget]} #{row[:LastUpdateTime]}"
end

커밋 타임스탬프 열에 고유한 값 지정

spanner.commit_timestamp()(또는 클라이언트 라이브러리 상수)를 열 값으로 전달하는 대신에 커밋 타임스탬프 열에 고유한 값을 지정할 수 있습니다. 이 값은 과거의 타임스탬프여야 합니다. 이 같은 제한은 타임스탬프 기록 작업을 저렴하고 신속한 작업으로 만들어 줍니다. 과거의 값인지 확인하는 쉬운 방법은 CURRENT_TIMESTAMP SQL 함수가 반환하는 값과 비교하는 것입니다. 미래의 타임스탬프가 지정되면 서버가 FailedPrecondition 오류를 반환합니다.

변경 로그 만들기

테이블에 발생하는 모든 변형에 대한 변경 로그를 만들어서 감사에 사용하려는 경우를 가정해 보겠습니다. 변경 내역을 워드 프로세싱 문서로 저장하는 테이블을 예로 들 수 있습니다. 커밋 타임스탬프를 사용하면 변경 로그를 만들기가 더 쉽습니다. 타임스탬프는 변경 로그 항목의 순서를 강제할 수 있기 때문입니다. 다음의 예시와 같은 스키마를 사용하여 특정 문서에 대한 변경 내역을 저장하는 변경 로그를 빌드할 수 있습니다.

CREATE TABLE Documents (
  UserId     INT64 NOT NULL,
  DocumentId INT64 NOT NULL,
  Contents   STRING(MAX) NOT NULL,
) PRIMARY KEY (UserId, DocumentId);

CREATE TABLE DocumentHistory (
  UserId     INT64 NOT NULL,
  DocumentId INT64 NOT NULL,
  Ts         TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
  Delta      STRING(MAX),
) PRIMARY KEY (UserId, DocumentId, Ts),
  INTERLEAVE IN PARENT Documents ON DELETE NO ACTION;

변경 로그를 만들려면, DocumentHistory에 행을 삽입하거나 업데이트하는 동일한 트랜잭션에서 Document에 새 행을 삽입합니다. DocumentHistory에 새 행을 삽입할 때, 자리표시자 spanner.commit_timestamp()(또는 클라이언트 라이브러리 상수)를 사용하여 Cloud Spanner에게 Ts 열에 커밋 타임스탬프를 기록하라고 명령합니다. DocumentsHistory 테이블과 Documents 테이블을 인터리브 처리하면 데이터 위치 파악과 더 효율적인 삽입 및 업데이트가 가능해집니다. 그러나 상위 행과 하위 행을 함께 삭제해야 한다는 제약도 추가됩니다. Documents의 행 다음에 있는 DocumentHistory의 행이 삭제되도록 하려면 이들 테이블을 인터리브 처리하지 마세요.

최고의 성능을 위해 행의 크기를 4GB 미만으로 제한하는 것이 좋습니다. 행의 크기에는 최상위 행과 인터리브 처리된 하위 행 및 색인 행이 모두 포함됩니다. 이 예시에는 행 크기 제한으로 인해 특정 문서의 DocumentHistory에 있을 수 있는 행 수에 제한이 있습니다. DocumentHistory의 변경 로그가 클 것으로 예상되는 경우, DocumentHistory에서 가장 오래된 행을 삭제하도록 앱을 디자인할 수 있습니다. 또는 DocumentHistory가 인터리브 처리된 행이 아닌 최상위 테이블이 되도록 스키마를 디자인할 수 있습니다.

다음 단계

커밋 타임스탬프를 사용하여 Go로 변경 로그 만들기