本页介绍了 Spanner 架构要求、如何使用架构创建分层关系,以及架构功能。它还引入了 交错表,可以提高在 父子关系
架构是一个命名空间,其中包含数据库对象,例如表、视图、 索引和函数。您可以使用架构来整理对象、应用精细的访问权限特权,并避免命名冲突。您必须为每个 Pod 定义一个架构 数据库。
您还可以跨多种标准层级,进一步细分和存储数据库表中的行, 不同地理区域有关详情,请参阅 地理分区概览。
强类型数据
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 可以物理共置相关表的行。通过 架构示例展示了此物理布局的外观。
数据库分片
您最多可以定义 7 个交错父子关系的层次结构 深层,这意味着您可以将七个独立表的行共置。 如果表中的数据量比较少,那么您的数据库或许可以由单个 Spanner 服务器处理。但是,当您的 表不断增长并开始达到单个服务器的资源限制? Spanner 是一个分布式数据库,这意味着,作为您的 Spanner 会将您的数据划分为多个称为 “拆分”。各个分片可以彼此独立移动并被分配给可能位于不同物理位置的多个服务器。答 包含一系列连续的行。这一范围的开始和结束键称为“分片边界”。Spanner 会自动添加和移除 分块边界,这改变了 数据库
基于负载进行分片
作为示例,说明 Spanner 如何执行基于负载的拆分, 减少读取热点,假设您的数据库包含一个有 10 行 读取频率比表中所有其他行的读取频率更高。 Spanner 就可以在这 10 行中的每一行之间添加分片边界,以便每一行分别由不同的服务器处理,这样可避免这些行的所有读取操作消耗单台服务器的资源。
一般而言,如果您遵循架构设计最佳实践,Spanner 可以缓解热点问题,使读取吞吐量每隔几分钟就会提高,直到实例中的资源达到饱和或遇到无法添加新分块边界的情况(因为您有一个分块仅涵盖一行,且没有交错子行)。
已命名的架构
命名架构可帮助您将类似数据整理在一起。这有助于您 在 Google Cloud 控制台中查找对象、应用权限并避免命名 冲突。
与其他数据库对象一样,命名架构也使用 DDL 进行管理。
借助 Spanner 命名架构,您可以使用完全限定名称 (FQN) 查询数据。借助 FQN,您可以组合使用架构名称和对象名称来标识数据库对象。例如,您可以创建架构
名为 warehouse
。使用此 API 的表
架构可能包括: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
表的行按相应的连续主键进行存储:
关于此架构的一个重要注意事项是
Singers
表与 Albums
表之间的数据存放区域关系,因为
它们都是顶级表随着数据库不断增长,Spanner 可以在任何行之间添加分片边界。这意味着,Albums
表行的分片结束位置可能不同于 Singers
表行,并且两个分片可以彼此独立移动。
根据您的应用的具体需求,可以让 Albums
数据位于不同于 Singers
数据的分片上。不过,由于需要协调对不同资源的读取和更新,因此这可能会导致性能下降。如果您的应用经常需要检索有关特定歌手的所有专辑的信息,则应该将 Albums
创建为 Singers
的交错子表,这样做可沿着主键维度协同定位两个表中的行。下一个例子将详细解释这一点
。
创建交错表
交错表是指您声明作为其交错子项的表 另一个表,因为您希望子表的行以物理方式 与关联的父行一起存储。如前所述,父表主键必须是子表复合主键的第一部分。
在设计音乐应用时,假设您已经意识到,
在访问 Albums
表中的行时,
Singers
行。例如,当您访问行 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 |
如果必须为每个租户使用单独的数据库,则存在以下限制: 知晓:
- 每个实例的数据库数量有限制 以及每个数据库的表和索引数量。取决于客户的具体数量,可能无法安排单独的数据库或表。
- 添加新表和非交错索引可能需要很长时间 时间。您可能不会 也就无法获得所需的性能了 添加新表和索引。
如果想要创建单独的数据库,那么当您将表分布到不同数据库时,您成功的几率更大,因为采用这种方式,每个数据库每周的架构更改量较少。
如果您为应用的每个客户创建单独的表和索引, 不要将所有表和索引放在同一个数据库中。相反,请将它们拆分到多个数据库中,以减轻创建大量索引带来的性能问题。
要详细了解其他数据管理模式和应用设计, 请参阅在 Google Cloud 控制台中实现多租户 Spanner