架构概览

本页介绍了 Spanner 架构要求、如何使用架构创建分层关系,以及架构功能。它还引入了交错表,可在查询具有父子关系的表时提高查询性能。

架构是包含数据库对象(例如表、视图、索引和函数)的命名空间。您可以使用架构来整理对象、应用精细的访问权限控制特权,并避免命名冲突。您必须为 Spanner 中的每个数据库定义一个架构。

您还可以进一步对不同地理区域中数据库表内的行数据进行细分和存储。如需了解详情,请参阅地理位置分区概览

强类型数据

Spanner 中的数据是强类型的。数据类型包括标量和复杂类型,GoogleSQL 中的数据类型PostgreSQL 数据类型中对此进行了介绍。

选择主键

Spanner 数据库可以包含一个或多个表。表格以行和列的形式构建。表架构会将一个或多个表列定义为表的主键,用于唯一标识每行。主键始终会编入索引,以便快速查找行。如果您想要更新或删除表中的现有行,则该表必须具有主键。没有主键列的表只能有一行。只有 GoogleSQL 方言数据库中可以有没有主键的表。

通常,您的应用已经有一个本身就适合用作主键的字段。例如,对于 Customers 表,可能有一个应用提供的 CustomerId 充当主键。在其他情况下,您可能需要在插入行时生成主键。这通常是没有业务意义的唯一整数值(代理主键)。

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

父子表关系

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

Spanner 的表交错是许多父子关系的理想选择。通过交错,Spanner 会在存储空间中将子行与父行放置在一起。共存可以显著提高性能。例如,如果您有一个 Customers 表和一个 Invoices 表,并且您的应用经常为客户提取所有账单,那么您可以将 Invoices 定义为 Customers 的交错子表。这样做可声明两个独立表之间的数据存放区域关系。您指示 Spanner 将一个或多个 Invoices 行与一个 Customers 行存储在一起。

您可以使用 DDL 将子表与父表相关联,方法是将子表声明为在父表中交错,并将父表主键作为子表复合主键的第一个部分包含在内。如需详细了解交错,请参阅本页面后面的创建交错表

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

Google 建议您选择将父子关系表示为交错表或外键,但不能同时表示为这两者。如需详细了解外键及其与交错表的比较,请参阅外键概览

交错表中的主键

对于交错,每个表都必须具有主键。如果您将某个表声明为另一个表的交错子表,则该表必须具有复合主键,其中包含父表主键的所有组成部分(顺序相同),通常还包含一个或多个额外的子表列。

Spanner 会按主键值的排序顺序存储行,并在父行之间插入子行。如需查看交错行的图示,请参阅本页下文中的创建交错表

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

数据库分片

您可以定义最多七层的互交父子关系层次结构,这意味着您可以将七个独立表的行存储在一起。如果表中的数据量比较少,那么您的数据库或许可以由单个 Spanner 服务器处理。但是,当相关表不断增长,开始达到单个服务器的资源限制时,会发生什么情况呢?Spanner 是一个分布式数据库,这意味着随着数据库不断增长,Spanner 会将数据划分为称作“分块”的区块。各个分片可以彼此独立移动并被分配给可能位于不同物理位置的多个服务器。分块包含一系列连续的行。这一范围的开始和结束键称为“分块边界”。Spanner 会根据大小和负载自动添加和移除分片边界,这样做会改变数据库中的分片数量。

基于负载进行分片

我们来看一个 Spanner 如何基于负载进行分片从而缓解读取热点的示例,假设您的数据库中有一个表,其中有 10 行的读取频率高于表中的所有其他行。Spanner 就可以在这 10 行中的每一行之间添加分片边界,以便每一行分别由不同的服务器处理,这样可避免这些行的所有读取操作消耗单台服务器的资源。

一般而言,如果您遵循架构设计最佳实践,Spanner 可以缓解热点问题,因此读取吞吐量应该会每隔几分钟提高一次,直到实例中的资源达到饱和或遇到无法添加新分块边界的情况(因为您有一个分块仅涵盖一行,且没有交错的子行)。

命名架构

命名架构可帮助您将类似数据整理在一起。这有助于您在 Google Cloud 控制台中快速查找对象、应用权限,并避免命名冲突。

与其他数据库对象一样,命名架构也使用 DDL 进行管理。

借助 Spanner 命名架构,您可以使用完全限定名称 (FQN) 查询数据。借助 FQN,您可以组合使用架构名称和对象名称来标识数据库对象。例如,您可以为仓库业务部门创建一个名为 warehouse 的架构。使用此架构的表可能包括:productordercustomer information。或者,您也可以为履单业务部门创建一个名为 fulfillment 的架构。此架构还可以包含名为 productordercustomer information 的表。在第一个示例中,FQN 为 warehouse.product,在第二个示例中,FQN 为 fulfillment.product。这样可以避免在多个对象共用同一名称的情况下造成混淆。

CREATE SCHEMA DDL 中,表对象既有 FQN(例如 sales.customers),也有短名称(例如 sales)。

以下数据库对象支持命名架构:

  • TABLE
    • CREATE
    • INTERLEAVE IN [PARENT]
    • FOREIGN KEY
    • SYNONYM
  • VIEW
  • INDEX
  • FOREIGN KEY
  • SEQUENCE

如需详细了解如何使用命名架构,请参阅管理命名架构

将精细访问权限控制与命名架构搭配使用

借助命名架构,您可以向架构中的每个对象授予架构级访问权限。这适用于您授予访问权限时存在的架构对象。您必须授予对后续添加的对象的访问权限。

精细访问权限控制可限制对整组数据库对象(例如表、列和表中的行)的访问权限。

如需了解详情,请参阅向命名架构授予精细访问权限控制特权

架构示例

本部分中的架构示例演示了如何创建具有和不具有交错的父子表,并说明了相应的数据物理布局。

创建父级表

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

包含 5 行 4 列的 Singers 表

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

您可以使用以下 DDL 定义表:

GoogleSQL

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

PostgreSQL

CREATE TABLE singers (
singer_id   BIGINT PRIMARY KEY,
first_name  VARCHAR(1024),
last_name   VARCHAR(1024),
singer_info BYTEA
);

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

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

Singers 表中行的物理布局是什么样的?下图显示了 Singers 表的行,它们是按主键(“Singers(1)”和“Singers(2)”,其中括号中的数字是主键值)进行存储的。

按主键顺序存储的示例表行

上图说明了 Singers(3)Singers(4) 键控的行之间有一个示例分片边界,系统会将生成的分片中的数据分配给不同服务器进行处理。随着此表不断增长,Singers 数据的行可能存储在不同位置。

创建父级表和子表

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

包含 5 行 3 列的 Albums 表

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

-- Schema hierarchy:
-- + Singers (sibling table of Albums)
-- + Albums (sibling table of Singers)

GoogleSQL

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

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)
);

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

行的物理布局

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

根据您的应用的具体需求,可以让 Albums 数据位于不同于 Singers 数据的分片上。不过,由于需要协调对不同资源的读取和更新,因此这可能会导致性能下降。如果您的应用经常需要检索有关特定歌手的所有专辑的信息,则应该将 Albums 创建为 Singers 的交错子表,这样做可沿着主键维度协同定位两个表中的行。下面的示例更详细地对此进行了介绍。

创建交错表

交错表是指您声明为另一个表的交错子表的表,目的是让子表的行与关联的父行实际存储在一起。如前所述,父表主键必须是子表复合主键的第一部分。

表格交错后,便无法恢复。您无法撤消交错。而是需要重新创建表并将数据迁移到该表。

在设计音乐应用时,假设您发现该应用在访问 Singers 行时需要频繁访问 Albums 表中的行。例如,当您访问行 Singers(1) 时,还需要访问行 Albums(1, 1)Albums(1, 2)。在这种情况下,SingersAlbums 需要具有强大的数据存放区域关系。您可以通过将 Albums 创建为 Singers 表的交错子表,来声明此数据存放区域关系。

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)

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

GoogleSQL

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

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 的主键。
  • ON DELETE CASCADE 注解表示,当父表中的行被删除时,其子行也会自动删除。如果子表没有此注释,或注释为 ON DELETE NO ACTION,则您必须先删除子行,然后才能删除父行。
  • 交错行首先按父表的行进行排序,然后按共享父表主键的子表的连续行进行排序。例如,依次按“Singers(1)”“Albums(1, 1)”和“Albums(1, 2)”排序。
  • 如果对此数据库进行分片,只要 Singers 行及其所有 Albums 行的大小不超过分块大小限制,并且在这些 Albums 行中没有热点,则每个歌手与其专辑数据的数据存放区域关系都将保留下来。
  • 在插入子行之前,父行必须已经存在。 父行可以已经存在于数据库中,也可以在将子行插入到同一事务中之前插入。

Albums 行在 Singers 行之间交错

创建交错表的层次结构

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

具有 6 行 4 列的 Songs 表

Songs 必须具有一个主键,并且该主键必须包含层次结构中更高级别表的所有主键(即 SingerIdAlbumId)。

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
--     + Songs (interleaved table, child table of Albums)

GoogleSQL

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

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;

下图显示了交错行的物理视图。

Songs 在 Albums 中交错,后者在 Singers 之间交错

在此示例中,随着歌手数量增长,Spanner 会增加歌手之间的分片边界,以保留歌手与其专辑和歌曲数据之间的数据局部性。但是,如果歌手行及其子行的大小超过分块大小限制,或在子行中检测到热点,则 Spanner 会尝试添加分块边界以隔离该热点行及其下的所有子行。

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

使用交错表进行联接

如果可能,请通过主键联接交错表中的数据。因为每个交错行通常与其父行存储在同一分块中,所以 Spanner 可以在本地通过主键执行联接,最大限度地减少存储访问和网络流量。在以下示例中,SingersAlbums 通过主键 SingerId 进行联接。

GoogleSQL

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

在 GoogleSQL 中,如果您希望将 NULL 存储在主键列中,请在架构中省略该列的 NOT NULL 子句。(PostgreSQL 方言数据库不支持主键列中的 NULL。)

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

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

主键列可为 null 的属性必须在父表和子表声明之间匹配。在此示例中,不允许为 Albums.SingerId 列使用 NOT NULL,因为 Singers.SingerId 会忽略它。

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

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;

不允许的类型

以下列的类型不能为 ARRAY

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

设计多租户架构

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

经典多租户架构

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

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

由架构管理的多租户

在 Spanner 中设计多租户架构的另一种方法是,将所有客户的数据都存储在单个数据库的单个表中,并为每个客户使用不同的主键值。例如,您可以在表中添加 CustomerId 键列。如果您将 CustomerId 作为第一个键列,那么每位客户的数据都能具有良好的存放区域。然后,Spanner 可以有效地使用数据库分片,以根据数据大小和负载模式最大限度地提高性能。在以下示例中,所有客户都位于一个 Singers 表中:

Spanner 多租户数据库
CustomerId SingerId FirstName LastName
11MarcRichards
12CatalinaSmith
21小艾Trentor
22GabrielWright
31BenjaminMartinez
32HannahHarris

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

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

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

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

如需详细了解针对多租户的其他数据管理模式和应用设计,请参阅在 Spanner 中实现多租户