进行架构更新

Spanner 让您可以在不停机的情况下进行架构更新。您可以通过几种方式更新现有数据库的架构:

支持的架构更新

Spanner 支持对现有数据库进行以下架构更新:

  • 新建一个表。 新表格中的列可以为 NOT NULL
  • 删除一个表,前提是该表内没有交错其他表,并且没有二级索引。
  • 创建或删除带有外键的表。
  • 在现有表中添加或移除外键。
  • 将一个非键列添加到任何表。新的非键列不能为 NOT NULL
  • 从任何表中删除非键列,除非二级索引、外键、存储的生成列或检查限制条件使用该列。
  • NOT NULL 添加到非键列,从而排除 ARRAY 列。
  • 从非键列中移除 NOT NULL
  • STRING 列更改为 BYTES 列,或将 BYTES 列更改为 STRING 列。
  • 增加或减少 STRINGBYTES 类型的长度限制(包括增加到 MAX),前提是它不是由一个或多个子表继承的主键列。
  • 在值和主键列中启用或停用提交时间戳
  • 添加或移除二级索引。
  • 在现有表中添加或移除检查限制条件。
  • 在现有表中添加或移除存储的生成列。
  • 构造一个新的优化器统计信息软件包

架构更新性能

Spanner 中的架构更新不需要停机。如果您向某个 Spanner 数据库发出一批 DDL 语句,当 Spanner 将更新作为长时间运行的操作应用时,您可以继续从数据库写入和读取数据,而不会出现中断。

执行 DDL 语句所需的时间取决于更新是否需要验证现有数据或回填任何数据。例如,如果您向现有列添加 NOT NULL 注解,则 Spanner 必须读取该列中的所有值,以确保该列不包含任何 NULL 值。如果有大量数据需要验证,则此步骤可能需要很长时间。另一个示例是,如果您要向数据库添加索引:Spanner 会使用现有数据回填索引,并且该过程可能需要很长时间,具体取决于索引的定义方式和相应基表的大小。但是,如果您向表中添加新列,则没有要验证的现有数据,因此 Spanner 可以更快地进行更新。

总而言之,不需要 Spanner 验证现有数据的架构更新可以在几分钟内完成。需要验证的架构更新可能需要更长时间,具体取决于需要验证的现有数据量,但数据验证操作会在后台以低于生产流量的优先级执行。下一节将详细讨论需要验证数据的架构更新。

根据视图定义验证的架构更新

当您进行架构更新时,Spanner 会验证该更新不会使用于定义现有视图的查询失效。如果验证成功,则架构更新就会成功。如果验证不成功,则架构更新将失败。如需了解详情,请参阅创建视图时的最佳做法

需要验证数据的架构更新

您可以执行需要验证现有数据是否符合新的限制条件的架构更新。如果架构更新需要验证数据,Spanner 会禁止对受影响的架构实体进行存在冲突的架构更新,并在后台验证数据。如果验证成功,则架构更新就会成功。如果验证失败,则架构更新不成功。验证操作会作为长时间运行的操作执行。您可以检查这些操作的状态,以此确定这些操作是成功还是失败。

例如,假设您在架构中定义了一个 Songwriters 表:

GoogleSQL

CREATE TABLE Songwriters (
  Id         INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  Nickname   STRING(MAX),
  OpaqueData BYTES(MAX),
) PRIMARY KEY (Id);

可进行以下架构更新,但这些架构更新需要验证,并且可能需要较长时间才能完成,具体取决于现有数据量:

  • NOT NULL 注解添加到非键列。例如:

    ALTER TABLE Songwriters ALTER COLUMN Nickname STRING(MAX) NOT NULL;
    
  • 缩短列的长度。例如:

    ALTER TABLE Songwriters ALTER COLUMN FirstName STRING(10);
    
  • BYTES 改为 STRING。例如:

    ALTER TABLE Songwriters ALTER COLUMN OpaqueData STRING(MAX);
    
  • 在现有 TIMESTAMP 列上启用提交时间戳。例如:

    ALTER TABLE Albums ALTER COLUMN LastUpdateTime SET OPTIONS (allow_commit_timestamp = true);
    
  • 向现有表添加检查限制条件。

  • 将存储的生成列添加到现有表中。

  • 创建带有外键的新表。

  • 向现有表中添加外键。

如果基础数据不满足新的限制条件,则这些架构更新会失败。例如,如果 Nickname 列中的任何值为 NULL,则上面的 ALTER TABLE Songwriters ALTER COLUMN Nickname STRING(MAX) NOT NULL 语句会失败,因为现有数据不符合新定义的 NOT NULL 限制。

验证数据操作可能需要数分钟到数小时。完成数据验证的时间取决于以下因素:

  • 数据集的大小
  • 实例的计算容量
  • 实例上的负载

在更新完成之前,某些架构更新可以更改数据库请求的行为。例如,如果您在某列中添加 NOT NULL,则对于针对该列使用 NULL 的新请求,Spanner 会几乎立即开始拒绝写入。如果新的架构更新因未通过数据验证而最终失败,那么写入操作会在一段时间内被阻止,即使这些操作原本应该会被旧架构接受,也是如此。

您可以使用 projects.instances.databases.operations.cancel 方法或使用 gcloud spanner operations 取消长时间运行的数据验证操作。

批量执行语句的顺序

如果您使用 Google Cloud CLI、REST API 或 RPC API,则可以批量发出一个或多个 CREATEALTERDROP 语句

Spanner 将按顺序应用同一批次中的语句,并在出现第一个错误时停止。如果应用语句时产生错误,则系统会回滚该语句。该批次中以前应用过的语句的结果不会回滚。

Spanner 可能会对来自不同批次的语句进行组合和重新排序,可能会将来自不同批次的语句混合到应用于数据库的一个原子更改中。在每个原子更改中,不同批次的语句按照任意顺序执行。例如,如果一批语句包含 ALTER TABLE MyTable ALTER COLUMN MyColumn STRING(50),另一批语句包含 ALTER TABLE MyTable ALTER COLUMN MyColumn STRING(20),则 Spanner 会将该列保留为这两个状态之一,但不指定哪一个。

在架构更新期间创建的架构版本

Spanner 使用架构版本控制,以便在对大型数据库进行架构更新时不会出现停机。Spanner 会维护旧的架构版本,以支持在处理架构更新时读取。然后,Spanner 会创建一个或多个新版本的架构来处理架构更新。如上所述,每个版本包含单个原子更改中的语句集合的结果。

架构版本不一定与批量 DDL 语句或单独的 DDL 语句一一对应。某些单独的 DDL 语句(例如为现有基表或需要数据验证的语句创建索引)会导致产生多个架构版本。在其他情况下,可以在单个版本中将多个 DDL 语句作为一批进行处理。旧架构版本可能会占用大量服务器和存储资源,并且会一直保留,直到过期(不再需要为旧版本数据的读取提供服务)。

下表显示了 Spanner 更新架构所需的时间。

架构操作 估计用时
CREATE TABLE 几分钟
CREATE INDEX

如果基表是在索引之前创建的,则为几分钟到几个小时。

如果语句与基表的 CREATE TABLE 语句同时执行,则为几分钟。

DROP TABLE 几分钟
DROP INDEX 几分钟
ALTER TABLE ... ADD COLUMN 几分钟
ALTER TABLE ... ALTER COLUMN

如果需要后台验证,则为几个小时。

如果不需要后台验证,则为几分钟。

ALTER TABLE ... DROP COLUMN 分钟
ANALYZE

几分钟到几小时,具体取决于数据库大小。

数据类型变更和变更数据流

如果您更改变更数据流监控的列的数据类型,相关后续变更数据流记录column_types 字段会反映其新类型,记录的 mods 字段中的 old_values JSON 数据也会反映。

变更数据流记录的 mods 字段的 new_values 始终与列的当前类型匹配。更改受监控列的数据类型不会影响此更改之前的任何变更数据流记录。

在从 BYTES 更改为 STRING 这一特定情况下,Spanner 会在架构更新过程中验证列的旧值。因此,在写入任何后续变更数据流记录时,Spanner 已安全地将旧的 BYTES 类型值解码为字符串。

架构更新的最佳做法

以下各节介绍了更新架构的最佳做法。

发出架构更新之前的过程

在您发出架构更新之前,请完成以下操作:

  • 验证数据库中您正在更改的所有现有数据是否满足架构更新所施加的限制条件。由于某些类型的架构更新的成功取决于数据库中的数据,而不仅仅取决于其当前架构,因此,测试数据库的架构成功更新并不能保证生产数据库的架构可成功更新。以下是一些常见示例:

    • 如果您向现有列添加 NOT NULL 注解,则需要确认该列不包含任何现有的 NULL 值。
    • 如果缩短 STRINGBYTES 列允许的长度,请检查该列中的所有现有值是否符合所需的长度限制条件。
  • 如果要向正在进行架构更新的列、表或索引中写入数据,请确保所写入的值符合新的限制条件。

  • 如果要删除某个列、表或索引,请确保不再向其中写入数据或从中读取数据。

限制架构更新的频率

如果您在短时间内执行太多架构更新,Spanner 可能会 throttle 排队架构更新的处理过程。这是因为 Spanner 限制了存储架构版本的空间。如果在保留期限内有太多旧架构版本,您的架构更新可能会受到限制。架构更改的最大速率取决于许多因素,其中之一是数据库中的总列数。例如,具有 2000 列(在 INFORMATION_SCHEMA.COLUMNS 中约为 2000 行)的数据库在保留期限内最多可以执行 1500 次简单的架构更改(如果架构更改需要多个版本,则更少)。如需查看正在进行的架构更新的状态,请使用 gcloud spanner operations list 命令并按 DATABASE_UPDATE_DDL 类型的操作进行过滤。如需取消正在进行的架构更新,请使用 gcloud spanner operations cancel 命令并指定操作 ID。

DDL 语句的批处理方式及其在每个批次中的顺序都会影响生成的架构版本数。为了最大限度增加在任何给定时间段内可以执行的架构更新次数,您都应该使用批处理,以最大限度减少架构版本数量。大规模更新中介绍了一些经验法则。

架构版本中所述,某些 DDL 语句将创建多个架构版本,当考虑进行批处理以及每个批次内的排序时,这些版本非常重要。有两种主要类型的语句可能会创建多个架构版本:

  • 可能需要回填索引数据的语句,例如 CREATE INDEX
  • 可能需要验证现有数据的语句,例如添加 NOT NULL

但是,这些类型的语句不一定会创建多个架构版本。Spanner 将尝试检测这些类型的语句何时可以优化,以避免使用多个架构版本(后者依赖于批处理)。例如,如果一个 CREATE INDEX 语句与索引基表的 CREATE TABLE 语句在同一批次中发生,而没有针对其他表添加任何中间语句,则可以避免需要回填索引数据,因为 Spanner 可以保证在创建索引时基表为空。大型更新部分介绍了如何使用此属性高效地创建多个索引。

如果无法批量处理 DDL 语句以避免创建多个架构版本,您应限制单个数据库的架构在其保留期限内的架构更新次数。请增加进行架构更新的时间范围,以允许 Spanner 在创建新版本之前移除旧版架构。

  • 对于某些关系型数据库管理系统,有一些软件包会在每次生产部署时对数据库进行一连串的升级和降级架构更新。不建议在 Spanner 中使用这些类型的进程。
  • Spanner 经过优化,可使用主键对多租户解决方案的数据进行分区。对每个客户使用单独表的多租户解决方案可能会导致大量架构更新操作积压,需要很长时间才能完成。
  • 需要验证或索引回填的架构更新会使用更多服务器资源,因为每个语句都会在内部创建多个架构版本。

大型架构更新方法

如需创建表并在该表上创建大量索引,最佳方式是同时创建所有索引,以便仅创建一个架构版本。最佳做法是创建索引,紧跟在 DDL 语句列表中的表后面。您可以在创建数据库时创建表及其索引,也可以在单个大批量语句中创建表及其索引。如果需要创建多个表,每个表都具有多个索引,则可以将所有语句包含在一个批次中。如果所有语句可以使用单个架构版本一起执行,则您可以在单个批次中包含数千个语句。

当语句需要回填索引数据或需要执行数据验证时,它无法在单个架构版本中执行。当索引的基表已存在(原因可能是在上一批次的 DDL 语句中创建了基表,或者是需要多个架构版本的 CREATE TABLECREATE INDEX 语句之间的批次中有一个语句)时,CREATE INDEX 语句便会发生上述问题。Spanner 要求单个批次中的此类语句不超过 10 个。创建需要回填的索引时,请特别注意,每个索引需要使用多个架构版本,因此最好每天创建不超过 3 个需要回填的新索引(无论以何种方式进行批处理,除非这种批处理方式能够避免回填)。

例如,下面这批语句将使用单个架构版本:

GoogleSQL

CREATE TABLE Singers (
SingerId   INT64 NOT NULL,
FirstName  STRING(1024),
LastName   STRING(1024),
) PRIMARY KEY (SingerId);

CREATE INDEX SingersByFirstName ON Singers(FirstName);

CREATE INDEX SingersByLastName ON Singers(LastName);

CREATE TABLE Albums (
SingerId   INT64 NOT NULL,
AlbumId    INT64 NOT NULL,
AlbumTitle STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId);

CREATE INDEX AlbumsByTitle ON Albums(AlbumTitle);

相比之下,该批次将使用许多架构版本,因为 UnrelatedIndex 需要回填(因为其基表必须已存在),并且强制要求以下所有索引也需要进行回填(即使它们与其基表位于同一批次):

GoogleSQL

CREATE TABLE Singers (
SingerId   INT64 NOT NULL,
FirstName  STRING(1024),
LastName   STRING(1024),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
SingerId   INT64 NOT NULL,
AlbumId    INT64 NOT NULL,
AlbumTitle STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId);

CREATE INDEX UnrelatedIndex ON UnrelatedTable(UnrelatedIndexKey);

CREATE INDEX SingersByFirstName ON Singers(FirstName);

CREATE INDEX SingersByLastName ON Singers(LastName);

CREATE INDEX AlbumsByTitle ON Albums(AlbumTitle);

最好是将 UnrelatedIndex 的创建操作移到批次的末尾或移到其他批次,以最大限度地减少架构版本。

等待 API 请求完成

在发出 projects.instances.databases.updateDdl (REST API) 或 UpdateDatabaseDdl (RPC API) 请求时,请分别使用 projects.instances.databases.operations.get (REST API) 或 GetOperation (RPC API) 等待每个请求完成,然后再开始新的请求。等待每个请求完成后,您的应用就可以跟踪架构更新的进度。这样做还可将待处理架构更新的积压量保持在可管理的范围内。

批量加载

如果您在创建表后将数据批量加载到表中,则在加载数据后创建索引通常更高效。如果要添加多个索引,则更高效的做法可能是在创建数据库时在初始架构中包含所有表和索引,如大型更新方法中所述。