GoogleSQL 언어 데이터베이스에서 타임스탬프 커밋

여기서는 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() 자리표시자 문자열을 사용합니다. 클라이언트 라이브러리에서 제공하는 커밋 타임스탬프 상수를 사용할 수도 있습니다. 예를 들어 자바 클라이언트의 이 상수는 Value.COMMIT_TIMESTAMP입니다.

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

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

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

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

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

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

키와 색인

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

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

allow_commit_timestamp 옵션은 상위 테이블과 하위 테이블의 모든 기본 키에서 일관되어야 합니다. 이 옵션이 모든 기본 키에서 일관되지 않으면 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)

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

기존 타임스탬프 열을 커밋 타임스탬프 열로 변환할 수 있으나 이렇게 하려면 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 문에서 커밋 타임스탬프를 씁니다. Spanner는 트랜잭션이 커밋될 때 커밋 타임스탬프를 선택합니다.

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

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

다음 코드 예시에서는 PENDING_COMMIT_TIMESTAMP 함수를 사용하여 LastUpdateTime 열에 커밋 타임스탬프를 씁니다.

C++

void DmlStandardUpdateWithTimestamp(google::cloud::spanner::Client client) {
  using ::google::cloud::StatusOr;
  namespace spanner = ::google::cloud::spanner;
  auto commit_result = client.Commit(
      [&client](spanner::Transaction txn) -> StatusOr<spanner::Mutations> {
        auto update = client.ExecuteDml(
            std::move(txn),
            spanner::SqlStatement(
                "UPDATE Albums SET LastUpdateTime = PENDING_COMMIT_TIMESTAMP()"
                "  WHERE SingerId = 1"));
        if (!update) return std::move(update).status();
        return spanner::Mutations{};
      });
  if (!commit_result) throw std::move(commit_result).status();
  std::cout << "Update was successful "
            << "[spanner_dml_standard_update_with_timestamp]\n";
}

C#


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

public class UpdateUsingDmlWithTimestampCoreAsyncSample
{
    public async Task<int> UpdateUsingDmlWithTimestampCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        using var cmd = connection.CreateDmlCommand("UPDATE Albums SET LastUpdateTime = PENDING_COMMIT_TIMESTAMP() WHERE SingerId = 1");
        int rowCount = await cmd.ExecuteNonQueryAsync();

        Console.WriteLine($"{rowCount} row(s) updated...");
        return rowCount;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

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

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

	_, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		stmt := spanner.Statement{
			SQL: `UPDATE Albums
				SET LastUpdateTime = PENDING_COMMIT_TIMESTAMP()
				WHERE SingerId = 1`,
		}
		rowCount, err := txn.Update(ctx, stmt)
		if err != nil {
			return err
		}
		fmt.Fprintf(w, "%d record(s) updated.\n", rowCount)
		return nil
	})
	return err
}

자바

static void updateUsingDmlWithTimestamp(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        String sql =
            "UPDATE Albums "
                + "SET LastUpdateTime = PENDING_COMMIT_TIMESTAMP() WHERE SingerId = 1";
        long rowCount = transaction.executeUpdate(Statement.of(sql));
        System.out.printf("%d records updated.\n", rowCount);
        return null;
      });
}

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

database.runTransaction(async (err, transaction) => {
  if (err) {
    console.error(err);
    return;
  }
  try {
    const [rowCount] = await transaction.runUpdate({
      sql: `UPDATE Albums
        SET LastUpdateTime = PENDING_COMMIT_TIMESTAMP()
        WHERE SingerId = 1`,
    });

    console.log(`Successfully updated ${rowCount} records.`);
    await transaction.commit();
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    // Close the database when finished.
    database.close();
  }
});

PHP

use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Spanner\Transaction;

/**
 * Update data with a DML statement using timestamps.
 *
 * The database and table must already exist and can be created using
 * `create_database`.
 * Example:
 * ```
 * insert_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function update_data_with_dml_timestamp(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $database->runTransaction(function (Transaction $t) {
        $rowCount = $t->executeUpdate(
            'UPDATE Albums '
            . 'SET LastUpdateTime = PENDING_COMMIT_TIMESTAMP() WHERE SingerId = 1');
        $t->commit();
        printf('Updated %d row(s).' . PHP_EOL, $rowCount);
    });
}

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"

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

def update_albums(transaction):
    row_ct = transaction.execute_update(
        "UPDATE Albums "
        "SET LastUpdateTime = PENDING_COMMIT_TIMESTAMP() "
        "WHERE SingerId = 1"
    )

    print("{} record(s) updated.".format(row_ct))

database.run_in_transaction(update_albums)

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
row_count = 0

client.transaction do |transaction|
  row_count = transaction.execute_update(
    "UPDATE Albums SET LastUpdateTime = PENDING_COMMIT_TIMESTAMP() WHERE SingerId = 1"
  )
end

puts "#{row_count} records updated."

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(std::move(select));
  for (auto& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::move(row).status();
    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#


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

public class QueryDataWithTimestampColumnAsyncSample
{
    public class Album
    {
        public int SingerId { get; set; }
        public int AlbumId { get; set; }
        public DateTime? LastUpdateTime { get; set; }
        public long? MarketingBudget { get; set; }
    }

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

        using var connection = new SpannerConnection(connectionString);
        using var cmd = connection.CreateSelectCommand("SELECT SingerId, AlbumId, MarketingBudget, LastUpdateTime FROM Albums ORDER BY LastUpdateTime DESC");

        var albums = new List<Album>();
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            albums.Add(new Album
            {
                SingerId = reader.GetFieldValue<int>("SingerId"),
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                LastUpdateTime = reader.IsDBNull(reader.GetOrdinal("LastUpdateTime")) ? (DateTime?)null : reader.GetFieldValue<DateTime>("LastUpdateTime"),
                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 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(string $instanceId, string $databaseId): void
{
    $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("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()(또는 클라이언트 라이브러리 상수)를 열 값으로 전달하는 대신에 커밋 타임스탬프 열에 고유한 값을 지정할 수 있습니다. 이 값은 과거의 타임스탬프여야 합니다. 이 같은 제한은 타임스탬프 기록 작업을 저렴하고 신속한 작업으로 만들어 줍니다. 미래의 타임스탬프가 지정되면 서버가 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()(또는 클라이언트 라이브러리 상수)를 사용하여 Spanner에게 Ts 열에 커밋 타임스탬프를 기록하라고 명령합니다. DocumentsHistory 테이블과 Documents 테이블을 인터리브 처리하면 데이터 위치 파악과 더 효율적인 삽입 및 업데이트가 가능해집니다. 그러나 상위 행과 하위 행을 함께 삭제해야 한다는 제약도 추가됩니다. Documents의 행 다음에 있는 DocumentHistory의 행이 삭제되도록 하려면 이들 테이블을 인터리브 처리하지 마세요.

커밋 타임스탬프로 최근 데이터 쿼리 최적화

커밋 타임스탬프를 사용하면 특정 시간 이후에 작성된 데이터를 검색할 때 쿼리 I/O를 줄일 수 있는 Spanner 최적화가 사용 설정됩니다.

이 최적화를 활성화하려면 쿼리의 WHERE 절에 다음 속성을 사용하여 테이블의 커밋 타임스탬프 열과 사용자가 제공하는 특정 시간 간의 비교를 포함해야 합니다.

  • 특정 시간을 상수 표현식(리터럴, 매개변수 또는 자체 인수가 상수로 평가되는 함수)으로 제공합니다.

  • > 또는 >= 연산자를 사용하여 커밋 타임스탬프가 지정된 시간보다 최신인지 여부를 비교합니다.

  • 필요한 경우 AND를 사용하여 WHERE 절에 제약사항을 더 추가합니다. OR을 사용하여 절을 확장하면 이 최적화에서 쿼리가 유효하지 않습니다.

예를 들어 커밋 타임스탬프 열이 포함된 다음 Performances 테이블을 살펴보겠습니다.

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

이 쿼리는 테이블의 커밋 타임스탬프 열과 상수 표현식(이 경우 리터럴) 간에 크거나 같음 비교가 있기 때문에 앞서 설명한 커밋 타임스탬프 최적화의 이점을 얻습니다.

SELECT * FROM Performances WHERE LastUpdateTime >= "2022-05-01";

커밋 쿼리와 인수가 모두 쿼리 실행 중에 상수로 평가되는 함수보다 크므로 다음 쿼리도 최적화에 적합합니다.

SELECT * FROM Performances
  WHERE LastUpdateTime > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY);

다음 단계