本页面讨论了架构并引入了交错表,从而提高查询在父子关系中的表时的性能。
Cloud Spanner 数据库包含一个或多个表。表的结构是行和列。有一个或多个列被定义为表的主键,用于唯一标识每行。主键始终会编入索引,以便快速查找行,并且您可以对一个或多个列定义二级索引。
Cloud Spanner 中的数据是强类型的数据。您必须为每个数据库定义一个架构,并且该架构必须指定每个表中每一列的数据类型。数据类型包括标量和复杂类型,具体说明请参阅 Google 标准 SQL 中的数据类型(对于 Google 标准 SQL 方言数据库)和 PostgreSQL 数据类型(对于 PostgreSQL 方言数据库)。
父子表关系
在 Cloud Spanner 中定义父子关系的方法有两种:表交错和外键。
Cloud Spanner 的表交错非常适合许多父子关系。通过交错,Cloud Spanner 以物理方式将子行与存储中的父行存储在一起。位于同一位置可能会显著提升性能。例如,如果您有一个 Customers
表和一个 Invoices
表,并且您的应用经常为客户提取所有帐单,则可以将 Invoices
定义为 Customers
的交错子表。这样做可声明两个独立表之间的数据局部关系。您需要告知 Cloud Spanner 将一行或多行 Invoices
与一行 Customers
存储在一起。
如需将子表与父表相关联,您可以使用 DDL 将子表声明为在父项中交错,并将父表主键添加为子表复合主键的第一部分。如需详细了解交错,请参阅本主题后面的创建交错表。
外键是一种较通用的父子解决方案,并解决了其他用例。外键不限于主键列,而表可以具有多个外键关系,二者在某些关系中可以作为父键,而在其他关系中可以作为子键。但是,外键关系不隐含表在存储层中的共用位置关系。
Google 建议您选择将父子关系表示为交错表或外键,但不能同时表示为这两者。如需详细了解外键及其与交错表的比较,请参阅外键概览。
交错表中的主键
如何指示 Cloud Spanner 要存储哪些 Invoices
行以及哪些 Customers
行呢?为此,您可以使用这些表的主键。对于交错,每个表都必须有一个主键。如果您将某个表声明为另一个表的交错子表,则该表必须具有一个复合主键,该键包含父主键的所有组成部分,顺序相同,通常还包含一个或多个其他子表列。
Cloud Spanner 按主键值的排序顺序存储行,并在父行之间插入子行。如需了解本主题的交错行,请参阅创建交错表。
总而言之,Cloud Spanner 能以物理方式将相关表的行存储在一起。 架构示例展示了此物理布局的外观。
选择主键
主键唯一标识表中的每一行。如果您想要更新或删除表中的现有行,则该表必须具有由一列或多列组成的主键没有主键列的表只能有一行。只有 Google 标准 SQL 方言数据库可以具有没有主键的表。
通常,您的应用已经有一个本身就适合用作主键的字段。例如,对于 Customers
表,可能有一个应用提供的 CustomerId
充当主键。在其他情况下,您可能需要在插入行时生成主键。它通常是没有业务意义的唯一整数值(代理主键)。
无论哪种情况,都请务必小心谨慎,千万不要在选择主键时形成热点。例如,如果您插入一些记录,而这些记录将单调递增的整数用作键,那么您将始终在键空间末尾进插入记录。这种情况是不理想的,因为 Cloud Spanner 会按照键范围划分服务器之间的数据,这意味着,您的插入操作将集中于单个服务器,从而形成一个热点。可利用一些方法将负载分散到多个服务器上,从而避免热点:
- 对键进行哈希处理,并将其存储在一列中。使用哈希列(或同时使用哈希列和唯一键列)作为主键。
- 交换主键中列的顺序。
- 使用通用唯一标识符 (UUID)。 建议使用版本 4 UUID,因为它使用高位随机值。请勿使用将时间戳存储在高位中的 UUID 算法(如版本 1 UUID)。
- 对顺序值进行位反转。
根据主键添加二级索引
在某些情况下,基于主键添加二级索引可提高数据库使用量。如果您经常运行需要对表的主键进行逆向扫描的查询,尤其如此。
数据库分片
您可以定义最多 7 个层的交错父子关系的层次结构,这意味着您可以将 7 个独立的表的行存储在一起。如果表中数据的大小很小,则单个 Cloud Spanner 服务器可能会处理您的数据库。然而,当相关表不断增长,开始达到单个服务器的资源限制时,会发生什么情况呢?Cloud Spanner 是一个分布式数据库,这意味着随着数据库不断增长,Cloud Spanner 会将数据划分为称作“拆分”的区块。各个分片可以彼此独立移动并分配给不同服务器,这些服务器可以位于不同的物理位置。分片包含一系列连续的行。这一范围的开始和结束键称为“分片边界”。Cloud Spanner 会根据大小和负载自动添加和移除分片边界,而这会改变数据库中的分片数量。
基于负载进行分片
我们来看一个 Cloud Spanner 如何基于负载进行分片从而缓解读取热点的示例,假设您的数据库中有一个表,其中有 10 行的读取频率高于表中的所有其他行。Cloud Spanner 就可以在这 10 行中的每一行之间添加分片边界,以便每一行分别由不同的服务器处理,这样可避免这些行的所有读取操作消耗单台服务器的资源。
一般来说,如果您遵循架构设计最佳做法,则 Cloud Spanner 可以减少热点,使读取吞吐量应该每隔几分钟就改善性能,直到您耗尽实例中的资源或者运行时无法添加新分片边界(因为您有一个分片仅覆盖一行,没有交错子项)。
架构示例
以下架构示例展示了如何创建具有和不支持交错的父表和子表,并说明了相应的数据物理布局。
创建父表
假设您正在创建一个音乐应用,并且您需要一个简单表来存储歌手数据行:
请注意,该表包含一个主键列 SingerId
,它显示在粗体行左侧,并且按行和列整理。
您可以使用 Cloud Spanner 架构来定义表,如下所示:
Google 标准 SQL
CREATE TABLE Singers ( SingerId INT64 NOT NULL, FirstName STRING(1024), LastName STRING(1024), SingerInfo BYTES(MAX), ) PRIMARY KEY (SingerId);
PostgreSQL
CREATE TABLE singers ( singer_id BIGINT PRIMARY KEY, first_name VARCHAR(1024), last_name VARCHAR(1024), singer_info BYTEA );
请注意有关示例架构的以下事项:
Singers
是位于数据库层次结构根目录处的表(因为它没有被定义为另一个表的交错子项)。- 对于 Google 标准 SQL 方言数据库,主键列通常带有
NOT NULL
注解(但是,如果您希望在键列中允许NULL
值,则可以省略此注解)。如需了解详情,请参阅键列。 - 未包含在主键中的列称为非键列,它们可以具有可选的
NOT NULL
注释。 - 在 Google 标准 SQL 中使用
STRING
或BYTES
类型的列必须使用长度来定义,该长度表示字段中可以存储的 Unicode 字符数上限。长度规范对 PostgreSQLvarchar
和character varying
类型而言可选。如需了解详情,请参阅 Google 标准 SQL 方言数据库的标量数据类型以及 PostgreSQL 方言数据库的 PostgreSQL 数据类型。
Singers
表中行的物理布局是什么样的?下图显示了 Singers
表的行,即主键 ("Singers(1)) 和 "Singers(2)”等,其中“Singers(1)”表示 Singers 表中键为 1 的行。
上图说明了 Singers(3)
和 Singers(4)
键控的行之间的拆分边界示例,并将生成的拆分中的数据分配给不同的服务器。随着此表格的增长,Singers
数据行可以存储在不同位置。
创建父表和子表
假设您现在想要将每个歌手的专辑的一些基本数据添加到音乐应用中。
请注意,Albums
的主键由两列组成:SingerId
和 AlbumId
,它们将每个专辑与其歌手相关联。以下示例架构在数据库层次结构的根目录中定义 Albums
和 Singers
表,这使它们成为同级表。
-- Schema hierarchy: -- + Singers (sibling table of Albums) -- + Albums (sibling table of Singers)
Google 标准 SQL
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);
PostgreSQL
CREATE TABLE singers ( singer_id BIGINT PRIMARY KEY, first_name VARCHAR(1024), last_name VARCHAR(1024), singer_info BYTEA ); CREATE TABLE albums ( singer_id BIGINT, album_id BIGINT, album_title VARCHAR, PRIMARY KEY (singer_id, album_id) );
Singers
和 Albums
行的物理布局如下图所示,Albums
表的行按连续的主键存储,然后 Singers
行按连续的主键存储:
关于该架构的一个重要注意事项是,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)
Google 标准 SQL
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;
PostgreSQL
CREATE TABLE singers ( singer_id BIGINT PRIMARY KEY, first_name VARCHAR(1024), last_name VARCHAR(1024), singer_info BYTEA ); CREATE TABLE albums ( singer_id BIGINT, album_id BIGINT, album_title VARCHAR, PRIMARY KEY (singer_id, album_id) ) 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)”,依此类推。
- 如果此数据库进行拆分,将保留每个歌手与其专辑数据的数据局部关系,前提是
Singers
行及其所有Albums
行的大小始终低于拆分大小限制,并且其中任何Albums
行没有热点。 - 在插入子行之前,父行必须已经存在。 父行可以已经存在于数据库中,也可以在将子行插入到同一事务中之前插入。
创建交错表的层次结构
Singers
和 Albums
之间的父子关系可以扩展到更多的后代表。例如,您可以创建一个名为 Songs
的交错表作为 Albums
的子表,用于存储每个专辑的曲目清单:
Songs
必须具有一个主键,其中包含层次结构中位于其上方的所有表主键,即 SingerId
和 AlbumId
。
-- Schema hierarchy: -- + Singers -- + Albums (interleaved table, child table of Singers) -- + Songs (interleaved table, child table of Albums)
Google 标准 SQL
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;
PostgreSQL
CREATE TABLE singers ( singer_id BIGINT PRIMARY KEY, first_name VARCHAR(1024), last_name VARCHAR(1024), singer_info BYTEA ); CREATE TABLE albums ( singer_id BIGINT, album_id BIGINT, album_title VARCHAR, PRIMARY KEY (singer_id, album_id) ) INTERLEAVE IN PARENT singers ON DELETE CASCADE; CREATE TABLE songs ( singer_id BIGINT, album_id BIGINT, track_id BIGINT, song_name VARCHAR, PRIMARY KEY (singer_id, album_id, track_id) ) INTERLEAVE IN PARENT albums ON DELETE CASCADE;
下图显示了交错行的物理视图。
在此示例中,随着歌手数量增长,Cloud Spanner 会增加歌手之间的分片边界,以保留歌手与其专辑和歌曲数据之间的数据局部性。但是,如果歌词行及其子行的大小超出了拆分行数上限,或者在子行中检测到热点,Cloud Spanner 会尝试添加拆分边界,以隔离该热点行及其所有子行。
总之,父表及其所有子表和后代表形成架构中的表层次结构。虽然层次结构中的每个表在逻辑上都是独立的,但通过这种方式物理交错表可以提高性能,从而有效地预联接表,并同时访问相关行,同时最大限度地减少存储空间访问。
使用交错表联接
如果可能,请通过主键联接交错表中的数据。由于每个交错行通常与其父行一起存储在同一个分片中,因此 Cloud Spanner 可以在本地通过主键执行联接,从而最大限度地减少存储访问和网络流量。在以下示例中,Singers
和 Albums
通过主键 SingerId
进行联接。
Google 标准 SQL
SELECT s.FirstName, a.AlbumTitle FROM Singers AS s JOIN Albums AS a ON s.SingerId = a.SingerId;
PostgreSQL
SELECT s.first_name, a.album_title FROM singers AS s JOIN albums AS a ON s.singer_id = a.singer_id;
键列
表的键不可更改;您无法在现有表中添加键列,也不能从现有表中移除键列。
存储 NULL
在 Google 标准 SQL 中,如果要将 NULL 存储在主键列中,请在架构中省略此列的 NOT NULL
子句。(PostgreSQL 方言数据库不支持主键列中的 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 |
---|---|---|
1 | 小艾 | Trentor |
2 | Gabriel | Wright |
SingerId | FirstName | LastName |
---|---|---|
1 | Benjamin | Martinez |
2 | Hannah | Harris |
由架构管理的多租户
在 Cloud Spanner 中设计多租户的另一种方法是,将所有客户放在单个数据库中,并为每个客户使用不同的主键值。例如,您可以在表中包含一个 CustomerId
键列。如果您将 CustomerId
作为第一个键列,那么每位客户的数据都能具有良好的存放区域。然后,Cloud Spanner 可以有效地使用数据库分片,以根据数据大小和负载模式最大限度地提高性能。在以下示例中,所有客户都有一个 Singers
表:
CustomerId | SingerId | FirstName | LastName |
---|---|---|---|
1 | 1 | Marc | Richards |
1 | 2 | Catalina | Smith |
2 | 1 | 小艾 | Trentor |
2 | 2 | Gabriel | Wright |
3 | 1 | Benjamin | Martinez |
3 | 2 | Hannah | Harris |
如果每个租户必须拥有单独的数据库,请注意以下限制:
- 每个实例的数据库数量以及每个数据库的表数量和索引数量都有限制。可能无法有单独的数据库或表,具体取决于客户的数量。
- 添加新表和非交错索引可能需要很长时间。 如果您的架构设计依赖于添加新表和索引,那么您可能无法获得所需的性能。
如果想要创建单独的数据库,那么当您将表分布到不同数据库时,您成功的几率更大,因为采用这种方式,每个数据库每周的架构更改量较少。
如果要为您的应用的每位客户创建单独的表和索引,请不要将所有表和索引放在同一个数据库中。而是将它们拆分到多个数据库中,以缓解创建大量索引时出现的性能问题。
如需详细了解针对多租户的其他数据管理模式和应用设计,请参阅在 Cloud Spanner 中实现多租户