架构和数据模型

数据模型总结

一个 Cloud Spanner 数据库可以包含一个或多个表。这些表与关系型数据库表类似,它们也是由行、列和值构成的,并且包含主键。Cloud Spanner 中的数据是强类型的:您必须为每个数据库定义一个架构,并且该架构必须指定每个表中每一列的数据类型。允许的数据类型包括标量和数组类型,数据类型中有更详细的介绍。您也可以在表上定义一个或多个二级索引

父子表关系

您可以通过两种方式在 Cloud Spanner 中定义父子关系:表交错外键

Cloud Spanner 的表交错是许多父子关系的理想选择,其中子表的主键包含父表的主键列。子行及其父行共用位置可以显著提高性能。例如,如果您有一个 Customers 表和一个 Invoices 表,并且您的应用经常为特定客户提取所有账单,那么您可以将 Invoices 定义为 Customers 的子表。这样做可声明两个逻辑上独立的表之间的数据存放区域关系:您指示 Cloud Spanner 以物理方式将一个或多个 Invoices 行与一个 Customers 行存储在一起。

如需详细了解交错,请参阅下面的创建交错表

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

如需详细了解外键及其与交错表的比较,请参阅外键概览

主键

如何指示 Cloud Spanner 要存储哪些 Invoices 行以及哪些 Customers 行呢?可以使用这些表的主键实现。每个表都必须有一个主键,并且该主键可以由该表的零列或多列组成。如果您将某个表声明为另一个表的子表,则父表的主键列必须是子表主键的前缀。这意味着,如果父表的主键由 N 列组成,则其每个子表的主键也必须由相同的 N 列组成,顺序相同,并且以相同的列开始。

Cloud Spanner 会按主键值的排序顺序存储行,并在父行之间插入共享相同主键前缀的子行。这种沿着主键维度在父行之间插入子行的行为称为交错,而子表也称为交错表(如需查看交错行的图示,请参阅下面的创建交错表)。

总而言之,Cloud Spanner 能以物理方式将相关表的行存储在一起。 下面的架构示例展示了此物理布局的外观。

选择主键

主键唯一标识表中的每一行。如果您想要更新或删除表中的现有行,则该表必须具有由一列或多列组成的主键(没有主键列的表只能有一行)。通常,您的应用已经有一个本身就适合用作主键的字段。例如,在上面的 Customers 表示例中,可能有一个应用提供的 CustomerId 充当主键。在其他情况下,您可能需要在插入行时生成主键,如您生成的唯一 INT64 值。

无论哪种情况,都请务必小心谨慎,千万不要在选择主键时形成热点。例如,如果您插入一些记录,而这些记录将单调递增的整数用作键,那么您将始终在键空间末尾进插入记录。这种情况是不理想的,因为 Cloud Spanner 会按照键范围划分服务器之间的数据,这意味着,您的插入操作将集中于单个服务器,从而形成一个热点。可利用一些方法将负载分散到多个服务器上,从而避免热点:

数据库分片

您可以在表之间定义最多七层的父子关系层次结构,意味着您可以将七个逻辑上独立的表的行存储在一起。如果表中的数据量比较少,那么您的数据库或许可以由单个 Cloud Spanner 服务器处理。然而,当相关表不断增长,开始达到单个服务器的资源限制时,会发生什么情况呢?Cloud Spanner 是一个分布式数据库,这意味着随着数据库不断增长,Cloud Spanner 会将数据划分为称作“分片”的区块,各个分片可以彼此独立移动并被分配给不同物理位置的多个服务器。分片是指顶级(即非交错)表中的一系列行,这些行按主键进行排序。这一系列行的开始和结束键称为“分片边界”。Cloud Spanner 会自动添加和移除分片边界,这样做会改变数据库中的分片数量。

Cloud Spanner 基于负载对数据进行分片:当检测到某个分片中许多键之间的读取或写入负载较高时,Cloud Spanner 就会自动添加分片边界。您可以在一定程度上控制数据的分片方式,因为 Cloud Spanner 只能在位于层次结构根目录层的表(即未在父表中交错的表)的行之间绘制分片边界。此外,由于交错表的行按照主键的排序顺序与其父表中共享相同主键前缀的行存储在一起,不能从其父表中的相应行分割交错表的行(有关交错行的图示,请参阅创建交错表的层次结构)。因此,利用您定义的父子表关系以及您为相关表的行设置的主键值,您能够控制数据在后台的分片方式

基于负载进行分片

我们来看一个 Cloud Spanner 如何基于负载进行分片从而缓解读取热点的示例,假设您的数据库中有一个表,其中有 10 行的读取频率高于表中的所有其他行。只要该表位于数据库层次结构的根目录层(换句话说,它不是交错表),Cloud Spanner 就可以在这 10 行中的每一行之间添加分片边界,以便每一行分别由不同的服务器处理,这样可避免这些行的所有读取操作消耗单台服务器的资源。

通常情况下,如果您遵循架构设计的最佳做法,Cloud Spanner 就可以缓解读取非交叉表的行时所产生的热点。这样,读取吞吐量应该可以每隔几分钟便提高一次,直到您用尽实例中的资源或无法再添加新的分片边界(因为您的分片只包括单行及其交错的子行)。

架构示例

下面的架构示例演示了如何创建具有和不具有父子关系的 Cloud Spanner 表,并说明了相应的数据物理布局。

创建表

假设您正在创建一个音乐应用,并且您需要一个简单表来存储歌手数据行:

具有 5 行 4 列的 Singers 表。

简单 Singers 表中的行的逻辑视图。主键列显示在粗线的左侧。

请注意,该表包含一个主键列 SingerId,其显示在粗线的左侧,并且该表是由行、列和值构成的。

您可以使用 Cloud Spanner 架构来定义表,如下所示:

CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

请注意有关示例架构的以下事项:

  • Singers 是位于数据库层次结构根目录层的表(因为它没有被定义为另一个表的子表)。
  • 主键列通常带有 NOT NULL 注释(但是,如果希望在键列中允许 NULL 值,则可以忽略此注释;请参阅键列了解详情)。
  • 未包含在主键中的列称为非键列,它们可以具有可选的 NOT NULL 注释。
  • 必须为使用 STRINGBYTES 类型的列定义一个长度,该长度表示可以在该字段中存储的最多 Unicode 字符数。(如需了解详情,请参阅标量数据类型。)

Singers 表中行的物理布局是什么样的?下图显示了 Singers 表的行,它们是按连续主键(即主键的排序顺序)进行存储的,也就是说,首先是“Singers(1)”,然后是“Singers(2)”,以此类推,其中,“Singers(1)”表示 Singers 表中键为 1 的行。

按连续键顺序存储的示例表行。

Singers 表中行的物理布局,带有示例分片边界,导致分片由不同的服务器处理。

上图还说明了分片边界可能出现在 Singers 表的任何行之间,这是因为 Singers 位于数据库层次结构的根目录层。它还说明了 Singers(3)Singers(4) 键控的行之间有一个示例分片边界,系统会将生成的分片中的数据分配给不同服务器进行处理。这意味着,随着此表不断增长,Singers 数据的行可能存储在不同位置。

创建多个表

假设您现在想要将每个歌手的专辑相关的一些基本数据添加到音乐应用中:

具有 5 行 3 列的 Albums 表

Albums 表中行的逻辑视图。主键列显示在粗线的左侧

请注意,Albums 的主键由两列组成:SingerIdAlbumId,它们将每个专辑与其歌手相关联。以下示例架构在数据库层次结构的根目录层定义 AlbumsSingers 表,这使它们成为同级表:

-- Schema hierarchy:
-- + Singers (sibling table of Albums)
-- + Albums (sibling table of Singers)
CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

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

SingersAlbums 的行物理布局如图所示,首先,Albums 表的行按相应的连续主键进行存储,随后,Singers 表的行按相应的连续主键进行存储:

行的物理布局:Albums 和 Singers 行分别按键值顺序存储

Singers 和 Albums 表的行物理布局,这两个表均位于数据库层次结构的根目录层。

请注意,在上述架构中,Cloud Spanner 假定 SingersAlbums 表之间没有数据存放区域关系,因为这两个表都是顶级表。随着数据库不断增长,Cloud Spanner 可以在上面所示的任何行之间添加分片边界。这意味着,Albums 表行的分片结束位置可能不同于 Singers 表行,并且两个分片可以彼此独立移动。

根据您的应用的具体需求,可以让 Albums 数据位于不同于 Singers 数据的分片上。然而,如果您的应用经常需要检索有关给定歌手的所有专辑的信息,则应该将 Albums 创建为 Singers 的子表,这样做可沿着主键维度协同定位两个表中的行。下面的示例更详细地对此进行了介绍。

创建交错表

在设计音乐应用时,假设您意识到应用需要频繁访问 SingersAlbums 表中某特定主键的行(例如,每次访问行 Singers(1) 时,您还需要访问行 Albums(1, 1)Albums(1, 2))。换句话说,SingersAlbums 之间需要建立强大的数据存放区域关系。

您可以通过将 Albums 创建为 Singers 表的子表或“交错”表,来声明此数据存放区域关系。正如主键中所述,交错表是指您声明为另一个表的子表的表,目的是让子表的行与关联的父行实际存储在一起。如上所述,子表的主键前缀必须是父表的主键。

下面架构中的粗体行演示了如何将 Albums 创建为 Singers 的交错表。

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

有关此架构的注意事项:

  • SingerId 是子表 Albums 的主键的前缀,也是其父表 Singers 的主键。如果 SingersAlbums 处于层次结构的同一级别,则该前缀不是必需的;但是由于 Albums 被声明为 Singers 的子表,该前缀在此架构中是必需的。
  • ON DELETE CASCADE 注释表示,当父表中的行被删除时,此表中的子行(即所有以相同主键开头的行)也会自动被删除。如果子表没有此注释,或注释为 ON DELETE NO ACTION,则您必须先删除子行,然后才能删除父行。
  • 交错行首先按父表的行进行排序,然后按共享父表主键的子表的连续行进行排序,例如,依次按“Singers(1)”、“Albums(1, 1)”、“Albums(1, 2)”等排序。
  • 如果对此数据库进行分片,则每个歌手与其专辑数据的数据存放区域关系都将保留下来,因为只能在 Singers 表的行之间插入分片。
  • 在插入子行之前,父行必须已经存在。 父行可以已经存在于数据库中,也可以在将子行插入到同一事务中之前插入。

行的物理布局:Albums 行在 Singers 行之间交错

Singers 及其子表 Albums 的行物理布局。

创建交错表的层次结构

SingersAlbums 之间的父子关系可以扩展到更多的后代表。例如,您可以创建一个名为 Songs 的交错表作为 Albums 的子表,用于存储每个专辑的曲目清单:

具有 6 行 4 列的 Songs 表

Songs 表中行的逻辑视图。主键列显示在粗线的左侧

Songs 必须具有一个主键,并且该主键必须由层次结构中其上方表的所有主键(即 SingerIdAlbumId)组成。

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
--     + Songs (interleaved table, child table of Albums)
CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE TABLE Songs (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  TrackId      INT64 NOT NULL,
  SongName     STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId, TrackId),
  INTERLEAVE IN PARENT Albums ON DELETE CASCADE;

交错行的物理视图表明,歌手与其专辑和歌曲数据之间的数据存放区域关系将保留:

行的物理视图:Songs 在 Albums 中交错,后者在 Singers 之间交错

Singers、Albums 和 Songs 表的行物理布局,这些行形成交错表的层次结构。

总之,父表及其所有子表和后代表形成架构中的表层次结构。尽管层次结构中的每个表在逻辑上是独立的,但是通过物理方式交错排列表可以提高性能,这种方式可有效地预联接表,既让您能够访问相关行,又可最大限度地减少磁盘访问次数。

如果可能,请通过主键联接交错表中的数据。每个交错行都一定会通过物理方式与其父行一起存储在同一个分片中。因此,Cloud Spanner 可以在本地通过主键执行联接,这样做可最大限度减少磁盘访问次数和网络流量。在以下示例中,SingersAlbums 通过主键 SingerId 进行联接:

SELECT s.FirstName, a.AlbumTitle
FROM Singers AS s JOIN Albums AS a ON s.SingerId = a.SingerId;

不强制要求在 Cloud Spanner 中交错表,但是建议对于具有强大数据存放区域关系的表这样做。如果单行及其后代行的大小有可能会大于数 GB,请避免交错表。

键列

表的键不可更改;您无法在现有表中添加键列,也不能从现有表中移除键列。

存储 NULL

主键列可以定义为存储 NULL。如果希望将 NULL 存储在主键列中,请在架构中省略该列的 NOT NULL 子句。

以下示例在主键列 SingerId 中省略了 NOT NULL 子句。请注意,由于 SingerId 是主键,Singers 表中最多只能有一行在该列中存储 NULL

CREATE TABLE Singers (
  SingerId   INT64,
  FirstName  STRING(1024),
  LastName   STRING(1024),
) PRIMARY KEY (SingerId);

主键列可为 null 的属性必须在父表和子表声明之间匹配。在此示例中,不允许使用 Albums.SingerId INT64 NOT NULL。由于 Singers.SingerId 省略了 NOT NULL 子句,键声明也必须将其省略。

CREATE TABLE Singers (
  SingerId   INT64,
  FirstName  STRING(1024),
  LastName   STRING(1024),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,  -- NOT ALLOWED!
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

不允许的类型

以下项目的类型不能为 ARRAY

  • 表的键列。
  • 索引的键列。

设计多租户架构

如果您存储的数据属于不同客户,那么您可能需要提供多租户解决方案。例如,某个音乐服务可能想要分开存储每个唱片公司的数据。

经典多租户架构

设计多租户架构的经典方法是为每个客户创建一个单独的数据库。在此示例中,每个数据库都有自己的 Singers 表:

数据库 1:Ackworth Records
SingerId FirstName LastName
1MarcRichards
2CatalinaSmith
数据库 2:Cama Records
SingerId FirstName LastName
3MarcRichards
4GabrielWright
数据库 3:Eagan Records
SingerId FirstName LastName
1BenjaminMartinez
2HannahHarris

在 Cloud Spanner 中设计多租户架构的推荐方法是为每位客户使用不同的主键值。应在表中包含一个 CustomerId 键列或类似键列。如果您将 CustomerId 作为第一个键列,那么每位客户的数据都能具有良好的存放区域。Cloud Spanner 会根据节点上数据的大小和负载模式自动拆分这些数据。在此示例中,所有客户都位于一个 Singers 表中:

Cloud Spanner 多租户数据库
CustomerId SingerId FirstName LastName
11MarcRichards
12CatalinaSmith
23MarcRichards
24GabrielWright
31BenjaminMartinez
32HannahHarris

如果每个租户必须拥有单独的数据库,请注意以下限制:

  • 每个实例的数据库数量和每个数据库的表数量都有相应限制。取决于客户的具体数量,可能无法安排单独的数据库或表。
  • 添加新表和非交错索引可能需要很长时间。 如果您的架构设计依赖于添加新表和索引,那么您可能无法获得所需的性能。

如果想要创建单独的数据库,那么当您将表分布到不同数据库时,您成功的几率更大,因为采用这种方式,每个数据库每周的架构更改量较少

如果要为您的应用的每位客户创建单独的表和索引,请不要将所有表和索引放在同一个数据库中。相反,请将它们拆分到多个数据库中,以减轻创建大量索引带来的性能问题。每个数据库的表数量和索引数量也有限制。