架构更新最佳实践

本文档介绍了更新架构的最佳实践。

开始架构更新之前的过程

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

  • 确保数据库中的所有现有数据都符合架构更新引入的限制条件。由于某些架构更新取决于实际数据,而不仅仅是当前架构,因此测试数据库中的成功更新并不能保证生产数据库中的成功更新。以下是一些常见示例:

    • 如果您向现有列添加 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
  • 强制 Spanner 验证现有数据的语句,例如添加 NOT NULL 或长度限制。

但是,这些类型的语句不一定会创建多个架构版本。Spanner 将尝试检测何时可以优化这些类型的语句以避免使用多个架构版本(这取决于批处理)。例如,与用于索引基表的 CREATE TABLE 语句位于同一批次的 CREATE INDEX 语句(不干预用于其他表的语句)无需回填索引数据,因为 Spanner 可以保证在创建索引时基表为空。大型更新部分介绍了如何使用此属性高效地创建多个索引。

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

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

批量执行语句的顺序

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

Spanner 将按顺序应用同一批次中的语句,并在第一个错误处停止。如果应用语句时产生错误,则系统会回滚该语句。该批次中以前应用过的语句的结果不会回滚。这种按顺序应用语句的方式意味着,如果您希望需要进行不可避免的回填的语句并行运行(例如在大型现有表上创建多个索引),则应在单独的批次中提交这些语句,因为每次回填可能需要很长时间。另一方面,如果您要创建包含索引的新表,最佳实践是将它们放在同一批次中(先执行 CREATE TABLE,再执行 CREATE INDEX),以完全避免回填。

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

大型架构更新方法

创建表并对该表创建大量索引的最佳方式是同时创建所有这些内容,这样只会创建一个架构版本。最佳实践是在 DDL 语句列表中紧跟在表之后创建索引。您可以在创建数据库时创建表及其索引,也可以在单个大批量 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) 等待每个请求完成,然后再开始新的请求。等待每个请求完成后,您的应用就可以跟踪架构更新的进度。这样做还可将待处理架构更新的积压量保持在可管理的范围内。

批量加载

将数据批量加载到新表中时,您可以在加载数据之前或之后创建二级索引。如果在加载数据后创建索引,加载数据的速度会更快,但这意味着必须回填索引。

如果您先加载数据,然后再创建索引,则数据注入速度会更快,因为此时只会写入表,而稍后的索引回填可以以优化的批次写入索引数据,这比同时写入索引数据和表数据更高效。不过,回填索引需要多个架构版本,并且存在限制;如大型更新的选项中所述,您应在单个批次中创建不超过 10 个需要回填的索引,最好每天创建不超过 3 个此类索引。

或者,您也可以在同一批次中创建表和索引,如大型更新方法中所述。这样可以避免索引回填,但批量加载数据会变慢,因为在加载数据时必须更新每个索引。

在特定情况下,哪种选择更好取决于要加载的数据量、具体的表和索引键、需要的索引数量,以及在同一数据库中需要批量加载操作的频率。经验法则是,如果需要将大量数据加载到每个表中,但只需要少量索引,最好单独创建索引。