有关对 Spanner Graph 查询进行调优的最佳实践

本文档介绍了用于对 Spanner Graph 查询性能进行调优的最佳实践,包括以下优化:

  • 避免对节点和边缘的输入表进行完整扫描。
  • 减少查询需要从存储空间读取的数据量。
  • 减小中间数据的大小。

从基数较低的节点开始

编写路径遍历,使其从基数较低的节点开始。此方法可以保持较小的中间结果集,并加快查询执行速度。

例如,以下查询具有相同的语义:

  • 正向边缘遍历:

    GRAPH FinGraph
    MATCH (p:Person {name:"Alex"})-[:Owns]->(a:Account {is_blocked: true})
    RETURN p.id AS person_id, a.id AS account_id;
    
  • 反向边缘遍历:

    GRAPH FinGraph
    MATCH (a:Account {is_blocked:true})<-[:Owns]-(p:Person {name: "Alex"})
    RETURN p.id AS person_id, a.id AS account_id;
    

假设名为 Alex 的人数少于被屏蔽的账号数,我们建议您采用正向边缘遍历编写此查询。

从基数较低的节点开始对于可变长度的路径遍历尤其重要。以下示例展示了查找与给定账号相隔不超过 3 次转移的账号的推荐方法。

GRAPH FinGraph
MATCH (:Account {id: 7})-[:Transfers]->{1,3}(a:Account)
RETURN a.id;

默认指定所有标签

如果省略了标签,Spanner Graph 会推理符合条件的节点和边缘标签。我们建议您尽可能为所有节点和边缘指定标签,因为这种推理可能并非总是可行,并且可能会导致扫描的标签多于必要数量。

单个 MATCH 语句

以下示例查找与给定账号相隔不超过 3 次转移的关联账号:

GRAPH FinGraph
MATCH (src:Account {id: 7})-[:Transfers]->{1,3}(dst:Account)
RETURN dst.id;

跨 MATCH 语句

当节点和边缘引用同一元素但跨 MATCH 语句时,请对它们指定标签。

以下示例展示了此推荐方法:

GRAPH FinGraph
MATCH (acct:Account {id: 7})-[:Transfers]->{1,3}(other_acct:Account)
RETURN acct, COUNT(DISTINCT other_acct) AS related_accts
GROUP BY acct

NEXT

MATCH (acct:Account)<-[:Owns]-(p:Person)
RETURN p.id AS person, acct.id AS acct, related_accts;

使用 IS_FIRST 优化查询

您可以使用 IS_FIRST 函数通过对图中的边进行采样并限制遍历来提高查询性能。此函数有助于处理高基数节点并优化多跳查询。

如果您指定的样本大小太小,查询可能不会返回任何数据。因此,您可能需要尝试不同的样本大小,以找到返回数据与提升查询性能之间的最佳平衡点。

这些 IS_FIRST 示例使用 FinGraph,这是一个包含用于转账的 Account 节点和 Transfers 边的金融图。如需创建 FinGraph 并使用它来运行示例查询,请参阅设置和查询 Spanner Graph

限制遍历的边以提高查询性能

查询图时,某些节点的入边或出边数量可能远大于其他节点。这些高基数节点有时称为超级节点或中心节点。超级节点可能会导致性能问题,因为通过它们进行的遍历可能涉及处理海量数据,从而导致数据倾斜和执行时间过长。

如需优化包含超级节点的图的查询,请在 FILTER 子句中使用 IS_FIRST 函数,以限制查询从节点遍历的边的数量。由于 FinGraph 中的某些账号可能比其他账号拥有多得多的交易数量,因此您可以使用 IS_FIRST 来防止查询效率低下。如果您不需要完整枚举超级节点的所有连接,则此方法特别有用。

以下查询会查找直接或间接从受限账号 (a1) 接收转账的账号 (a2)。该查询使用 IS_FIRST 来限制每个 Account 要考虑的 Transfers 边的数量,从而防止在账号有许多转账时出现性能缓慢的问题。

GRAPH FinGraph
MATCH
(a1:Account {is_blocked: true})
-[e:Transfers WHERE e IN
  {
    MATCH -[selected_e:Transfers]->
    FILTER IS_FIRST(@max_transfers_per_account) OVER (
      PARTITION BY SOURCE_NODE_ID(selected_e)
      ORDER BY selected_e.create_time DESC)
    RETURN selected_e
  }
]->{1,5}
(a2:Account)
RETURN a1.id AS src_id, a2.id AS dst_id;

此示例使用以下内容:

  • @max_transfers_per_account:一个查询参数,用于指定每个账号 (a1) 要考虑的 Transfers 边的数量上限。

  • PARTITION BY SOURCE_NODE_ID(selected_e):确保 IS_FIRST 限制独立应用于每个账号 (a1)。

  • ORDER BY selected_e.create_time DESC:指定返回最近的转账。

对中间节点进行采样以优化多跳查询

您还可以通过使用 IS_FIRST 对多跳查询中的中间节点进行采样来提高查询效率。此方法通过限制查询为每个中间节点考虑的路径数量来提高效率。为此,请将多跳查询拆分为多个以 NEXT 分隔的 MATCH 语句,并在需要采样的中间点应用 IS_FIRST

GRAPH FinGraph
MATCH (a1:Account {is_blocked: true})-[e1:Transfers]->(a2:Account)
FILTER IS_FIRST(1) OVER (PARTITION BY a2)
RETURN a1, a2

NEXT

MATCH (a2)-[e2:Transfers]->(a3:Account)
RETURN a1.id AS src_id, a2.id AS mid_id, a3.id AS dst_id;

了解 IS_FIRST 如何优化此查询:

  • FILTER IS_FIRST(1) OVER (PARTITION BY a2) 子句应用于第一个 MATCH 语句。

  • 对于每个中间 account 节点 (a2),IS_FIRST 仅考虑第一个入站 Transfers 边 (e1),从而减少了第二个 MATCH 语句中需要探索的路径数量。

  • 整个两跳查询的效率得到了提高,因为第二个 MATCH 不会处理不必要的数据,尤其是当 a2 有许多入站转账时。

后续步骤