架构概览

本页面讨论 Spanner 架构要求以及如何使用架构 创建分层关系和架构功能。它还引入了 交错表,这可以提高在 父子关系

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

您还可以跨多种标准层级,进一步细分和存储数据库表中的行, 不同地理区域有关详情,请参阅 地理位置分区概览

强类型数据

Spanner 中的数据是强类型的数据。数据类型包括标量和复杂数据 类型,具体说明请参阅 GoogleSQL 中的数据类型PostgreSQL 数据类型

选择主键

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

通常,您的应用已经有一个很适合用作 主键。例如,对于 Customers 表,可能有 应用提供的 CustomerId,用作主键。在其他 则您可能需要在插入行时生成主键。这个 通常是没有商业意义的唯一整数值( 代理主键)。

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

父子表关系

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

Spanner 的表交错对于许多 父子关系通过交错,Spanner 在存储空间内将子行与父行共置。主机托管可大幅 提高性能。例如,如果您有一个 Customers 表和一个 Invoices 表,并且您的应用会频繁提取某个日期的所有账单 您可以将 Invoices 定义为 Customers.如此一来,您就声明了 两个独立的表。您告诉 Spanner 用于存储一行或多行 Invoices,其中包含一个 Customers 行。

若要将子表与父表相关联,您可以使用 DDL 在父表中交错的子表,以及通过添加父表 作为子表复合主键的第一部分。有关 有关交错的信息,请参阅本单元后面的创建交错表 页面。

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

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

交错表中的主键

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

Spanner 按主键值的排序顺序存储行, 在父行之间插入子行。查看交错行图示 (本页面后面部分的创建交错表)。

总而言之,Spanner 可以物理共置相关表的行。通过 架构示例展示了此物理布局的外观。

数据库分片

您最多可以定义 7 个交错父子关系的层次结构 深层,这意味着您可以将七个独立表的行共置。 如果表中的数据规模很小,则使用单个 Spanner 服务器或许可以处理您的数据库。但是,当您的 表不断增长并开始达到单个服务器的资源限制? Spanner 是一个分布式数据库,这意味着,作为您的 Spanner 会将您的数据划分为多个称为 “拆分”。各个分片可以彼此独立移动并被分配给可能位于不同物理位置的多个服务器。答 包含一系列连续的行。该范围的开始键和结束键分别为 称为“分块边界”。Spanner 会自动添加和移除 基于大小和负载的分块边界,这会改变 数据库

基于负载进行分片

作为示例,说明 Spanner 如何执行基于负载的拆分, 以缓解读取热点问题,假设您的数据库包含一个具有 10 行 读取频率比表中所有其他行的读取频率更高。 Spanner 可以在这 10 行中的每一行之间添加分块边界, 它们会分别由不同的服务器处理,而不是允许所有 读取这些行以消耗单个服务器的资源。

一般来说,如果您遵循架构设计最佳实践, Spanner 可以缓解热点问题, 应该每隔几分钟改进一次,直到资源中的 或遇到无法添加新的分块边界的情况(因为 则有一个分块,其中仅包含一行,没有交错子项)。

已命名的架构

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

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

Spanner 命名架构允许您使用完全限定名称 (FQN) 来查询数据。通过 FQN,您可以将架构名称和 对象名称来标识数据库对象。例如,您可以创建架构 名为 warehouse。使用此 API 的表 架构可能包括: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

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

使用命名架构实现精细的访问权限控制

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

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

有关详情,请参阅向已命名的 架构

架构示例

本部分中的架构示例展示了如何使用 和 创建父表和子表 没有交错,并展示了数据的相应物理布局。

创建父表

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

包含五行四列的 Singers 表

请注意,该表包含一个主键列 SingerId, 这些表格是按行和 列。

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

GoogleSQL

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 是位于数据库层次结构根目录层的表(因为它不是 定义为另一个表的交错子项)。
  • 对于 GoogleSQL 方言数据库,主键列通常带有 NOT NULL 注释 (不过,如果您想在NULL 键列。有关详情,请参阅密钥 列)。
  • 未包含在主键中的列称为非键列,它们可以具有可选的 NOT NULL 注释。
  • 在 GoogleSQL 中使用 STRINGBYTES 类型的列必须 使用一个长度定义,该长度表示 Unicode 的最大数量 字段中可存储的字符。长度规范为 对于 PostgreSQL varcharcharacter varying 是可选的 。如需了解详情,请参阅标量数据类型 用于 GoogleSQL 方言数据库和 PostgreSQL 数据 类型

Singers 表中行的物理布局是什么样的?通过 下图显示了由主键存储的 Singers 表的行 (“Singers(1)”,然后是“Singers(2)”,其中括号中的数字为 主键值。

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

上图展示了行之间的分屏边界示例 由 Singers(3)Singers(4) 键控,包含生成的分块中的数据 分配给不同的服务器。随着此表不断增大,可能出现 Singers 数据存储在不同的位置。

创建父表和子表

假设您现在想将有关每位歌手专辑的一些基本数据添加到 音乐应用

包含五行三列的相册表

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

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

GoogleSQL

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

SingersAlbums 行的物理布局类似于 在下图中,Albums 表的行由连续主实例存储 键,后跟连续主键存储的 Singers 行:

行的物理布局

关于此架构的一个重要注意事项是 Singers 表与 Albums 表之间的数据存放区域关系,因为 它们都是顶级表随着数据库不断增长,Spanner 可以 分块边界。这意味着 Albums 中的行 表最终的分块可能与 Singers 表的行不同, 这两个分块可以彼此独立移动

根据您的应用的具体需求,可以让 Albums 数据位于不同于 Singers 数据的分片上。不过,这可能会产生 这会降低性能 不同资源如果您的应用经常需要 特定歌手的所有专辑的相关信息,则应创建 Albums,如下所示 Singers 的交错子表,将两个表之间的行共置到一起 和主键维度数据。下一个示例将详细介绍 。

创建交错表

交错表是指您声明作为其交错子项的表 另一个表,因为您希望子表的行以物理方式 与关联的父行一起存储。如前所述,父表 主键必须是子表复合主键的第一部分。

在设计音乐应用时,假设您已经意识到, 在访问 Albums 表中的行时, Singers 行。例如,在访问 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,
 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 的主键。
  • ON DELETE CASCADE 注解表示,当父表中的行被删除时, 子行也会自动删除。如果子表没有 或者注解为 ON DELETE NO ACTION,则必须 必须先删除子行,然后才能删除父行。
  • 交错行首先按父表的行进行排序,然后按 子表中共享父表主键的连续行。对于 例如,“Singers(1)”,然后是“Albums(1, 1)”,最后是“Albums(1, 2)”。
  • 每个歌手与其专辑数据的数据存放区域关系为 在此数据库拆分时保留,前提是 Singers 行的大小 并且其所有 Albums 行都低于分屏大小限制,并且 这 Albums 行中的任何一行都没有热点。
  • 在插入子行之前,父行必须已经存在。 父行 可以已经存在于数据库中,也可以插入到 在同一个事务中插入子行。

相册行在 Singers 行之间交错

创建交错表的层次结构

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

包含六行四列的 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,
 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;

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

歌曲在专辑中交错,在歌手之间交错

在此示例中,随着歌手数量的增长,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,
  FirstName  STRING(1024),
  LastName   STRING(1024),
) PRIMARY KEY (SingerId);

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

CREATE TABLE Singers (
  SingerId   INT64,
  FirstName  STRING(1024),
  LastName   STRING(1024),
) 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;

不允许的类型

以下列的类型不能为 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

如果必须为每个租户使用单独的数据库,则存在以下限制: 知晓:

  • 每个实例的数据库数量有限制 以及每个数据库的表和索引数量。具体取决于 那么可能无法拥有单独的数据库或表。
  • 添加新表和非交错索引可能需要很长时间 时间。您可能 也就无法获得所需的性能了 添加新表和索引。

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

如果您为应用的每个客户创建单独的表和索引, 不要将所有表和索引放在同一个数据库中。相反, 跨多个数据库进行扩缩,以降低性能 与创建大量 Pod 相关的问题 索引的数量。

要详细了解其他数据管理模式和应用设计, 请参阅在 Google Cloud 控制台中实现多租户 Spanner