שמירת חותמות זמן במסדי נתונים של ניב GoogleSQL

במאמר הזה מוסבר איך לכתוב חותמת זמן של ביצוע פעולת commit לכל פעולת הוספה ועדכון שמבצעים ב-Spanner. כדי להשתמש בתכונה הזו, צריך להגדיר את האפשרות allow_commit_timestamp בעמודה 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 כדי לכתוב את חותמת הזמן של השמירה.

    אם אתם מבצעים הוספות או עדכונים באמצעות מוטציות, השתמשו במחרוזת placeholder‏ spanner.commit_timestamp() בהוספות או בעדכונים של עמודת חותמת הזמן של השליחה. אפשר גם להשתמש בקבוע של חותמת הזמן של השליחה שסופק על ידי ספריית הלקוח. לדוגמה, הקבוע הזה בלקוח Java הוא Value.COMMIT_TIMESTAMP.

כש-Spanner מבצע את הטרנזקציה באמצעות הפלייסולדרים האלה כערכי עמודות, חותמת הזמן של הביצוע בפועל נכתבת בעמודה שצוינה (לדוגמה: העמודה LastUpdateTime). אחר כך תוכלו להשתמש בערך של העמודה הזו כדי ליצור היסטוריה של עדכונים בטבלה.

אין ערובה לכך שערכי חותמת הזמן של השמירה יהיו ייחודיים. יכול להיות שטרנזקציות שכותבות לקבוצות לא חופפות של שדות יקבלו את אותה חותמת זמן. לעסקאות שכותבות לקבוצות חופפות של שדות יש חותמות זמן ייחודיות.

חותמות הזמן של ביצוע השינויים ב-Spanner הן ברמת דיוק של מיקרו-שנייה, והן מומרות לננו-שניות כשהן מאוחסנות בעמודות TIMESTAMP.

יצירה ומחיקה של עמודה עם חותמת זמן של קומיט

משתמשים באפשרות העמודה allow_commit_timestamp כדי להוסיף או להסיר תמיכה בחותמות זמן של קומיטים:

  • כשיוצרים טבלה חדשה כדי לציין שעמודה תומכת בחותמות זמן של ביצוע פעולות.
  • כשמשנים טבלה קיימת:
    • כדי להוסיף עמודה חדשה שתתמוך בחותמות זמן של ביצועים,
    • כדי לשנות עמודה קיימת TIMESTAMP כך שתתמוך בחותמות זמן של ביצועים,
    • כדי לשנות עמודה קיימת TIMESTAMP ולהסיר את התמיכה בחותמת הזמן של הקומיט

מפתחות ואינדקסים

אתם יכולים להשתמש בעמודה של חותמת הזמן של השמירה כעמודה של מפתח ראשי או כעמודה ללא מפתח. אפשר להגדיר מפתחות ראשיים כ-ASC או כ-DESC.

  • ASC (ברירת מחדל) – מפתחות עולים הם אידיאליים למענה על שאילתות החל מזמן ספציפי ואילך.
  • DESC – מקשים בסדר יורד שומרים את השורות האחרונות בחלק העליון של הטבלה. הם מאפשרים גישה מהירה לרשומות האחרונות.

האפשרות allow_commit_timestamp חייבת להיות עקבית בכל המפתחות הראשיים של טבלאות האב והצאצא. אם האפשרות לא עקבית במפתחות הראשיים, Spanner מחזיר שגיאה. האפשרות הזו יכולה להיות לא עקבית רק כשיוצרים או מעדכנים את הסכימה.

שימוש בחותמות זמן של ביצוע פעולות (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)
    

נקודות חמות מפחיתות את ביצועי הנתונים, גם עם שיעורי כתיבה נמוכים. אין תקורה של ביצועים אם חותמות הזמן של השמירה מופעלות בעמודות שאינן עמודות מפתח ושלא נוסף להן אינדקס.

יצירת עמודה של חותמת זמן לשליחת שינויים

ה-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

הוספת האפשרות משנה את עמודת חותמת הזמן באופן הבא:

  • אפשר להשתמש במחרוזת placeholder‏ spanner.commit_timestamp() (או בקבוע שסופק על ידי ספריית הלקוח) להוספות ולעדכונים.
  • העמודה יכולה להכיל רק ערכים שמתייחסים לעבר. מידע נוסף מופיע במאמר בנושא הוספת ערך משלכם לחותמת הזמן.

האפשרות allow_commit_timestamp היא תלוית אותיות רישיות.

הוספת עמודה של חותמת זמן של ביצוע Commit לטבלה קיימת

כדי להוסיף עמודה של חותמת זמן של ביצוע Commit לטבלה קיימת, משתמשים בהצהרה ALTER TABLE. לדוגמה, כדי להוסיף עמודה LastUpdateTime לטבלה Performances משתמשים בהצהרה הבאה:

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

המרת עמודה של חותמות זמן לעמודה של חותמות זמן של ביצוע

אפשר להמיר עמודה קיימת של חותמות זמן לעמודה של חותמות זמן של ביצוע פעולת Commit, אבל כדי לעשות את זה, Spanner צריך לוודא שערכי חותמות הזמן הקיימים הם מהעבר. לדוגמה:

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

אי אפשר לשנות את סוג הנתונים או את הביאור NULL של עמודה בהצהרה ALTER TABLE שכוללת SET OPTIONS. פרטים נוספים זמינים במאמר בנושא שפת הגדרת נתונים.

הסרת האפשרות של חותמת הזמן של הקומיט

כדי להסיר את התמיכה בחותמת הזמן של ביצוע השינויים מעמודה, משתמשים באפשרות allow_commit_timestamp=null בהצהרה ALTER TABLE. ההתנהגות של חותמת הזמן של השמירה מוסרת, אבל העמודה עדיין מכילה חותמת זמן. שינוי האפשרות לא משנה מאפיינים אחרים של העמודה, כמו סוג או אפשרות לערך null ‏ (NOT NULL). לדוגמה:

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

כתיבת חותמת זמן של ביצוע באמצעות פקודת DML

משתמשים בפונקציה PENDING_COMMIT_TIMESTAMP כדי לכתוב את חותמת הזמן של ביצוע השינוי בפקודת DML. מערכת Spanner בוחרת את חותמת הזמן של השמירה כשמתבצעת שמירה של הטרנזקציה.

פקודת ה-DML הבאה מעדכנת את העמודה LastUpdateTime בטבלה Performances עם חותמת הזמן של השמירה:

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

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

הזנת ערך משלכם בעמודה של חותמת הזמן של השליחה

אתם יכולים לספק ערך משלכם לעמודה של חותמת הזמן של השליחה, במקום להעביר את הערך 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, משתמשים ב-placeholder spanner.commit_timestamp() (או בקבוע של ספריית הלקוח) כדי להנחות את Spanner לכתוב את חותמת הזמן של השמירה בעמודה Ts. שילוב של טבלת DocumentsHistory עם טבלת Documents יאפשר לנתונים להיות מקומיים ויעיל יותר להוסיף ולעדכן אותם. עם זאת, היא מוסיפה גם את האילוץ ששורות ההורה והילד חייבות להימחק יחד. כדי שהשורות בטבלה DocumentHistory יישארו אחרי שהשורות בטבלה Documents יימחקו, אל תשלבו בין הטבלאות.

אופטימיזציה של שאילתות על נתונים מהזמן האחרון באמצעות חותמות זמן של ביצוע פעולות

חותמות הזמן של ביצוע השינויים מאפשרות לבצע אופטימיזציה ב-Spanner, שיכולה לצמצם את קלט/פלט השאילתות כשמאחזרים נתונים שנכתבו אחרי זמן מסוים.

כדי להפעיל את האופטימיזציה הזו, סעיף WHERE של שאילתה צריך לכלול השוואה בין עמודת חותמת הזמן של ביצוע השינויים בטבלה לבין זמן ספציפי שאתם מספקים, עם המאפיינים הבאים:

  • מציינים את השעה הספציפית כביטוי קבוע: ערך מילולי, פרמטר או פונקציה שהארגומנטים שלה הם קבועים.

  • אפשר להשוות בין חותמת הזמן של הקומיט לבין הזמן שצוין באמצעות האופרטורים > או >=.

  • אופציונלי: מוסיפים הגבלות נוספות לסעיף WHERE באמצעות AND. הוספת התנאי OR לפסקה גורמת לכך שהשאילתה לא עומדת בדרישות של האופטימיזציה הזו.

לדוגמה, נניח שיש לכם את הטבלה Performances הבאה, שכוללת עמודה של חותמת זמן של ביצוע 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);

השאילתה הזו נהנית מהאופטימיזציה של חותמת הזמן של השליחה שמתוארת למעלה, כי יש בה השוואה של גדול או שווה בין עמודת חותמת הזמן של השליחה של הטבלה לבין ביטוי קבוע – במקרה הזה, ערך מילולי:

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

גם השאילתה הבאה עומדת בדרישות לאופטימיזציה, כי יש בה השוואה מסוג 'גדול מ' בין חותמת הזמן של השליחה לבין פונקציה שכל הארגומנטים שלה מוערכים כקבועים במהלך ההרצה של השאילתה:

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

המאמרים הבאים