Spanner 允许您在不停机的情况下进行架构更新。您可以通过几种方式更新现有数据库的架构:
在 Google Cloud 控制台中
在 Spanner Studio 页面上提交
ALTER TABLE命令。如需访问 Spanner Studio 页面,请在数据库概览或表概览页面中点击 Spanner Studio。
使用
gcloud spanner命令行工具使用
gcloud spanner databases ddl update命令提交ALTER TABLE命令。使用客户端库
使用
projects.instances.databases.updateDdlREST API使用
UpdateDatabaseDdlRPC API
支持的架构更新
Spanner 支持对现有数据库执行以下架构更新操作:
- 添加或删除命名架构。
- 新建一个表。 新表格中的列可以为
NOT NULL。 - 删除一个表,前提是该表内没有交错其他表,并且没有二级索引。
- 创建或删除带有外键的表。
- 在现有表中添加或移除外键。
- 将一个非键列添加到任何表。新的非键列不能为
NOT NULL。 - 从任何表中删除非键列,除非二级索引、外键、存储的生成列或检查限制条件使用该列。
- 将
NOT NULL添加到非键列,从而排除ARRAY列。 - 从非键列中移除
NOT NULL。 - 将
STRING列更改为BYTES列,或将BYTES列更改为STRING列。 - 将
PROTO列更改为BYTES列,或将BYTES列更改为PROTO列。 - 更改
PROTO列的 proto 消息类型。 - 使用
ALTER PROTO BUNDLE向ENUM定义添加新值并重命名现有值。 - 以任意方式更改
PROTO BUNDLE中定义的消息,前提是这些消息的被修改字段不会用作任何表中的键,并且现有数据满足新限制条件。 - 增加或减少
STRING或BYTES类型的长度限制(包括增加到MAX),前提是它不是由一个或多个子表继承的主键列。 - 将
ARRAY<STRING>、ARRAY<BYTES>或ARRAY<PROTO>列的长度限制增加或减少到允许的最大值。 - 在值和主键列中启用或停用提交时间戳。
- 添加或移除二级索引。
- 在现有表中添加或移除检查限制条件。
- 在现有表中添加或移除存储的生成列。
- 构建新的优化器统计信息软件包。
- 创建和管理视图。
- 创建和管理序列。
- 创建数据库角色并授予权限。
- 设置、更改或删除列的默认值。
- 更改数据库选项(例如
default_leader或version_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,您可以发出一批(一个或多个)CREATE、ALTER 或 DROP 语句。
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 |
如果基表是在索引之前创建的,则为几分钟到几个小时。 如果语句与基表的 |
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值。 - 如果缩短
STRING或BYTES列允许的长度,请检查该列中的所有现有值是否符合长度限制条件。
- 如果您向现有列添加
如果要向正在进行架构更新的列、表或索引中写入数据,请确保所写入的值符合新的限制条件。
如果要删除某个列、表或索引,请确保不再向其中写入数据或从中读取数据。
限制架构更新的频率
如果您在短时间内执行的架构更新过多,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 TABLE 语句位于同一批次的 CREATE INDEX 语句(不干预用于其他表的语句)无需回填索引数据,因为 Spanner 可以保证在创建索引时基表为空。大型更新部分介绍了如何使用此属性高效地创建多个索引。
如果无法批量处理 DDL 语句以避免创建多个架构版本,您应限制单个数据库的架构在其保留期限内的架构更新次数。请增加进行架构更新的时间范围,以便 Spanner 能在创建新版本之前移除旧的架构版本。
- 对于某些关系型数据库管理系统,有一些软件包会在每次生产部署时对数据库进行一连串的升级和降级架构更新。Spanner 不建议使用这些类型的处理。
- Spanner 经过优化,可使用主键对多租户解决方案的数据进行分区。对每个客户使用单独表的多租户解决方案可能会导致大量架构更新操作积压,需要很长时间才能完成。
- 需要验证或索引回填的架构更新会使用更多服务器资源,因为每个语句都会在内部创建多个架构版本。
大型架构更新方法
创建表并对该表创建大量索引的最佳方式是同时创建所有这些内容,这样只会创建一个架构版本。最佳实践是在 DDL 语句列表中紧跟在表之后创建索引。您可以在创建数据库时创建表及其索引,也可以在单个大批量 DDL 语句中创建表及其索引。如果您需要创建许多表,每个表都包含许多索引,则可以将所有语句都包含在一个批次中。如果所有语句都可以使用单个架构版本一起执行,您可以在单个批次中包含数千个语句。
当语句需要回填索引数据或需要执行数据验证时,它无法在单个架构版本中执行。当索引的基表已存在(原因可能是在上一批次的 DDL 语句中创建了基表,或者是需要多个架构版本的 CREATE TABLE 和 CREATE 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) 等待每个请求完成,然后再开始新的请求。等待每个请求完成后,您的应用就可以跟踪架构更新的进度。这样做还可将待处理架构更新的积压量保持在可管理的范围内。
批量加载
如果您在创建表后将数据批量加载到表中,则在加载数据后创建索引通常更高效。如果要添加多个索引,则更高效的做法可能是在创建数据库时在初始架构中包含所有表和索引,如大型更新方法中所述。