外键

本文档介绍了 Spanner 中的外键,以及如何使用这些键在数据库中强制执行参照完整性。以下主题可帮助您了解外键及其使用方式:

Spanner 中外键的概览

外键用于定义表之间的关系。您可以使用外键确保保持 Spanner 中这些关系的数据完整性。

假设您是一家电子商务企业的首席开发者。您正在设计一个数据库来处理客户订单。数据库必须存储每个订单、客户和产品的相关信息。图 1 展示了应用的基本数据库结构。

订单处理数据库的基本结构。

图 1:订单处理数据库示意图

您可以定义一个存储客户信息的 Customers 表、一个跟踪所有订单的 Orders 表以及一个存储每件商品的相关信息的 Products 表。

图 1 还展示了映射到以下现实世界关系的表之间的关联:

  • 客户下单。

  • 下了某商品的订单。

您决定数据库应实施以下规则,以确保系统中的订单有效。

  • 您无法为不存在的客户创建订单。

  • 客户无法对您没有的商品下订单。

实施这些规则(即“限制条件”),即表示您正在维护数据的参照完整性。当数据库保持参照完整性时,所有尝试添加无效数据的操作都将失败,这将导致数据之间出现无效链接或引用。参照完整性可防止用户出错。默认情况下,Spanner 使用外键来强制执行参照完整性。

使用外键定义参照完整性

以下内容再次介绍了订单处理示例,如图 2 所示,在设计中添加了更多细节。

采用外键的数据库架构

图 2. 采用外键的数据库架构示意图

现在设计为每个表中都显示了列名称和类型。Orders 表还定义了两个外键关系。FK_CustomerOrder 预期 Orders 中所有行都具有有效的 CustomerIdFK_ProductOrder 外键要求 Orders 表中的所有 ProductId 值都有效。下表将这些约束条件与您希望强制执行的实际规则一一对应。

外键名称 限制 实际说明
FK_CustomerOrder 预计 Orders 中所有行都具有有效的 CustomerId 有效客户下单
FK_ProductOrder 预计 Orders 中所有行都具有有效的 ProductId 对有效商品下了订单

Spanner 会强制执行使用强制外键指定的约束条件。这意味着,Spanner 无法执行在 Orders 表中尝试插入或更新行的任何事务,该表具有的 CustomerIdProductIdCustomersProducts 表中找不到。此外,它也无法执行在 CustomersProducts 表中尝试更新或删除行的事务,从而导致 Orders 表中的数据失效。如需详细了解 Spanner 如何验证限制条件,请参阅事务限制条件验证部分。

与强制性外键不同,Spanner 不会对信息外键进行约束条件验证。这意味着,如果您在此场景中使用信息外键,则在 Orders 表中尝试插入或更新行的任何事务,该表具有的 CustomerIdProductIdCustomersProducts 表中找不到,则该事务不会进行验证,并且不会失败。此外,与强制性外键不同,只有 GoogleSQL 支持信息外键,PostgreSQL 不支持。

外键特性

下面列出了 Spanner 中的外键特性。

  • 定义外键的表是引用表,而外键列是引用列。

  • 外键引用“被引用”表的的“被引用”列

  • 如示例所示,您可以为每个外键约束条件指定名称。如果您未指定名称,Spanner 会为您生成一个名称。您可以通过 Spanner 的 INFORMATION_SCHEMA 查询生成的名称。限制条件名称的范围限定为架构以及表和索引的名称,并且在架构内必须具有唯一性。

  • 引用列数和被引用列数必须相同。顺序很重要。例如,第一个引用列引用第一个被引用列,第二个引用列引用第二个被引用列。

  • 引用列及其被引用的对应项目的类型必须相同。您必须能够为列编制索引。

  • 您无法使用 allow_commit_timestamp=true 选项在列上创建外键。

  • 数组列不受支持。

  • 不支持 JSON 列。

  • 外键可以引用同一表的列(自引用外键)。例如,一个 Employee 表具有一个引用该表的 EmployeeId 列的 ManagerId 列。

  • 外键还可以在表之间形成循环关系,其中两个表直接或间接地相互引用。必须先有被引用表,然后才能创建外键。这意味着必须使用 ALTER TABLE 语句添加至少其中一个外键。

  • 被引用键必须是唯一的。如果外键的被引用列与被引用表的主键列匹配,则 Spanner 使用被引用表的 PRIMARY KEY。如果 Spanner 无法使用被引用表的主键,则会在被引用列上创建 UNIQUE NULL_FILTERED INDEX

  • 外键不会使用您创建的二级索引。而是会创建自己的支持性索引。支持性索引可用于查询评估,包括明确的 force_index 指令。您可以通过 Spanner 的 INFORMATION_SCHEMA 查询支持性索引的名称。如需了解详情,请参阅支持性索引

外键的类型

外键分为两种:强制性外键和信息性外键。强制外键是默认设置,可强制执行参照完整性。信息外键不会强制执行参照完整性,最适合用于声明用于查询优化的预期逻辑数据模型。如需了解详情,请参阅以下强制性信息性外键部分以及外键类型对比表。

强制性外键

强制外键(Spanner 中的默认外键类型)可强制执行参照完整性。由于强制性外键会强制执行参照完整性,因此会导致尝试执行以下操作失败:

  • 向引用表添加行时,如果外键值在被引用表中不存在,则会失败。

  • 从被引用表中删除被引用表中的行引用的行会失败。

所有 PostgreSQL 外键都会强制执行。默认情况下,系统会强制执行 GoogleSQL 外键。由于外键默认会强制执行,因此使用 ENFORCED 关键字指定强制执行 GoogleSQL 外键是可选操作。

信息外键

信息外键用于声明预期的逻辑数据模型,以便进行查询优化。虽然对于信息外键,引用的表键必须是唯一的,但系统不会强制执行引用完整性。如果您想在使用信息外键时选择性验证参照完整性,则需要在客户端管理验证逻辑。如需了解详情,请参阅使用信息外键

使用 NOT ENFORCED 关键字指定 GoogleSQL 外键是信息性外键。PostgreSQL 不支持信息外键。

外键类型比较

强制性信息性政策都有各自的好处。以下部分比较了两种外键,并提供了一些最佳实践。

外键的概要差异

概括而言,强制性外键和信息性外键之间的一些区别如下:

  • 违规处置。强制外键会验证写入操作并保证参照完整性。信息外键不会验证或保证参照完整性。

  • 存储。强制外键可能需要为受约束表的后备索引额外分配存储空间。

  • 写入吞吐量。与信息外键相比,强制性外键在写入路径中可能会产生更多的开销。

  • 查询优化。这两种类型的外键都可以用于查询优化。当允许优化器使用信息外键时,如果数据与信息外键关系不匹配(例如,如果某些受限键在引用表中没有匹配的引用键),查询结果可能不会反映实际数据。

外键差异表

下表列出了强制性外键和信息性外键之间的详细区别:

强制性外键 信息外键
关键字 ENFORCED NOT ENFORCED
受 GoogleSQL 支持 可以。默认情况下,GoogleSQL 中的外键会强制执行。 可以。
受 PostgreSQL 支持 可以。PostgreSQL 中的外键只能强制执行。 单元编号
存储 强制性外键最多需要存储两个支持性索引。 信息外键最多需要存储一个支持性索引。
在需要时,在引用的表列上创建后备索引 可以。 可以。
在需要时,在引用表列上创建后备索引 可以。 单元编号
外键操作支持 可以。 单元编号
验证和强制执行参照完整性 可以。 不需要。不进行验证可以提高写入性能,但当使用信息外键进行查询优化时,可能会影响查询结果。您可以使用客户端验证或强制外键来确保参照完整性。

选择要使用的外键类型

您可以参考以下准则来确定要使用哪种外键类型:

我们建议您先从强制外键开始。强制外键可确保数据和逻辑模型始终保持一致。强制性外键是推荐的选项,除非它们不适用于您的用例。

如果以下所有条件都成立,我们建议您考虑使用信息外键:

  • 您希望在查询优化中使用由信息外键描述的逻辑数据模型。

  • 维护严格的参照完整性不切实际,或者会严重影响性能。以下是一些何时可以考虑使用信息外键的示例:

    • 您的上游数据源采用最终一致性模型。在这种情况下,在源系统中进行的更新可能不会立即反映在 Spanner 中。由于更新可能不会立即生效,因此外键关系中可能会出现短暂的不一致性。

    • 您的数据包含大量引用关系的引用行。对这些行的更新可能会使用大量资源,因为 Spanner 必须验证(在某些情况下,还必须删除)与维护参照完整性相关的所有行。在这种情况下,更新可能会影响 Spanner 性能并降低并发事务速度。

  • 您的应用可以处理潜在的数据不一致性及其对查询结果的影响。

使用信息外键

以下主题仅供参考外键。如需了解同时适用于信息性外键和强制性外键的主题,请参阅以下内容:

创建包含信息外键的新表

您可以使用 DDL 语句在 Spanner 数据库中创建和移除信息外键 。您可以使用 CREATE TABLE 语句将外键添加到新表中。同样,您也可以使用 ALTER TABLE 语句在现有表中添加或移除外键。

以下示例使用 GoogleSQL 创建了一个包含信息外键的新表。PostgreSQL 不支持信息外键。

GoogleSQL

CREATE TABLE Customers (
  CustomerId INT64 NOT NULL,
  CustomerName STRING(MAX) NOT NULL,
) PRIMARY KEY(CustomerId);

CREATE TABLE Orders (
  OrderId INT64 NOT NULL,
  CustomerId INT64 NOT NULL,
  Quantity INT64 NOT NULL,
  ProductId INT64 NOT NULL,
  CONSTRAINT FK_CustomerOrder FOREIGN KEY (CustomerId)
   REFERENCES Customers (CustomerId) NOT ENFORCED
 ) PRIMARY KEY (OrderId);

PostgreSQL

Not Supported

如需查看如何创建和管理外键的更多示例,请参阅创建和管理外键关系。如需详细了解 DDL 语句,请参阅 DDL 参考文档

使用信息外键进行查询优化

强制外键信息外键都可以供查询优化器使用,以提高查询性能。使用信息外键可让您利用经过优化的查询计划,而无需承担严格强制执行参照完整性的开销。

如果您启用查询优化器来使用信息外键信息,请务必了解优化的正确性取决于数据是否与信息外键所描述的逻辑模型一致。如果存在不一致之处,则查询结果可能无法反映实际数据。例如,如果受限列中的值在引用的列中没有匹配的值,则会出现不一致的情况。

默认情况下,查询优化器使用 NOT ENFORCED 外键。如需更改此设置,请将数据库选项 use_unenforced_foreign_key_for_query_optimization 设置为 false。以下 GoogleSQL 示例演示了这一点(PostgreSQL 中不提供信息外键):

SET DATABASE OPTIONS (
    use_unenforced_foreign_key_for_query_optimization = false
);

布尔值查询语句提示 @{use_unenforced_foreign_key} 会按查询替换数据库选项,该选项用于控制优化器是否使用 NOT ENFORCED 外键。在对意外的查询结果进行问题排查时,停用此提示或数据库选项会很有用。以下展示了如何使用 @{use_unenforced_foreign_key}

@{use_unenforced_foreign_key=false} SELECT Orders.CustomerId
    FROM Orders
    INNER JOIN Customers ON Customers.CustomerId = Orders.CustomerId;

使用强制外键

以下主题仅适用于强制性外键。如需了解同时适用于信息性外键和强制性外键的主题,请参阅以下内容:

创建具有强制性外键的新表

您可以使用 DDL 在 Spanner 数据库中创建和移除外键,并对其进行强制执行。您可以使用 CREATE TABLE 语句将外键添加到新表中。同样,您也可以使用 ALTER TABLE 语句在现有表中添加或移除外键。

您可以使用 DDL 在 Spanner 数据库中创建和移除外键。您可以使用 CREATE TABLE 语句将外键添加到新表中。同样,您也可以使用 ALTER TABLE 语句在现有表中添加或移除外键。

以下是使用强制外键创建新表的示例。

GoogleSQL

CREATE TABLE Customers (
CustomerId INT64 NOT NULL,
CustomerName STRING(MAX) NOT NULL,
) PRIMARY KEY(CustomerId);

CREATE TABLE Orders (
OrderId INT64 NOT NULL,
CustomerId INT64 NOT NULL,
Quantity INT64 NOT NULL,
ProductId INT64 NOT NULL,
CONSTRAINT FK_CustomerOrder FOREIGN KEY (CustomerId)
  REFERENCES Customers (CustomerId) ENFORCED
) PRIMARY KEY (OrderId);

PostgreSQL

CREATE TABLE Customers (
CustomerId bigint NOT NULL,
CustomerName character varying(1024) NOT NULL,
PRIMARY KEY(CustomerId)
);

CREATE TABLE Orders (
OrderId BIGINT NOT NULL,
CustomerId BIGINT NOT NULL,
Quantity BIGINT NOT NULL,
ProductId BIGINT NOT NULL,
CONSTRAINT FK_CustomerOrder FOREIGN KEY (CustomerId)
  REFERENCES Customers (CustomerId),
PRIMARY KEY (OrderId)
);

如需查看如何创建和管理外键的更多示例,请参阅创建和管理外键关系

外键操作

只能对强制性外键定义外键操作。

外键操作用于控制在被引用列被删除或更新时受约束列会发生什么情况。Spanner 支持使用 ON DELETE CASCADE 操作。使用外键 ON DELETE CASCADE 操作时,如果您删除包含引用外键的行,则在同一事务中,系统也会删除引用该键的所有行。

使用 DDL 创建数据库时,您可以通过操作添加外键。使用 CREATE TABLE 语句可通过操作将外键添加到新表中。同样,您可以使用 ALTER TABLE 语句向现有表添加外键操作或移除外键操作。以下示例展示了如何使用外键操作创建新表。

GoogleSQL

CREATE TABLE ShoppingCarts (
CartId INT64 NOT NULL,
CustomerId INT64 NOT NULL,
CustomerName STRING(MAX) NOT NULL,
CONSTRAINT FKShoppingCartsCustomers FOREIGN KEY(CustomerId, CustomerName)
  REFERENCES Customers(CustomerId, CustomerName) ON DELETE CASCADE,
) PRIMARY KEY(CartId);

PostgreSQL

CREATE TABLE ShoppingCarts (
CartId bigint NOT NULL,
CustomerId bigint NOT NULL,
CustomerName character varying(1024) NOT NULL,
PRIMARY KEY(CartId),
CONSTRAINT fkshoppingcartscustomers FOREIGN KEY (CustomerId, CustomerName)
  REFERENCES Customers(CustomerId, CustomerName) ON DELETE CASCADE
);

下面列出了 Spanner 中外键操作的特性。

  • 外键操作为 ON DELETE CASCADEON DELETE NO ACTION

  • 您可以查询 INFORMATION_SCHEMA,以查找具有操作的外键约束条件。

  • 不支持对现有外键约束条件添加外键操作。您必须使用操作添加新的外键约束条件。

约束条件验证

约束条件验证仅适用于强制性外键。

在提交事务时或在写入影响对事务中的后续操作可见时,Spanner 会验证强制执行的外键约束条件。

插入到引用列中的值将与被引用表和被引用列的值进行匹配。系统不会检查具有 NULL 引用值的行,即可以将这些行添加到引用表中。

在尝试使用 DML 语句或 API 更新数据时,Spanner 会验证所有适用的强制外键引用限制条件。如果有任何限制条件无效,则所有待更改项都将回滚。

每个 DML 语句完成之后立即进行验证。例如,您必须在插入引用行之前先插入被引用的行。使用 Mutation API 时,Mutation 会进行缓冲,直到提交事务为止。 强制执行的外键验证将推迟到提交事务之后进行。在这种情况下,可以先插入引用行。

将评估每个事务的修改是否会影响强制性外键约束条件。此类评估可能需要向服务器发送额外请求。支持性索引还需要额外的处理时间来评估事务修改并维护索引。此外,每个索引都需要额外的存储空间。

长时间运行的删除级联操作

当您从被引用表中删除行时,Spanner 必须删除引用被删除行的引用表中的所有行。这可能会导致级联效应,即单次删除操作会导致数千次其他删除操作。向表中添加带有删除级联操作的外键约束条件,或创建带有删除级联操作的外键约束条件的表,可能会降低删除操作的速度。

外键删除级联操作超出变更限制

使用外键删除级联删除大量记录可能会影响性能。这是因为,每个被删除的记录都会触发与其相关的所有记录的删除。如果您需要使用外键删除级联删除大量记录,请先从子表中显式删除行,然后再从父表中删除行。这可防止由于更改限制而导致事务失败。

强制外键和表交错的比较

Spanner 的表交错是许多父子关系的理想选择,其中子表的主键包含父表的主键列。子行及其父行共用位置可以显著提高性能。

外键是一种较通用的父子解决方案,并解决了其他用例。外键不限于主键列,而表可以具有多个外键关系,二者在某些关系中可以作为父键,而在其他关系中可以作为子键。但是,外键关系不会隐含表在存储层中共处同一位置的关系。

下面的示例使用了以下定义的 Orders 表:

采用外键的数据库架构

图 3.采用强制外键的数据库架构示意图

图 3 中的设计存在一些限制。例如,每个订单只能包含一个订购商品。

假设您的客户希望每笔订单能够订购多件商品。您可以通过引入 OrderItems 表来增强设计,该表包含客户订购的每件商品。您可以引入另一个强制性外键来表示 OrdersOrderItems 之间新的一对多关系。不过,您也知道,您经常需要对各个订单及其各自的订购商品运行查询。由于此类数据共用空间可以提高性能,因此您需要使用 Spanner 的表交错功能创建父子关系。

下面是如何定义与 Orders 交错的 OrderItems 表。

GoogleSQL

CREATE TABLE Products (
ProductId INT64 NOT NULL,
Name STRING(256) NOT NULL,
Price FLOAT64
) PRIMARY KEY(ProductId);

CREATE TABLE OrderItems (
OrderId INT64 NOT NULL,
ProductId INT64 NOT NULL,
Quantity INT64 NOT NULL,
FOREIGN KEY (ProductId) REFERENCES Products (ProductId)
) PRIMARY KEY (OrderId, ProductId),
INTERLEAVE IN PARENT Orders ON DELETE CASCADE;

PostgreSQL

CREATE TABLE Products (
ProductId BIGINT NOT NULL,
Name varchar(256) NOT NULL,
Price float8,
PRIMARY KEY(ProductId)
);

CREATE TABLE OrderItems (
OrderId BIGINT NOT NULL,
ProductId BIGINT NOT NULL,
Quantity BIGINT NOT NULL,
FOREIGN KEY (ProductId) REFERENCES Products (ProductId),
PRIMARY KEY (OrderId, ProductId)
) INTERLEAVE IN PARENT Orders ON DELETE CASCADE;

图 4 直观展示了更新后的数据库架构,因为引入了这个新表 OrderItemsOrders 交错。在这里,您还可以看到这两个表之间的一对多关系。

数据库架构显示 Orders 与新交错的 OrderItems 表之间的一对多关系

图 4. 添加交错的 OrderItems 表

在此配置中,您可以在每个订单中拥有多个 OrderItems 条目,每个订单的 OrderItems 条目是交错的,因此与这些订单位于同一位置。通过这种方式进行物理交错 OrdersOrderItems 可以提高性能,有效地预连接表并允许您一起访问相关的行,同时最小化磁盘访问。例如,Spanner 可以在本地通过主键执行联接,这样做可最大限度减少磁盘访问次数和网络流量。

如果事务中的更改数量超过 8 万,则事务将失败。这种大规模级联删除适用于具有“交错在父级中”关系的表,但不适用于具有外键关系的表。如果您有外键关系,并且需要删除大量行,则应先从子表中明确删除这些行。

如果用户表与另一个表存在外键关系,并且从引用表中删除一行会触发数百万行的删除,则您应在架构设计中使用“在父表中交错”的删除级联操作。

对照表

下表汇总了强制性外键与表交错的比较方式。您可以根据这些信息来确定什么方式适合您的设计。

父子关系类型 表交错 强制性外键
可以使用主键
可以使用非主键列
支持的父数据数量 0 .. 1 0 .. N
同时存储父数据和子数据
支持级联删除
Null 匹配模式 如果所有引用值与被引用值没有区别,则传递。
Null 值与 Null 值没有区别,但 Null 值与非 Null 值有区别。
如果有任何引用值为 Null,则传递。
如果所有引用值均为非 Null 值,且被引用表中有值等于引用值的行,则传递。
如果找不到匹配的行,则失败。
强制执行时间 使用 Mutation API 时的操作。
使用 DML 时的每个语句。
使用变更 API 时的每项事务。
使用 DML 时的每个语句。
可以移除 不可以。除非删除整个子表,否则一旦创建表交错就无法移除。

支持性索引

外键不使用用户创建的索引。而是创建自己的支持性索引。强制性信息性外键在 Spanner 中创建支持性索引的方式不同:

  • 对于强制性外键,Spanner 可以为每个外键创建最多两个二级支持性索引,一个用于引用列,另一个用于被引用列。

  • 对于信息外键,Spanner 可以根据需要为被引用列创建最多一个支持性索引。信息外键不会为引用列创建支持性索引。

对于强制性外键和信息性外键,外键通常会引用被引用表的主键,因此通常不需要为被引用表创建索引。因此,信息外键通常没有任何支持性索引。如有需要,为被引用表创建的后备索引为 UNIQUE NULL_FILTERED 索引。如果任何现有数据违反了索引的唯一性限制条件,则无法创建外键。

信息外键没有引用表的支持性索引。对于强制性外键,引用表的支持性索引为 NULL_FILTERED

如果两个或更多外键需要相同的支持性索引,则 Spanner 会为每个键创建一个索引。丢弃使用该索引的外键,将同时丢弃支持性索引。您无法更改或丢弃支持性索引。

Spanner 使用每个数据库的信息架构来存储与后备索引相关的元数据。INFORMATION_SCHEMA.INDEXESSPANNER_IS_MANAGED 值为 true 的行用于描述后备索引。

除了直接调用信息架构的 SQL 查询之外,Google Cloud 控制台不会显示与数据库的后备索引相关的任何信息。

长时间运行架构更改

向现有表中添加强制外键或使用外键创建新表可能会导致产生长时间运行的操作。对于新表,只有在长时间运行的操作完成之后,才能写入该表。

下表显示了当强制性外键和信息性外键位于新表或现有表中时,Spanner 中会发生的情况:

表类型 强制性外键 信息外键
Spanner 会根据需要为每个外键回填被引用索引。 Spanner 会根据需要为每个外键回填被引用索引。
现有 Spanner 会根据需要回填引用索引和被引用索引。Spanner 还会验证表中的现有数据,以确保其符合外键的参照完整性限制条件。如果有任何数据无效,架构更改将失败。 Spanner 会根据需要回填被引用的索引,但不会验证表中现有的数据。

不支持:

  • 向现有强制性外键约束条件添加外键操作。
  • 更改现有外键的强制执行。

对于这两种情况,我们建议您改为执行以下操作:

  1. 添加包含所需操作或违规处置措施的新约束条件。
  2. 删除旧约束条件。

添加新约束条件并舍弃旧约束条件可防止出现长时间运行的更改约束条件操作问题。例如,假设您想对现有外键添加 DELETE CASCADE 操作。使用 ON DELETE CASCADE 操作创建新外键后,这两个约束条件的效果就是 DELETE CASCADE 操作。然后,您可以安全地删除旧约束条件。

如果其他外键约束条件未使用这些索引,则丢弃约束条件可能会导致丢弃外键支持性索引。因此,如果您先删除旧限制条件,然后通过操作添加相同的外键限制条件,可能会导致长时间运行的操作,例如回填索引、验证唯一索引约束条件或验证外键参照约束条件。

您可以查询 INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS.SPANNER_STATE 以检查外键创建状态。

后续步骤