GoogleSQL 言語データベースでの commit タイムスタンプ

このトピックでは、Spanner で実行する挿入オペレーションと更新オペレーションごとに commit タイムスタンプを書き込む方法について説明します。この機能を使用するには、TIMESTAMP 列に allow_commit_timestamp オプションを設定し、各トランザクションの一部としてタイムスタンプを書き込みます。

概要

TrueTime テクノロジに基づく commit タイムスタンプは、トランザクションがデータベースに commit された時刻を表します。allow_commit_timestamp 列オプションを使用すると、commit タイムスタンプを列にアトミックに格納できます。テーブルに格納された commit タイムスタンプを使用すると、ミューテーションの正確な順序を確認し、変更履歴のような機能を構築できます。

データベースに commit タイムスタンプを挿入するには、次の手順に沿って操作します。

  1. タイプが TIMESTAMP の列を作成し、スキーマ定義で列オプション allow_commit_timestamptrue に設定します。次に例を示します。

    CREATE TABLE Performances (
        ...
        LastUpdateTime  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
        ...
    ) PRIMARY KEY (...);
    
  2. DML を使用して挿入や更新を実行する場合は、PENDING_COMMIT_TIMESTAMP 関数を使用して commit タイムスタンプを書き込みます。

    ミューテーションを使用して挿入や更新を実行する場合は、commit タイムスタンプ列の挿入時または更新時に、プレースホルダ文字列 spanner.commit_timestamp() を使用します。クライアント ライブラリによって提供される commit タイムスタンプ定数を使用することもできます。たとえば、Java クライアントではこの定数は Value.COMMIT_TIMESTAMP です。

Spanner で、これらのプレースホルダを列の値として使用してトランザクションを commit すると、実際の commit タイムスタンプが指定された列(たとえば、LastUpdateTime 列)に書き込まれます。この列の値を使用して、テーブルの更新履歴を作成できます。

commit タイムスタンプの値は一意であるとは限りません。重複しない一連のフィールドに書き込むトランザクションの場合、同じタイムスタンプが存在する可能性があります。重複する一連のフィールドに書き込むトランザクションの場合は、タイムスタンプは一意になります。

Spanner の commit タイムスタンプはマイクロ秒単位の精度であり、TIMESTAMP 列に保存されるときにナノ秒に変換されます。

commit タイムスタンプ列の作成と削除

commit タイムスタンプのサポートを追加または削除するには、allow_commit_timestamp 列オプションを使用します。これにより、次のことができます。

  • 新しいテーブルを作成して、列で commit タイムスタンプがサポートされるように指定する。
  • 既存のテーブルを変更する場合:
    • commit タイムスタンプをサポートする新しい列を追加する。
    • 既存の TIMESTAMP 列を変更して commit タイムスタンプをサポートする。
    • 既存の TIMESTAMP 列を変更して commit タイムスタンプのサポートを削除する。

キーとインデックス

commit タイムスタンプ列は、主キー列または主キー以外の列として使用できます。主キーは ASC または DESC で定義します。

  • ASC(デフォルト)- キーを昇順にすると、特定の時間以降を照会する場合に便利です。
  • DESC - キーを降順にすると、最終行がテーブルの先頭になります。これにより、最新のレコードにすばやくアクセスできます。

親テーブルと子テーブルの主キーには、同じ allow_commit_timestamp オプションを設定する必要があります。主キーの間で異なるオプションを指定すると、Spanner がエラーを返します。スキーマを作成または更新している間は、このオプションが一致していない可能性があります。

次のシナリオで commit タイムスタンプを使用すると、データのパフォーマンスが低下するホットスポットが作成されます。

  • テーブルの主キーの最初の部分としての commit タイムスタンプ列:

    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)
    

ホットスポットは、書き込み率が低い場合でもデータのパフォーマンスを低下させます。インデックスが作成されていないキー以外の列に commit タイムスタンプを有効にする場合は、パフォーマンスのオーバーヘッドは発生しません。

commit タイムスタンプ列を作成する

次の DDL では、commit タイムスタンプをサポートする列を持つテーブルを作成します。

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 大文字と小文字が区別されます。

既存のテーブルに commit タイムスタンプ列を追加する

commit タイムスタンプ列を既存のテーブルに追加するには、ALTER TABLE ステートメントを使用します。たとえば、Performances テーブルに LastUpdateTime 列を追加するには、次のステートメントを使用します。

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

タイムスタンプ列を commit タイムスタンプ列に変換する

既存のタイムスタンプ列を commit タイムスタンプ列に変換できます。ただし、これを行うには Spanner で既存のタイムスタンプ値が過去の値であることを検証する必要があります。例:

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

SET OPTIONS を含む ALTER TABLE ステートメントで、列のデータ型または NULL アノテーションを変更することはできません。詳細については、データ定義言語をご覧ください。

commit タイムスタンプ オプションを削除する

列から commit タイムスタンプのサポートを削除する場合は、ALTER TABLE ステートメントでオプション allow_commit_timestamp=null を使用します。commit タイムスタンプの動作は削除されますが、列はタイムスタンプのままです。オプションを変更しても、型や NULL 値(NOT NULL)など、列の他の特性は変更されません。例:

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

DML ステートメントを使用して commit タイムスタンプを作成する

DML ステートメントで commit タイムスタンプを作成するには、PENDING_COMMIT_TIMESTAMP 関数を使用します。トランザクションが commit されると、Spanner が commit タイムスタンプを選択します。

次の DML ステートメントは、commit タイムスタンプで Performances テーブルの LastUpdateTime 列を更新します。

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

次のコード例では、PENDING_COMMIT_TIMESTAMP 関数を使用して、LastUpdateTime 列に commit タイムスタンプを作成します。

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
}

Java

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"

commit タイムスタンプは、allow_commit_timestamp=true オプションでアノテーションを設定した列にのみ書き込まれます。

複数のテーブルの行に変異がある場合は、各テーブルの commit タイムスタンプ列に spanner.commit_timestamp()(またはクライアント ライブラリ定数)を指定する必要があります。

commit タイムスタンプ列をクエリする

次の例では、テーブルの commit タイムスタンプ列をクエリで取得します。

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

Java

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

commit タイムスタンプ列に独自の値を指定する

列の値として spanner.commit_timestamp()(またはクライアント ライブラリ定数)を渡すのではなく、commit タイムスタンプ列に独自の値を指定することもできます。ただし、値は過去のタイムスタンプでなければなりません。タイムスタンプの作成コストを抑え、オペレーションを高速で実行するために、この制限が設定されています。未来のタイムスタンプが指定されると、サーバーは FailedPrecondition エラーを返します。

変更履歴を作成する

テーブルで発生する変異の変更履歴を作成し、その履歴を監査で使用するとします。たとえば、ワープロ文書の変更履歴を格納するテーブルがあるとします。commit タイムスタンプを使用すると、タイムスタンプによって変更履歴の項目順が決まるため、変更履歴を簡単に作成できます。次のようなスキーマを使用して変更履歴を作成し、特定のドキュメントに対する変更履歴を保存します。

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;

変更履歴を作成するには、Document の行を挿入または更新するトランザクションで、DocumentHistory に新しい行を挿入します。DocumentHistory に新しい行を挿入するときに、プレースホルダ spanner.commit_timestamp()(またはクライアント ライブラリ定数)を使用し、commit タイムスタンプを列 Ts に書き込むように Spanner に指示します。DocumentsHistory テーブルと Documents テーブルをインターリーブすることで、データの局所性が維持され、挿入や更新を効率的に行うことができます。ただし、親の行と子の行を一緒に削除する必要があります。Documents の行を削除した後も DocumentHistory に行を残す場合には、テーブルをインターリーブしないでください。

commit タイムスタンプを使用して最近のデータクエリを最適化する

commit タイムスタンプを使用すると Spanner を最適化でき、それによって特定の時間よりも後に書き込まれたデータを取得する際のクエリ I/O を削減できます。

この最適化を有効にするには、クエリの WHERE 句に、テーブルの commit タイムスタンプ列と指定した特定の時刻の間の比較を、次の属性とともに、含める必要があります。

  • 定数式として特定の時間を指定します。リテラル、パラメータ、または独自の引数が定数として評価される関数が該当します。

  • > 演算子または >= 演算子を使用して、commit タイムスタンプが指定時刻より新しいかどうかを比較します。

  • 必要に応じて、ANDWHERE 句にさらに制限を追加します。OR で句を拡張すると、クエリはこの最適化の対象から除外されます。

たとえば、commit タイムスタンプ列を含む次の 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);

このクエリには、テーブルの commit タイムスタンプ列と定数式(この場合)とより大きい値または等しい値が比較されるため、前述の commit タイムスタンプの最適化のメリットがあります。この場合、リテラルは以下のとおりです。

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

次のクエリも、commit タイムスタンプと、クエリの実行中にすべての引数が定数として評価される関数の間の比較よりも大きいため、最適化の対象になります。

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

次のステップ