GoogleSQL 方言数据库中的提交时间戳

本主题介绍如何为使用 Spanner 执行的每个插入和更新操作写入提交时间戳。要使用此功能,请在 TIMESTAMP 列设置 allow_commit_timestamp 选项,然后写入时间戳,将其作为每个事务的一部分。

概览

基于 TrueTime 技术的提交时间戳是在数据库中提交事务的时间。allow_commit_timestamp 列选项允许您以原子方式将提交时间戳存储到列中。借助存储在表中的提交时间戳,您可以确定变更的确切顺序并构建更改日志等功能。

要在数据库中插入提交时间戳,请完成以下步骤:

  1. 创建一个带有类型 TIMESTAMP 的列,并在架构定义中将列选项 allow_commit_timestamp 设置为 true。例如:

    CREATE TABLE Performances (
        ...
        LastUpdateTime  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
        ...
    ) PRIMARY KEY (...);
    
  2. 如果要使用 DML 执行插入或更新操作,请使用 PENDING_COMMIT_TIMESTAMP 函数来写入提交时间戳。

    如果要使用变更执行插入或更新操作,请使用占位符字符串 spanner.commit_timestamp() 插入或更新提交时间戳列。您还可以使用客户端库提供的提交时间戳常量。例如,Java 客户端中的此常量为 Value.COMMIT_TIMESTAMP

当 Spanner 使用这些占位符作为列值提交事务时,实际提交时间戳将写入指定的列(例如:LastUpdateTime 列)。然后,您可以使用此列值来创建表的更新历史记录。

提交时间戳的值不保证是唯一的。写入不重叠字段集的事务可能具有相同的时间戳。写入重叠字段集的事务具有唯一的时间戳。

Spanner 提交时间戳具有微秒精度,当存储在 TIMESTAMP 列中时会转换为纳秒。

创建和删除提交时间戳列

使用 allow_commit_timestamp 列选项来添加和移除对提交时间戳的支持:

  • 创建新表以指定列支持提交时间戳,请执行以下操作。
  • 更改现有表时,请执行以下操作:
    • 添加支持提交时间戳的新列,
    • 更改现有 TIMESTAMP 列以支持提交时间戳,
    • 更改现有 TIMESTAMP 列以移除提交时间戳支持,

键和索引

您可以使用提交时间戳列作为主键列或非键列。主键可以定义为 ASCDESC

  • 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 OPTIONSALTER TABLE 语句中某一列的数据类型或 NULL 注释。如需了解详情,请参阅数据定义语言

移除提交时间戳选项

如果您想从列中删除提交时间戳支持,请在 ALTER TABLE 语句中使用 allow_commit_timestamp=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
}

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