Práticas recomendadas para ajustar consultas do Spanner Graph

Este documento descreve as práticas recomendadas para ajustar a performance de consultas do Spanner Graph, incluindo as seguintes otimizações:

  • Evite uma verificação completa da tabela de entrada para nós e arestas.
  • Reduza a quantidade de dados que a consulta precisa ler do armazenamento.
  • Reduza o tamanho dos dados intermediários.

Começar com nós de baixa cardinalidade

Escreva a travessia de caminho para que ela comece com os nós de baixa cardinalidade. Essa abordagem mantém o conjunto de resultados intermediários pequeno e acelera a execução da consulta.

Por exemplo, as consultas a seguir têm a mesma semântica:

  • Transversalidade de borda para frente:

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

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

Supondo que haja menos pessoas com o nome Alex do que contas bloqueadas, recomendamos que você escreva essa consulta na travessia de borda direta.

Começar com nós de baixa cardinalidade é especialmente importante para a travessia de caminhos de comprimento variável. O exemplo a seguir mostra a maneira recomendada de encontrar contas que estão a três transferências de uma determinada conta.

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

Especificar todos os rótulos por padrão

O Spanner Graph deduz os nós qualificados e os rótulos de arestas se eles forem omitidos. Recomendamos que você especifique rótulos para todos os nós e arestas sempre que possível, porque essa inferência nem sempre é possível e pode fazer com que mais rótulos do que o necessário sejam verificados.

Uma única instrução MATCH

O exemplo a seguir encontra contas vinculadas por no máximo três transferências da conta especificada:

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

Em instruções MATCH

Especifique rótulos em nós e arestas quando eles se referirem ao mesmo elemento, mas estiverem em instruções MATCH diferentes.

O exemplo a seguir mostra essa abordagem recomendada:

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;

Usar o IS_FIRST para otimizar consultas

Você pode usar a função IS_FIRST para melhorar o desempenho da consulta ao fazer amostragem de arestas e limitar travessias em gráficos. Essa função ajuda a processar nós de alta cardinalidade e otimizar consultas de vários saltos.

Se o tamanho da amostra especificado for muito pequeno, a consulta poderá não retornar dados. Por isso, talvez seja necessário testar diferentes tamanhos de amostra para encontrar o equilíbrio ideal entre dados retornados e melhoria na performance da consulta.

Estes exemplos de IS_FIRST usam FinGraph, um gráfico financeiro com nós Account e arestas Transfers para transferências de dinheiro. Para criar o FinGraph e usá-lo para executar as consultas de exemplo, consulte Configurar e consultar o Spanner Graph.

Limitar as arestas percorridas para melhorar o desempenho da consulta

Ao consultar gráficos, alguns nós podem ter um número significativamente maior de arestas de entrada ou saída em comparação com outros nós. Esses nós de alta cardinalidade às vezes são chamados de supernós ou nós de hub. Os supernós podem causar problemas de desempenho porque as travessias por eles podem envolver o processamento de grandes quantidades de dados, o que leva a distorções de dados e longos tempos de execução.

Para otimizar uma consulta de um gráfico com supernós, use a função IS_FIRST em uma cláusula FILTER para limitar o número de arestas que a consulta atravessa de um nó. Como as contas no FinGraph podem ter um número significativamente maior de transações do que outras, use o IS_FIRST para evitar uma consulta ineficiente. Essa técnica é especialmente útil quando você não precisa de uma enumeração completa de todas as conexões de um supernó.

A consulta a seguir encontra contas (a2) que recebem transferências direta ou indiretamente de contas bloqueadas (a1). Ela usa IS_FIRST para evitar lentidão quando uma conta tem muitas transferências, limitando o número de arestas Transfers a serem consideradas para cada Account.

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;

Este exemplo usa o seguinte:

  • @max_transfers_per_account: um parâmetro de consulta que especifica o número máximo de arestas Transfers a serem consideradas para cada conta (a1).

  • PARTITION BY SOURCE_NODE_ID(selected_e): garante que o limite de IS_FIRST seja aplicado de forma independente a cada conta (a1).

  • ORDER BY selected_e.create_time DESC: especifica que as transferências mais recentes serão retornadas.

Amostrar nós intermediários para otimizar consultas de vários saltos

Também é possível melhorar a eficiência da consulta usando o IS_FIRST para amostrar nós intermediários em consultas de várias etapas. Essa técnica melhora a eficiência ao limitar o número de caminhos que a consulta considera para cada nó intermediário. Para fazer isso, divida uma consulta de vários saltos em várias instruções MATCH separadas por NEXT e aplique IS_FIRST no ponto médio em que você precisa fazer a amostragem:

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;

Para entender como o IS_FIRST otimiza essa consulta:

  • A cláusula FILTER IS_FIRST(1) OVER (PARTITION BY a2) é aplicada na primeira instrução MATCH.

  • Para cada nó de conta intermediário (a2), IS_FIRST considera apenas a primeira aresta de entrada Transfers (e1), reduzindo o número de caminhos a serem explorados na segunda instrução MATCH.

  • A eficiência geral da consulta de dois hops é melhorada porque o segundo MATCH não processa dados desnecessários, especialmente quando a2 tem muitas transferências de entrada.

Usar a execução fatorizada para otimizar consultas

Otimize a performance da consulta do Spanner Graph fatorando uma consulta que percorre um padrão de gráfico e cria resultados intermediários duplicados.

Uma consulta de grafo pode ser executada no modo fatorado, que remove a duplicação de prefixos de caminho de alta cardinalidade antes de percorrer o restante do caminho. Essa otimização melhora o desempenho da consulta reduzindo comparações de chaves repetidas ou sondagens de tabela hash durante a execução da consulta de gráfico. Para fatorar uma consulta, adicione a dica @{factorized_mode} ao percurso do padrão individual ou no nível da consulta.

Defina factorized_mode como uma das seguintes opções:

  • factorize_left: otimiza travessias de gráficos em que muitos caminhos convergem em alguns nós. O Spanner Graph remove caminhos duplicados por ID do nó de destino e busca propriedades do nó para reduzir o trabalho redundante.

  • factorize_both: otimiza consultas não lineares com vários subcaminhos que compartilham nós. O Spanner Graph evita gerar resultados intermediários para cada caminho de forma independente. Em vez disso, ele calcula cada subcaminho uma vez e os combina, adiando o produto cartesiano dos resultados até o final. Use isso para padrões como triângulos ou ramificações.

Para mais informações sobre dicas de travessia, consulte Dicas de gráfico.

Os exemplos a seguir mostram como usar dicas de modo fatorado para melhorar o desempenho das consultas de gráficos. Para executar o exemplo de código, crie o esquema FinGraph em Configurar e consultar o Spanner Graph.

Reduzir resultados intermediários em uma consulta linear

As contas fraudulentas costumam receber um grande volume de transações. As consultas criadas para identificar essas contas podem gerar grandes resultados intermediários, muitos deles duplicados porque convergem em um pequeno conjunto de contas de destino. Recuperar propriedades de nós fraudulentos desses resultados intermediários redundantes exige computação desnecessária, o que pode diminuir significativamente o desempenho da consulta.

A dica @{factorized_mode} resolve esse problema otimizando a execução da consulta. O exemplo a seguir demonstra como usar essa dica para recuperar detalhes da conta e da transação. Ele rastreia transferências originadas de uma conta bloqueada conhecida para contas potencialmente fraudulentas que estão a um a três saltos de distância. A versão não otimizada dessa consulta está disponível na segunda guia.

Consulta fatorizada

GRAPH FinGraph
MATCH (a1:Account {is_blocked: true})-[e1:Transfers]->{1,3}@{factorized_mode=factorize_left}(a2:Account)
LET total_amount = SUM(e1.amount)
RETURN a2.id, a2.create_time, total_amount;

Consulta não otimizada

GRAPH FinGraph
MATCH (a1:Account {is_blocked: true})-[e1:Transfers]->{1,3}(a2:Account)
LET total_amount = SUM(e1.amount)
RETURN a2.id, a2.create_time, total_amount;

Quando o número de nós de destino distintos a2 é muito menor do que o número de caminhos que levam a a2, essa técnica evita trabalho redundante. Isso é comum em redes de fraude, em que intermediários transferem dinheiro para alguns destinatários. A dica fatorada executa as seguintes ações:

  • Encontra todos os caminhos que incluem de uma a três arestas começando de um nó bloqueado e a soma das transferências ao longo desses caminhos.

  • Determina o número de nós de destino distintos a2.

  • Recupera o create_time para cada nó de destino a2 distinto apenas uma vez.

  • Combina o create_time de cada nó a2 com a lista de caminhos que levam a a2 para formar o resultado da consulta.

Adiar a geração de resultados intermediários em consultas não lineares complexas

As consultas de gráficos geralmente têm uma estrutura complexa, consistindo em vários padrões de subcaminho lineares. O exemplo a seguir mostra uma consulta que analisa transações originadas de uma conta com id igual a 1. Essa consulta categoriza todas as contas de destino por create_time em três buckets distintos:

  • Nos últimos 30 dias
  • De 30 a 90 dias atrás
  • Com mais de 90 dias

A consulta retorna todas as triplas e o valor total das transações.

A dica @{factorized_mode=factorize_both} otimiza a execução da consulta evitando a materialização antecipada de resultados intermediários. Ele usa o fato de que todos os subcaminhos na cláusula MATCH são condicionalmente independentes, considerando o valor do nó inicial a. factorize_both avalia os dois primeiros subcaminhos de forma independente e não repete a avaliação do último subcaminho.

Consulta fatorizada

GRAPH FinGraph
MATCH (a:Account {id: 1})-[e1:Transfers]->(a1:Account),
  @{factorized_mode=factorize_both}(a:Account)-[e2:Transfers]->(a2:Account),
  (a:Account)-[e3:Transfers]->(a3:Account)
WHERE e1.create_time < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
  AND e2.create_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
  AND e2.create_time < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
  AND e3.create_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
LET total_amount = e1.amount + e2.amount + e3.amount
RETURN a1.id as a1_id, a2.id as a2_id, a3.id as a3_id, total_amount;

Consulta não otimizada

GRAPH FinGraph
MATCH (a:Account {id: 1})-[e1:Transfers]->(a1:Account),
  (a:Account)-[e2:Transfers]->(a2:Account),
  (a:Account)-[e3:Transfers]->(a3:Account)
WHERE e1.create_time < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
  AND e2.create_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
  AND e2.create_time < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
  AND e3.create_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
LET total_amount = e1.amount + e2.amount + e3.amount
RETURN a1.id as a1_id, a2.id as a2_id, a3.id as a3_id, total_amount;

Para uma determinada conta inicial com id igual a 1, cada aresta Transfers de saída leva a vários nós de destino independentes. Confira este exemplo:

  • M é o número de contas (a1) que têm transferências no intervalo (-inf, 90 DAYS)

  • N é o número de contas (a2) que têm transferências no intervalo [90 DAYS, 30 DAYS)

  • K é o número de contas (a3) que têm transferências no intervalo [30 DAYS, +inf)

A consulta não otimizada gera resultados intermediários antecipadamente, calculando o segundo subcaminho M vezes e o terceiro subcaminho M * N vezes. Por outro lado, uma versão fatorada otimiza a execução da consulta fazendo o seguinte:

  • Obtenção de nós de destino M (a1) para o primeiro subcaminho e armazenamento temporário de um conjunto de nós a1.

  • Obter N nós de destino (a2) para o segundo subcaminho apenas uma vez e armazenar temporariamente um conjunto de nós a2.

  • Obter nós de destino K (a3) para o último subcaminho apenas uma vez.

  • Realizar vários produtos entre resultados intermediários temporários para criar o produto de registros M x N x K.

A seguir