外键

本主题介绍了 Cloud Spanner 中的外键,以及如何使用这些键在数据库解决方案中实施参照完整性。

简介

外键允许您定义表之间的关系。Cloud Spanner 可确保保持这些关系的数据完整性。

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

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

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

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

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

  • 客户下了订单

  • 下了某商品的订单

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

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

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

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

使用外键实施参照完整性

我们再次看一下订单处理示例,如图 2 所示,在设计中添加了更多细节。

采用外键的数据库架构

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

现在设计为每个表中都显示了列名称和类型。Orders 表还定义了两个外键关系。FK_CustomerOrder 可确保 Orders 中所有行都具有有效的 CustomerIDFK_ProductOrder 外键可确保 Orders 表中的所有 ProductID 值都有效。下表将这些限制条件与我们希望强制执行的实际规则一一对应。

外键名称 限制 实际说明
FK_CustomerOrder 确保 Orders 中所有行都具有有效 CustomerID 有效客户下了订单
FK_ProductOrder 确保 Orders 中所有行都具有有效 ProductID 对有效商品下了订单

Cloud Spanner 无法执行在 Orders 表中尝试插入或更新行的任何事务,该表具有的 CustomerIDProductIDCustomersProducts 表中找不到。此外,它也无法执行在 CustomersProducts 表中尝试更新或删除行的事务,从而导致 Orders 表中的数据失效。如需详细了解 Cloud Spanner 如何验证限制条件,请参阅下面的事务限制条件验证

定义外键

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

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)
) PRIMARY KEY (OrderID);

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

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

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

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

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

  • 引用列数和被引用列数必须相同。顺序很重要。第一个引用列引用第一个被引用列,第二个引用列引用第二个被引用列,以此类推。

  • 引用列及其被引用的对应项目的类型必须相同。这些列还必须可编入索引。

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

  • 数组列不受支持。

  • 不支持 JSON 列。

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

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

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

  • Cloud Spanner 可能还可以使用引用表的主键,但这种情况并不常见。如果不是,则 Cloud Spanner 会在引用列上创建 NULL_FILTERED INDEX

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

长时间运行架构更改

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

使用外键创建新表时,Cloud Spanner 需要根据需要为每个外键回填被引用索引。

向现有表添加外键时,Cloud Spanner 需要根据需要回填引用索引和被引用索引。此外,Cloud Spanner 还会验证表中的现有数据,以确保其符合外键的参照完整性限制条件。如果有任何限制条件无效,架构更改将失败。

如果由于违反 UNIQUE 限制条件而无法创建被引用索引,则上述架构更改都可能会失败。

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

事务的限制条件验证

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

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

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

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

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

支持性索引

外键不使用用户创建的索引。它们会创建自己的支持性索引。

Cloud Spanner 可以为每个外键创建最多两个二级支持性索引,一个用于引用列,另一个用于被引用列。但是,外键通常会引用被引用表的主键,因此通常不需要被引用表上的第二个索引。

引用的表的支持性索引是 UNIQUE NULL_FILTERED 索引;如果任何现有数据违反了索引的唯一性限制条件,则无法创建外键。引用表的支持性索引为 NULL_FILTERED

如果两个或更多外键需要相同的支持性索引,则 Cloud Spanner 将为所有这些键只创建一个索引。丢弃使用该索引的外键,将同时丢弃支持性索引。用户无法更改或丢弃支持性索引。

外键支持性索引可用于查询评估。您可以通过 Cloud Spanner 的 INFORMATION_SCHEMA 查询其名称。

外键和表交错的比较

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

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

我们来看一个示例,使用之前在本主题中讨论过的订单处理架构。回想一下,我们的 Orders 表的定义如下:

采用外键的数据库架构

图 3:采用外键的数据库架构示意图

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

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

下面是我们对 OrderItems 表的定义(与 Orders 交错)。

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;

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

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

图 4:添加交错的 OrderItems 表

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

总结

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

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

后续步骤