数据模型总结
一个 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 会按照键范围划分服务器之间的数据,这意味着,您的插入操作将集中于单个服务器,从而形成一个热点。可利用一些方法将负载分散到多个服务器上,从而避免热点:
- 对键进行哈希处理,并将其存储在一列中。使用哈希列(或同时使用哈希列和唯一键列)作为主键。
- 交换主键中列的顺序。
- 使用通用唯一标识符 (UUID)。 建议使用版本 4 UUID,因为它使用高位随机值。请勿使用将时间戳存储在高位中的 UUID 算法(如版本 1 UUID)。
- 对顺序值进行位反转。
数据库分片
您可以在表之间定义最多七层的父子关系层次结构,意味着您可以将七个逻辑上独立的表的行存储在一起。如果表中的数据量比较少,那么您的数据库或许可以由单个 Cloud Spanner 服务器处理。然而,当相关表不断增长,开始达到单个服务器的资源限制时,会发生什么情况呢?Cloud Spanner 是一个分布式数据库,这意味着随着数据库不断增长,Cloud Spanner 会将数据划分为称作“分片”的区块,各个分片可以彼此独立移动并被分配给不同物理位置的多个服务器。分片包含一系列连续的行。这一范围的开始和结束键称为“分片边界”。Cloud Spanner 会根据大小和/或负载自动添加和移除分片边界,这样做会改变数据库中的分片数量。
基于负载进行分片
我们来看一个 Cloud Spanner 如何基于负载进行分片从而缓解读取热点的示例,假设您的数据库中有一个表,其中有 10 行的读取频率高于表中的所有其他行。Cloud Spanner 就可以在这 10 行中的每一行之间添加分片边界,以便每一行分别由不同的服务器处理,这样可避免这些行的所有读取操作消耗单台服务器的资源。
一般来说,如果您遵循架构设计最佳做法,则 Cloud Spanner 可以减少热点,使读取吞吐量应该每隔几分钟就改善性能,直到您耗尽实例中的资源或者运行时无法添加新分片边界(因为您有一个分片仅覆盖一行,没有交错子项)。
架构示例
下面的架构示例演示了如何创建具有和不具有父子关系的 Cloud Spanner 表,并说明了相应的数据物理布局。
创建表
假设您正在创建一个音乐应用,并且您需要一个简单表来存储歌手数据行:
简单 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
注释。 - 必须为使用
STRING
或BYTES
类型的列定义一个长度,该长度表示可以在该字段中存储的最多 Unicode 字符数。(如需了解详情,请参阅标量数据类型。)
Singers
表中行的物理布局是什么样的?下图显示了 Singers
表的行,它们是按连续主键(即主键的排序顺序)进行存储的,也就是说,首先是“Singers(1)”,然后是“Singers(2)”,以此类推,其中,“Singers(1)”表示 Singers 表中键为 1 的行。
Singers 表中行的物理布局,带有示例分片边界,导致分片由不同的服务器处理。
下图说明了 Singers(3)
和 Singers(4)
键控的行之间有一个示例分片边界,系统会将生成的分片中的数据分配给不同服务器进行处理。这意味着,随着此表不断增长,Singers
数据的行可能存储在不同位置。
创建多个表
假设您现在想要将每个歌手的专辑相关的一些基本数据添加到音乐应用中:
Albums 表中行的逻辑视图。主键列显示在粗线的左侧
请注意,Albums
的主键由两列组成:SingerId
和 AlbumId
,它们将每个专辑与其歌手相关联。以下示例架构在数据库层次结构的根目录层定义 Albums
和 Singers
表,这使它们成为同级表:
-- 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);
Singers
和 Albums
的行物理布局如图所示,首先,Albums
表的行按相应的连续主键进行存储,随后,Singers
表的行按相应的连续主键进行存储:
Singers 和 Albums 表的行物理布局,这两个表均位于数据库层次结构的根目录层。
请注意,在上述架构中,Cloud Spanner 假定 Singers
和 Albums
表之间没有数据存放区域关系,因为这两个表都是顶级表。随着数据库不断增长,Cloud Spanner 可以在上面所示的任何行之间添加分片边界。这意味着,Albums
表行的分片结束位置可能不同于 Singers
表行,并且两个分片可以彼此独立移动。
根据您的应用的具体需求,可以让 Albums
数据位于不同于 Singers
数据的分片上。然而,如果您的应用经常需要检索有关给定歌手的所有专辑的信息,则应该将 Albums
创建为 Singers
的子表,这样做可沿着主键维度协同定位两个表中的行。下面的示例更详细地对此进行了介绍。
创建交错表
在设计音乐应用时,假设您意识到应用需要频繁访问 Singers
和 Albums
表中某特定主键的行(例如,每次访问行 Singers(1)
时,您还需要访问行 Albums(1, 1)
和 Albums(1, 2)
)。换句话说,Singers
和 Albums
之间需要建立强大的数据存放区域关系。
您可以通过将 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
的主键。如果Singers
和Albums
处于层次结构的同一级别,则该前缀不是必需的;但是由于Albums
被声明为Singers
的子表,该前缀在此架构中是必需的。ON DELETE CASCADE
注释表示,当父表中的行被删除时,此表中的子行(即所有以相同主键开头的行)也会自动被删除。如果子表没有此注释,或注释为ON DELETE NO ACTION
,则您必须先删除子行,然后才能删除父行。- 交错行首先按父表的行进行排序,然后按共享父表主键的子表的连续行进行排序,例如,依次按“Singers(1)”、“Albums(1, 1)”、“Albums(1, 2)”等排序。
- 如果对此数据库进行分片,只要歌手行及其所有
Albums
行的大小在 4 GB 以内,并且在这些Albums
行中没有热点,则每个歌手与其专辑数据的数据存放区域关系都将保留下来。 - 在插入子行之前,父行必须已经存在。 父行可以已经存在于数据库中,也可以在将子行插入到同一事务中之前插入。
Singers 及其子表 Albums 的行物理布局。
创建交错表的层次结构
Singers
和 Albums
之间的父子关系可以扩展到更多的后代表。例如,您可以创建一个名为 Songs
的交错表作为 Albums
的子表,用于存储每个专辑的曲目清单:
Songs 表中行的逻辑视图。主键列显示在粗线的左侧
Songs
必须具有一个主键,并且该主键必须由层次结构中其上方表的所有主键(即 SingerId
和 AlbumId
)组成。
-- 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;
下图表示交错行的物理视图。在此示例中,随着歌手数量增长,Cloud Spanner 会增加歌手之间的分片边界,以保留歌手与其专辑和歌曲数据之间的数据存放区域。但是,如果歌手行的大小及其子行大小超过 4 GB 限制,或者系统在子行中检测到热点,则 Cloud Spanner 将尝试向交错表添加分片边界以便将该热点行及其下面的所有子行隔离开。
以下是交错行的物理视图。除非单个歌手行及其所有子行的大小超过 4 GB 限制,或者系统在子行中检测到热点,否则 Cloud Spanner 建议在歌手之间添加分片边界以保留数据歌手与其专辑和歌曲数据之间的存放区域。
Singers、Albums 和 Songs 表的行物理布局,这些行形成交错表的层次结构。
总之,父表及其所有子表和后代表形成架构中的表层次结构。尽管层次结构中的每个表在逻辑上是独立的,但是通过物理方式交错排列表可以提高性能,这种方式可有效地预联接表,既让您能够访问相关行,又可最大限度地减少磁盘访问次数。
如果可能,请通过主键联接交错表中的数据。因为每个交错行通常与其父行存储在同一分片中,所以 Cloud Spanner 可以在本地通过主键执行联接,最大限度地减少磁盘访问和网络流量。在以下示例中,Singers
和 Albums
通过主键 SingerId
进行联接:
SELECT s.FirstName, a.AlbumTitle FROM Singers AS s JOIN Albums AS a ON s.SingerId = a.SingerId;
建议将 Cloud Spanner 中的交错表用于经常一起访问的一对多相关数据。
键列
表的键不可更改;您无法在现有表中添加键列,也不能从现有表中移除键列。
存储 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
表:
SingerId | FirstName | LastName |
---|---|---|
1 | Marc | Richards |
2 | Catalina | Smith |
SingerId | FirstName | LastName |
---|---|---|
3 | Marc | Richards |
4 | Gabriel | Wright |
SingerId | FirstName | LastName |
---|---|---|
1 | Benjamin | Martinez |
2 | Hannah | Harris |
对 Cloud Spanner 多租户使用架构数据管理模式
在 Cloud Spanner 中设计多租户的另一种方法是为每个客户使用不同的主键值。应在表中包含一个 CustomerId
键列或类似键列。如果您将 CustomerId
作为第一个键列,那么每位客户的数据都能具有良好的存放区域。Cloud Spanner 会根据节点上数据的大小和负载模式自动拆分这些数据。在此示例中,所有客户都位于一个 Singers
表中:
CustomerId | SingerId | FirstName | LastName |
---|---|---|---|
1 | 1 | Marc | Richards |
1 | 2 | Catalina | Smith |
2 | 3 | Marc | Richards |
2 | 4 | Gabriel | Wright |
3 | 1 | Benjamin | Martinez |
3 | 2 | Hannah | Harris |
如果每个租户必须拥有单独的数据库,请注意以下限制:
- 每个实例的数据库数量和每个数据库的表数量都有相应限制。取决于客户的具体数量,可能无法安排单独的数据库或表。
- 添加新表和非交错索引可能需要很长时间。 如果您的架构设计依赖于添加新表和索引,那么您可能无法获得所需的性能。
如果想要创建单独的数据库,那么当您将表分布到不同数据库时,您成功的几率更大,因为采用这种方式,每个数据库每周的架构更改量较少。
如果要为您的应用的每位客户创建单独的表和索引,请不要将所有表和索引放在同一个数据库中。相反,请将它们拆分到多个数据库中,以减轻创建大量索引带来的性能问题。每个数据库的表数量和索引数量也有限制。
如需详细了解针对多租户的其他数据管理模式和应用设计,请参阅在 Cloud Spanner 中实现多租户