本页面讨论 Spanner 架构要求、如何使用架构创建分层关系以及架构功能。此外,它还引入了交错表,在查询采用父子关系的表时,可以提高查询性能。
架构是包含数据库对象(例如表、视图、索引和函数)的命名空间。您可以使用架构来整理对象、应用精细的访问权限控制权限,并避免命名冲突。您必须为 Spanner 中的每个数据库定义一个架构。
您还可以跨不同地理区域进一步细分和存储数据库表中的行。如需了解详情,请参阅地理位置分区概览。
强类型数据
Spanner 中的数据是强类型的数据。数据类型包括标量和复杂类型,GoogleSQL 中的数据类型和 PostgreSQL 数据类型对数据类型进行了介绍。
选择主键
Spanner 数据库可以包含一个或多个表。表格的结构为行和列。表架构将一个或多个表列定义为表的主键,用于唯一标识每一行。主键始终会编入索引以进行快速行查找。如果要更新或删除表中的现有行,则该表必须具有主键。没有主键列的表只能有一行。只有 GoogleSQL 方言数据库可以有没有主键的表。
通常,您的应用已经有一个本身就适合用作主键的字段。例如,对于 Customers
表,可能有一个应用提供的 CustomerId
充当主键。在其他情况下,您可能需要在插入行时生成主键。这通常是没有业务意义的唯一整数值(代理主键)。
无论哪种情况,都请务必小心谨慎,千万不要在选择主键时形成热点。例如,如果您插入一些记录,而这些记录将单调递增的整数用作键,那么您将始终在键空间末尾进插入记录。这种做法是不可取的,因为 Spanner 会按键范围划分服务器之间的数据,这意味着您的插入操作会指向单个服务器,从而形成热点。可利用一些方法将负载分散到多个服务器上,从而避免热点:
- 对键进行哈希处理,并将其存储在一列中。使用哈希列(或同时使用哈希列和唯一键列)作为主键。
- 交换主键中列的顺序。
- 使用通用唯一标识符 (UUID)。建议使用版本 4 UUID,因为它使用高位随机值。请勿使用将时间戳存储在高位中的 UUID 算法(如版本 1 UUID)。
- 对顺序值进行位反转。
父子表关系
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
的架构。使用此架构的表可以包括:product
、order
和 customer information
。或者,您也可以为履单业务部门创建一个名为 fulfillment
的架构。此架构还可以包含名为 product
、order
和 customer
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
如需详细了解如何使用命名架构,请参阅管理命名架构。
使用命名架构实现精细的访问权限控制
通过命名架构,您可以授予对架构中每个对象的架构级访问权限。 这适用于在您授予访问权限时存在的架构对象。您必须授予对稍后添加的对象的访问权限。
精细的访问权限控制可限制对整组数据库对象(例如表中的表、列和行)的访问权限。
如需了解详情,请参阅授予对已命名架构的精细访问权限控制权限。
架构示例
本部分中的架构示例展示了如何创建具有交错和不交错的父表和子表,并说明了数据的相应物理布局。
创建父表
假设您正在创建一个音乐应用,并且需要一个存储各行歌手数据的表:
请注意,该表包含一个主键列 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 中使用
STRING
或BYTES
类型的列必须定义一个长度,该长度表示可以在该字段中存储的最多 Unicode 字符数。对于 PostgreSQLvarchar
和character varying
类型,长度规范是可选的。如需了解详情,请参阅 GoogleSQL 方言数据库的标量数据类型和 PostgreSQL 方言数据库的 PostgreSQL 数据类型。
Singers
表中行的物理布局是什么样的?下图显示了由主键存储的 Singers
表中的行(先是 Singers(1),然后是 Singers(2)),其中括号中的数字是主键值。
上图展示了由 Singers(3)
和 Singers(4)
键控的行之间的拆分边界示例,生成的分片中的数据被分配给了不同的服务器。随着此表不断扩大,Singers
数据行可能会存储在不同的位置。
创建父表和子表
假设您现在想将有关每位歌手专辑的一些基本数据添加到音乐应用。
请注意,Albums
的主键由两列组成:SingerId
和 AlbumId
,它们将每个专辑与其歌手相关联。以下示例架构在数据库层次结构的根目录定义了 Albums
和 Singers
表,这使它们成为同级表。
-- 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) );
Singers
和 Albums
的行的物理布局如下图所示,Albums
表中的行按连续的主键存储,Singers
的行按连续的主键存储:
关于架构的一个重要注意事项是,Spanner 假定 Singers
和 Albums
表之间没有数据存放区域关系,因为它们是顶级表。随着数据库不断增长,Spanner 可以在任何行之间添加分块边界。这意味着,Albums
表的行最终可能位于与 Singers
表行不同的分片中,并且两个分片可以彼此独立移动。
根据您的应用的具体需求,可以让 Albums
数据位于不同于 Singers
数据的分片上。但是,由于需要跨不同资源协调读取和更新,这可能会导致性能下降。如果您的应用经常需要检索有关特定歌手的所有专辑的信息,那么您应该将 Albums
创建为 Singers
的交错子表,它会沿着主键维度共置两个表中的行。下一个示例对此进行了更详细的说明。
创建交错表
交错表是指您声明作为另一个表的交错子表的表,因为您希望子表的行与关联的父行以物理方式存储。如前所述,父表主键必须是子表复合主键的第一部分。
在设计音乐应用时,假设您意识到应用在访问 Singers
行时需要频繁访问 Albums
表中的行。例如,当您访问 Singers(1)
行时,您还需要访问 Albums(1, 1)
和 Albums(1, 2)
行。在这种情况下,Singers
和 Albums
需要建立密切的数据存放区域关系。您可以通过将 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
和 Albums
之间的父子关系可以扩展到更多的后代表。例如,您可以创建一个名为 Songs
的交错表作为 Albums
的子表,用于存储每个专辑的曲目清单:
Songs
必须具有主键,并且其中包含层次结构中位于更高层级的表(即 SingerId
和 AlbumId
)的所有主键。
-- 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 可以在本地通过主键执行联接,从而最大限度地减少存储访问和网络流量。在以下示例中,Singers
和 Albums
通过主键 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 的属性必须在父表和子表声明之间匹配。在此示例中,不允许对列 Albums.SingerId
使用 NOT NULL
,因为 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
表:
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 |
架构管理的多租户
在 Spanner 中设计多租户的另一种方法是,将所有客户放在单个数据库的单个表中,并为每个客户使用不同的主键值对。例如,您可以在表中添加一个 CustomerId
键列。如果您将 CustomerId
设为第一个键列,则每个客户的数据都能具有良好的存放区域。然后,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 |
如果您必须为每个租户使用单独的数据库,请注意以下限制:
- 每个实例的数据库数量以及每个数据库的表和索引数量都有限制。可能无法拥有单独的数据库或表,具体取决于客户的数量。
- 添加新表和非交错索引可能需要很长时间。如果您的架构设计依赖于添加新表和索引,那么您可能无法获得所需的性能。
如果想要创建单独的数据库,那么当您将表分布到不同数据库时,您成功的几率更大,因为采用这种方式,每个数据库每周的架构更改量较少。
如果您为应用的每个客户创建单独的表和索引,请不要将所有表和索引都放在同一数据库中。而是将它们拆分到多个数据库中,以缓解创建大量索引产生的性能问题。
如需详细了解多租户的其他数据管理模式和应用设计,请参阅在 Spanner 中实现多租户。