进行架构更新

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

支持的架构更新

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

  • 添加或删除已命名的架构。
  • 新建一个表。 新表格中的列可以为 NOT NULL
  • 删除一个表,前提是该表内没有交错其他表,并且没有二级索引。
  • 创建或删除带有外键的表。
  • 在现有表中添加或移除外键。
  • 将一个非键列添加到任何表。新的非键列不能为 NOT NULL
  • 从任何表中删除非键列,除非二级索引、外键、存储的生成列或检查限制条件使用该列。
  • NOT NULL 添加到非键列,从而排除 ARRAY 列。
  • 从非键列中移除 NOT NULL
  • STRING 列更改为 BYTES 列,或将 BYTES 列更改为 STRING 列。
  • PROTO 列更改为 BYTES 列,或将 BYTES 列更改为 PROTO 列。
  • 更改 PROTO 列的 proto 消息类型。
  • 将新值添加到 ENUM 定义中,并重命名现有值: ALTER PROTO BUNDLE
  • 在提供的情况下,以任意方式更改 PROTO BUNDLE 中定义的消息 这些消息中经过修改的字段不会用作任何表中的键 以及现有数据是否满足新的限制条件。
  • 增加或减少 STRINGBYTES 类型的长度限制(包括增加到 MAX),前提是它不是由一个或多个子表继承的主键列。
  • 增加或减少 ARRAY<STRING>ARRAY<BYTES>、 或ARRAY<PROTO>列。
  • 在值和主键列中启用或停用提交时间戳
  • 添加或移除二级索引。
  • 在现有表中添加或移除检查限制条件。
  • 在现有表中添加或移除存储的生成列。
  • 构造一个新的优化工具统计信息软件包
  • 创建和管理视图
  • 创建和管理序列
  • 创建数据库角色并授予权限。
  • 设置、更改或删除列的默认值。
  • 更改数据库选项(例如 default_leaderversion_retention_period)。
  • 创建和管理变更数据流
  • 创建和管理机器学习模型。

不支持的架构更新

Spanner 不支持现有资源的以下架构更新 数据库:

  • 如果表或索引键引用了 ENUM 类型的 PROTO 字段,则无法从 proto 枚举中移除 ENUM 值。(支持从 ENUM<> 列使用的枚举中移除 ENUM 值,包括在这些列用作键时。)

架构更新性能

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

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

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

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

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

需要验证数据的架构更新

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

例如,假设您使用music.proto RecordLabel 枚举和 Songwriter 协议消息:

  enum RecordLabel {
    COOL_MUSIC_INC = 0;
    PACIFIC_ENTERTAINMENT = 1;
    XYZ_RECORDS = 2;
  }

  message Songwriter {
    required string nationality   = 1;
    optional int64  year_of_birth = 2;
  }

如需在架构中添加 Songwriters 表,请执行以下操作:

GoogleSQL

CREATE PROTO BUNDLE (
  googlesql.example.music.Songwriter,
  googlesql.example.music.RecordLabel,
);

CREATE TABLE Songwriters (
  Id         INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  Nickname   STRING(MAX),
  OpaqueData BYTES(MAX),
  SongWriter googlesql.example.music.Songwriter
) PRIMARY KEY (Id);

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

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

  • 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);
    
  • INT64/INT32 改为 ENUM。例如:

    ALTER TABLE Albums ALTER COLUMN Label googlesql.example.music.RecordLabel;
    
  • RecordLabel 枚举定义中移除现有值。

  • 在现有 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

根据数据库大小,从几分钟到几小时不等。

数据类型更改和更改流

如果更改某列的数据类型, stream 观看,column_types 字段 相关的后续更改 记录old_values 中的 JSON 数据 条记录的mods 字段。

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

在从 BYTES 更改为 STRING 这一具体情况下, Spanner 验证列的旧列 值。 因此,Spanner 已安全地解码 BYTES 类型的值转换为字符串。 变更数据流记录。

架构更新的最佳做法

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

发出架构更新之前的过程

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

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

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

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

限制架构更新的频率

如果您在短时间内执行太多架构更新,Spanner 可以throttle 已排队的架构更新的处理。这是因为 Spanner 限制了 用于存储架构版本的空间。如果保留期限内有太多旧架构版本,您的架构更新可能会受到节流。架构更改的最大速率取决于多种因素,其中之一是数据库中的列总数。例如,如果数据库包含 2,000 个列(在 INFORMATION_SCHEMA.COLUMNS 中大约为 2,000 行),则在保留期内最多可以执行 1,500 次架构更改(如果架构更改需要多个版本,则更改次数会更少)。要查看 持续进行架构更新,请使用 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) 等待每个请求完成,然后再开始新的请求。等待每个请求完成后,您的应用就可以跟踪架构更新的进度。这样做还可将待处理架构更新的积压量保持在可管理的范围内。

批量加载

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