设计 Spanner Graph 架构的最佳实践

本文档介绍了如何使用 Spanner Graph 架构设计最佳实践来创建高效的查询。您可以迭代进行架构设计,因此建议您先确定关键查询句式以指导架构设计。

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

优化边缘遍历

边缘遍历是指沿着图表的边缘来遍历图表的过程,具体方法是从特定节点开始,沿着其连接的边缘依次访问其他节点。边缘的方向由架构定义。边缘遍历是 Spanner Graph 中的一项基本操作,因此提高边缘遍历效率是提升应用性能的关键。

您可以沿两个方向遍历边缘:

  • 正向边缘遍历:沿着来源节点的传出边缘进行遍历。

  • 反向边缘遍历:沿着目标节点的传入边缘进行遍历。

在给定用户的情况下,以下示例查询会执行 Owns 边缘的正向边缘遍历:

GRAPH FinGraph
MATCH (person:Person {id: 1})-[owns:Owns]->(accnt:Account)
RETURN accnt.id;

在给定账号的情况下,以下示例查询会执行 Owns 边缘的反向边缘遍历:

GRAPH FinGraph
MATCH (accnt:Account {id: 1})<-[owns:Owns]-(person:Person)
RETURN person.name;

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

如需提高正向边缘遍历性能,请将边缘输入表交织到来源节点输入表中,以将边缘与来源节点放在同一位置。交织是 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 AccountOwnedByPerson
ON PersonOwnAccount (account_id), INTERLEAVE IN Account;

INTERLEAVE IN 声明二级索引与它交织的表(示例中为 Account)之间的数据存储区域关系。通过交织,AccountOwnedByPerson 二级索引的行会与 Account 表的相应行放在同一位置。如需详细了解交织,请参阅父子表关系。如需详细了解交织索引,请参阅索引和交织

使用信息性外键优化边缘遍历

如果您的应用场景存在因强制执行外键而导致的写入性能瓶颈(例如当您频繁更新具有许多连接边缘的中心节点时),请考虑使用信息性外键。对边缘表的引用列使用信息性外键有助于查询优化器舍弃冗余的节点表扫描。不过,由于信息性外键不需要对边缘表使用二级索引,因此当查询尝试使用端节点查找边缘时,它们不会提高查找速度。如需了解详情,请参阅外键类型比较

请务必了解,如果您的应用无法保证引用完整性,那么使用信息性外键进行查询优化可能会导致查询结果不正确。

以下示例会创建一个表,其中的 account_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) NOT ENFORCED
) 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,
  CONSTRAINT FK_Person FOREIGN KEY (id)
    REFERENCES Person (id) NOT ENFORCED,
  CONSTRAINT FK_Account FOREIGN KEY (account_id)
    REFERENCES Account (id) NOT ENFORCED
) PRIMARY KEY (id, account_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) 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 会删除所删除的 Account 节点的所有传入 PersonOwnAccount 边缘。

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

如果边缘的来源节点和目标节点的类型相同,并且边缘交织到来源节点中,则您只能为来源节点或目标节点(但不能同时为这两个节点)定义 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 AccountByNickName
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) 和边缘属性 (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 排序,这让查询引擎能够高效地针对 account_id 查找满足 create_time 条件的边缘。不过,如果不同的查询句式按不同的属性进行过滤,那么每个属性可能都需要单独的索引,这可能会增加开销。

  • 对边缘目标节点引用 (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) 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 在账号关闭 90 天后将其删除:

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 最长可存储十年,前提是相应账号保持活跃状态。删除账号后,转移历史记录也会一并删除。

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)。存在隐式边缘定义:当 id 等于 owner_id 时,键为 (id) 的 Person 节点拥有复合键为 (owner_id, account_id)Account 节点。

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

后续步骤