设计 Spanner 图架构的最佳实践

本文档介绍了如何使用最佳实践创建高效的查询 设计 Spanner 图架构。架构设计可以 因此我们建议您先确定关键查询模式 为您的架构设计提供指导

如需了解 Spanner 架构设计最佳实践的一般信息,请参阅架构设计最佳实践

优化边缘遍历

边遍历是指沿着图的边遍历图的过程,从特定节点开始,沿着连接的边移动到其他节点。边遍历是 Spanner 图中的基本操作,因此提高边遍历效率对应用性能至关重要。

可向两个方向遍历边:从源节点遍历到 那么从该节点到目标节点的遍历称为“前向边缘遍历”, 这个过程称为“反向边缘遍历”

使用交错优化正向边缘遍历

如需提高前向边缘遍历性能,请交错边缘输入表 到源节点输入表中,以将边缘与源节点共置。 交错是 Spanner 中的一种存储优化技术,可在存储空间中将子表行与其对应的父表行物理共置。如需详细了解交错,请参阅 架构概览

以下示例演示了这些最佳实践:

CREATE TABLE Person (
  id               INT64 NOT NULL,
  name             STRING(MAX),
) PRIMARY KEY (id);

CREATE TABLE PersonOwnAccount (
  id               INT64 NOT NULL,
  account_id       INT64 NOT NULL,
  create_time      TIMESTAMP,
) PRIMARY KEY (id, account_id),
  INTERLEAVE IN PARENT Person ON DELETE CASCADE;

使用外键优化反向边遍历

为了高效遍历反向边,请创建一个 外键限制条件 边缘和目标节点此外键会自动在以目标节点键为键的边上创建次级索引。二级索引为 在查询执行期间自动使用。

以下示例演示了这些最佳实践:

CREATE TABLE Person (
  id               INT64 NOT NULL,
  name             STRING(MAX),
) PRIMARY KEY (id);

CREATE TABLE Account (
  id               INT64 NOT NULL,
  create_time      TIMESTAMP,
) PRIMARY KEY (id);

CREATE TABLE PersonOwnAccount (
  id               INT64 NOT NULL,
  account_id       INT64 NOT NULL,
  create_time      TIMESTAMP,
  CONSTRAINT FK_Account FOREIGN KEY (account_id) REFERENCES Account (id),
) PRIMARY KEY (id, account_id),
  INTERLEAVE IN PARENT Person ON DELETE CASCADE;

使用二级索引优化反向边缘遍历

如果您不想在边缘创建外键,例如,由于 它强制执行严格的数据完整性,因此您可以直接在以下位置创建二级索引: 边缘输入表,如以下示例所示:

CREATE TABLE PersonOwnAccount (
  id               INT64 NOT NULL,
  account_id       INT64 NOT NULL,
  create_time      TIMESTAMP,
) PRIMARY KEY (id, account_id),
  INTERLEAVE IN PARENT Person ON DELETE CASCADE;

CREATE INDEX Reverse_PersonOwnAccount
ON PersonOwnAccount (account_id);

禁止悬空边

悬空边缘是指连接的节点数少于 2 个的边缘。如果删除节点时未移除其关联的边,或者创建边时未将其正确关联到其节点,则可能会出现悬空边。

禁止悬空边具有以下优势:

  • 强制执行图结构完整性。
  • 通过避免过滤掉不存在端点的边来提高查询性能。

禁止使用参照约束条件悬空边缘

要禁止悬挂边线,请在两者上指定约束条件 端点:

  • 将边缘输入表交错到源节点输入表中。这种方法可确保边的源节点始终存在。
  • 对边创建外键约束条件,以确保边的目标节点始终存在。

以下示例使用交错和外键来强制执行引用完整性:

CREATE TABLE PersonOwnAccount (
  id               INT64 NOT NULL,
  account_id       INT64 NOT NULL,
  create_time      TIMESTAMP,
  CONSTRAINT FK_Account FOREIGN KEY (account_id) REFERENCES Account (id) ON DELETE CASCADE,
) PRIMARY KEY (id, account_id),
  INTERLEAVE IN PARENT Person ON DELETE CASCADE;

使用 ON DELETE CASCADE 在删除节点时自动移除边

当您使用交错或外键禁止悬空边时,如果您想删除仍有边缘附加的节点,请使用 ON DELETE 子句来控制行为。如需了解详情,请参阅 删除交错表的级联删除与外键的级联

您可以通过以下方式使用 ON DELETE

  • ON DELETE NO ACTION(或省略 ON DELETE 子句):删除具有边的节点将会失败。
  • ON DELETE CASCADE:删除节点会自动移除同一事务中的关联边。

删除连接不同类型节点的边的级联

  • 在删除源节点时也删除边缘。例如,INTERLEAVE IN PARENT Person ON DELETE CASCADE 会删除所有外拨电话 正在删除 Person 节点的 PersonOwnAccount 条边。有关 请参阅 创建交错表

  • 在删除目标节点时删除边。例如: CONSTRAINT FK_Account FOREIGN KEY(account_id) REFERENCES Account(id) ON DELETE CASCADE 删除所有传入的 PersonOwnAccount 边缘 进入要删除的 Account 节点。如需了解详情,请参阅外键

删除连接相同类型节点的边的级联

当边的源节点和目标节点具有相同的类型且边交错到源节点中时,您只能为源节点或目标节点(但不能同时为这两个节点)定义 ON DELETE CASCADE

如需在上述两种情况下自动移除悬空边,请在边缘源节点引用上创建外键,而不是将边缘输入表交错到源节点输入表中。

对于 优化正向边缘遍历 请务必先验证这对工作负载的影响,然后再继续。请参阅以下示例,其中使用 AccountTransferAccount 作为边缘输入表:

--Define two Foreign Keys, each on one end Node of Transfer Edge, both with ON DELETE CASCADE action:
CREATE TABLE AccountTransferAccount (
  id               INT64 NOT NULL,
  to_id            INT64 NOT NULL,
  amount           FLOAT64,
  create_time      TIMESTAMP NOT NULL,
  order_number     STRING(MAX),
  CONSTRAINT FK_FromAccount FOREIGN KEY (id) REFERENCES Account (id) ON DELETE CASCADE,
  CONSTRAINT FK_ToAccount FOREIGN KEY (to_id) REFERENCES Account (id) ON DELETE CASCADE,
) PRIMARY KEY (id, to_id);

按具有二级索引的节点或边缘属性过滤

二级索引对于高效的查询处理至关重要。它们支持 根据特定属性值快速查找节点和边, 而无需遍历整个图表结构。在处理大型图时,这一点非常重要,因为遍历所有节点和边会非常低效。

按属性加快过滤节点的速度

要加快按节点属性过滤的速度,请在以下位置创建二级索引: 属性。例如,以下查询会查找指定昵称的账号。如果没有二级索引,系统会扫描所有 Account 个节点以进行匹配 过滤条件

GRAPH FinGraph
MATCH (acct:Account)
WHERE acct.nick_name = "abcd"
RETURN acct.id;

为了加快查询速度,请在过滤后的属性上创建二级索引,如下所示: 如以下示例中所示:

CREATE TABLE Account (
  id               INT64 NOT NULL,
  create_time      TIMESTAMP,
  is_blocked       BOOL,
  nick_name        STRING(MAX),
) PRIMARY KEY (id);

CREATE INDEX AccountByEmail
ON Account (nick_name);

提示:对于稀疏属性,请使用已滤除 NULL 的索引。如需更多信息 请参阅停用将 NULL 值编入索引的功能

通过对边缘属性进行过滤,加快正向边缘遍历

在遍历边时按其属性进行过滤时,您可以通过对边属性创建二级索引并将该索引交错到源节点中来加快查询速度。

例如,以下查询会查找在特定时间之后由指定用户拥有的账号:

GRAPH FinGraph
MATCH (person:Person)-[owns:Owns]->(acct:Account)
WHERE person.id = 1
  AND owns.create_time >= PARSE_TIMESTAMP("%c", "Thu Dec 25 07:30:00 2008")
RETURN acct.id;

默认情况下,此查询会读取指定人员的所有边缘,然后应用过滤条件 那些满足 create_time 条件的边缘。

以下示例展示了如何通过创建 边缘源节点引用 (id) 和 Edge 属性上的二级索引 (create_time).在源节点输入表下交错索引 将索引与源节点共置。

CREATE TABLE PersonOwnAccount (
  id               INT64 NOT NULL,
  account_id       INT64 NOT NULL,
  create_time      TIMESTAMP,
) PRIMARY KEY (id, account_id),
  INTERLEAVE IN PARENT Person ON DELETE CASCADE;

CREATE INDEX PersonOwnAccountByCreateTime
ON PersonOwnAccount (id, create_time)
INTERLEAVE IN Person;

使用这种方法,查询可以高效地查找满足 create_time 条件的所有边。

通过对边缘属性进行过滤,加快反向边缘遍历的速度

当您在遍历反向边缘的同时按其属性进行过滤时,可以执行以下操作: 使用目标节点创建二级索引并执行查询,从而加快查询速度 用于过滤的边缘属性。

以下示例查询会对边属性执行反向边遍历并进行过滤:

GRAPH FinGraph
MATCH (acct:Account)<-[owns:Owns]-(person:Person)
WHERE acct.id = 1
  AND owns.create_time >= PARSE_TIMESTAMP("%c", "Thu Dec 25 07:30:00 2008") 
RETURN person.id;

要使用二级索引加快此查询的速度,请使用以下某个选项 选项:

  • 在边缘目的地节点引用 (account_id) 和边缘属性 (create_time) 上创建次级索引,如以下示例所示:

    CREATE TABLE PersonOwnAccount (
      id               INT64 NOT NULL,
      account_id       INT64 NOT NULL,
      create_time      TIMESTAMP,
    ) PRIMARY KEY (id, account_id),
      INTERLEAVE IN PARENT Person ON DELETE CASCADE;
    
    CREATE INDEX PersonOwnAccountByCreateTime
    ON PersonOwnAccount (account_id, create_time);
    

    这种方法可提供更好的性能,因为反向边会按 account_idcreate_time 进行排序,这让查询引擎能够高效地找到满足 create_time 条件的 account_id 边。但是,如果不同的查询句式 属性,那么每个属性可能需要单独的索引 增加开销。

  • 在边缘目标节点引用上创建二级索引 (account_id) 并将 Edge 属性 (create_time) 存储在 存储列、 如以下示例中所示:

    CREATE TABLE PersonOwnAccount (
      id               INT64 NOT NULL,
      account_id       INT64 NOT NULL,
      create_time      TIMESTAMP,
    ) PRIMARY KEY (id, account_id),
      INTERLEAVE IN PARENT Person ON DELETE CASCADE;
    
    CREATE INDEX PersonOwnAccountByCreateTime
    ON PersonOwnAccount (account_id) STORING (create_time);
    

    这种方法可以存储多个属性;不过,查询必须读取目标节点的所有边,然后按边属性进行过滤。

您可以按照以下准则结合使用这两种方法:

  • 如果在索引列中使用边缘属性,请在这些列中使用这些属性。 性能关键型查询
  • 对于在对性能不太敏感的查询中使用的属性,请将其添加到存储列中。

带有标签和属性的模型节点和边类型

节点和边类型通常使用标签进行建模。不过,您也可以使用属性来对模型类型进行建模。假设有这样一个示例,其中有许多不同类型的账号,例如 BankAccountInvestmentAccountRetirementAccount。您可以将账号存储在单独的输入表中,并 将它们建模为单独的标签,也可以将账号存储在单一输入中 表格并使用属性来区分类型。

通过使用标签对类型进行建模,开始建模流程。考虑 如何使用属性来满足您的需求

改进架构管理

如果图有许多不同的节点和边类型, 会变得非常困难。为了简化架构管理, 将类型指定为属性。

用于管理频繁更改的类型的媒体资源中的模型类型

将类型建模为标签时,添加或移除类型需要 架构如果您在短时间内执行太多架构更新, Spanner 可能会 节流 已排队的架构更新的处理。如需了解详情,请参阅限制架构更新频率

如果您需要频繁更改架构,我们建议您在媒体资源中对类型进行建模,以规避架构更新频率限制。

加快查询速度

当使用节点或边缘模式时,具有属性的建模类型可能会加快查询速度 引用多个标签。以下示例查询会查找 Person 拥有的所有 SavingsAccountInvestmentAccount 实例,假设账号类型是使用标签建模的:

GRAPH FinGraph
MATCH (:Person {id: 1})-[:Owns]->(acct:SavingsAccount|InvestmentAccount)
RETURN acct.id;

acct 节点模式引用了两个标签。如果这是对性能至关重要的查询,请考虑使用属性对 Account 进行建模。这种方法可能会提供更好的查询性能,如以下查询示例所示。我们建议您对这两个查询进行基准测试。

GRAPH FinGraph
MATCH (:Person {id: 1})-[:Owns]->(acct:Account)
WHERE acct.type IN ("Savings", "Investment")
RETURN acct.id;

将类型存储在节点元素键中,以加快查询速度

若要加快按节点类型过滤的查询速度,请按以下步骤操作:如果节点类型是使用属性进行建模的,并且在节点生命周期内不会发生变化,

  1. 将该属性作为节点元素键的一部分添加。
  2. 在边缘输入表中添加节点类型。
  3. 在边缘引用键中添加节点类型。

以下示例将此优化应用于 Account 节点和 AccountTransferAccount 边。

CREATE TABLE Account (
  type             STRING(MAX) NOT NULL,
  id               INT64 NOT NULL,
  create_time      TIMESTAMP,
) PRIMARY KEY (type, id);

CREATE TABLE AccountTransferAccount (
  type             STRING(MAX) NOT NULL,
  id               INT64 NOT NULL,
  to_type          STRING(MAX) NOT NULL,
  to_id            INT64 NOT NULL,
  amount           FLOAT64,
  create_time      TIMESTAMP NOT NULL,
  order_number     STRING(MAX),
) PRIMARY KEY (type, id, to_type, to_id),
  INTERLEAVE IN PARENT Account ON DELETE CASCADE;

CREATE PROPERTY GRAPH FinGraph
  NODE TABLES (
    Account
  )
  EDGE TABLES (
    AccountTransferAccount
      SOURCE KEY (type, id) REFERENCES Account
      DESTINATION KEY (to_type, to_id) REFERENCES Account
  );

在节点和边缘上配置 TTL

Spanner 存留时间 (TTL) 是一种支持 数据的过期和移除 。这通常适用于生命周期或相关性有限的数据,例如会话信息、临时缓存或事件日志。在这些情况下,TTL 有助于维持数据库大小和性能。

以下示例使用 TTL 删除账号 closure:

CREATE TABLE Account (
  id               INT64 NOT NULL,
  create_time      TIMESTAMP,
  close_time       TIMESTAMP,
) PRIMARY KEY (id),
  ROW DELETION POLICY (OLDER_THAN(close_time, INTERVAL 90 DAY));

如果节点表中交错了 TTL 和边表,则必须使用 ON DELETE CASCADE 定义交错。同样,如果节点表具有 TTL,并且由边表通过外键引用,则必须使用 ON DELETE CASCADE 定义外键。

在以下示例中,AccountTransferAccount 会在账号保持活跃状态时存储最长 10 年。账号被删除后,转移 历史记录也会被删除

CREATE TABLE AccountTransferAccount (
  id               INT64 NOT NULL,
  to_id            INT64 NOT NULL,
  amount           FLOAT64,
  create_time      TIMESTAMP NOT NULL,
  order_number     STRING(MAX),
) PRIMARY KEY (id, to_id),
  INTERLEAVE IN PARENT Account ON DELETE CASCADE,
  ROW DELETION POLICY (OLDER_THAN(create_time, INTERVAL 3650 DAY));

合并节点和边输入表

您可以使用同一输入表在架构中定义多个节点和边。

在以下示例表中,Account 节点具有复合键 (owner_id, account_id)。有一个隐式边缘定义,即 Person 节点 具有键 (id) 拥有具有复合键的“Account”节点 当 id 等于 owner_id(owner_id, account_id)

CREATE TABLE Person (
  id INT64 NOT NULL,
) PRIMARY KEY (id);

-- Assume each account has exactly one owner.
CREATE TABLE Account (
  owner_id INT64 NOT NULL,   
  account_id INT64 NOT NULL,        
) PRIMARY KEY (owner_id, account_id);

在这种情况下,您可以使用 Account 输入表来定义 Account 节点和 PersonOwnAccount 边,如以下架构示例所示。为了确保所有元素表名称都是唯一的,该示例赋予了边缘 表定义别名 Owns

CREATE PROPERTY GRAPH FinGraph
  NODE TABLES (
    Person,
    Account
  )
  EDGE TABLES (
    Account AS Owns
      SOURCE KEY (owner_id) REFERENCES Person
      DESTINATION KEY (owner_id, account_id) REFERENCES Account
  );

后续步骤