Prácticas recomendadas para ajustar las consultas de Spanner Graph

En este documento, se describen las prácticas recomendadas para ajustar el rendimiento de las consultas de Spanner Graph, lo que incluye las siguientes optimizaciones:

  • Evita un análisis completo de la tabla de entrada para nodos y aristas.
  • Reduce la cantidad de datos que la consulta necesita leer del almacenamiento.
  • Reduce el tamaño de los datos intermedios.

Comienza con los nodos de menor cardinalidad

Escribe el recorrido de ruta de acceso de modo que comience con los nodos de menor cardinalidad. Este enfoque mantiene pequeño el conjunto de resultados intermedios y acelera la ejecución de la consulta.

Por ejemplo, las siguientes consultas tienen la misma semántica:

  • Recorrido de borde hacia adelante:

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

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

Suponiendo que hay menos personas con el nombre Alex que cuentas bloqueadas, te recomendamos que escribas esta consulta en el recorrido de borde hacia adelante.

Comenzar con nodos de menor cardinalidad es especialmente importante para el recorrido de rutas de longitud variable. En el siguiente ejemplo, se muestra la forma recomendada de encontrar cuentas que estén a tres transferencias de una cuenta determinada.

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

Especificar todas las etiquetas de forma predeterminada

Spanner Graph infiere los nodos y las etiquetas de borde aptos si se omiten las etiquetas. Te recomendamos que especifiques etiquetas para todos los nodos y bordes siempre que sea posible, ya que esta inferencia no siempre es posible y puede hacer que se analicen más etiquetas de las necesarias.

Una sola declaración MATCH

En el siguiente ejemplo, se buscan las cuentas vinculadas por un máximo de 3 transferencias desde la cuenta determinada:

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

En todas las declaraciones MATCH

Especifica etiquetas en los nodos y los bordes cuando se refieran al mismo elemento, pero se encuentren en diferentes instrucciones MATCH.

En el siguiente ejemplo, se muestra este enfoque recomendado:

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;

Usa IS_FIRST para optimizar las búsquedas

Puedes usar la función IS_FIRST para mejorar el rendimiento de las consultas tomando muestras de las aristas y limitando los recorridos en los gráficos. Esta función ayuda a controlar los nodos de alta cardinalidad y a optimizar las consultas de múltiples saltos.

Si el tamaño de la muestra que especificaste es demasiado pequeño, es posible que la consulta no devuelva datos. Por este motivo, es posible que debas probar diferentes tamaños de muestra para encontrar el equilibrio óptimo entre los datos devueltos y el rendimiento mejorado de las consultas.

En estos ejemplos de IS_FIRST, se usa FinGraph, un gráfico financiero con nodos Account y aristas Transfers para las transferencias de dinero. Para crear el FinGraph y usarlo para ejecutar las consultas de muestra, consulta Configurar y consultar Spanner Graph.

Limita los bordes recorridos para mejorar el rendimiento de las consultas

Cuando consultas gráficos, algunos nodos pueden tener una cantidad significativamente mayor de aristas entrantes o salientes en comparación con otros nodos. Estos nodos de alta cardinalidad a veces se denominan nodos centrales o supernodos. Los supernodos pueden causar problemas de rendimiento porque los recorridos a través de ellos pueden implicar el procesamiento de grandes cantidades de datos, lo que genera una distorsión de los datos y tiempos de ejecución prolongados.

Para optimizar una consulta de un gráfico con supernodos, usa la función IS_FIRST dentro de una cláusula FILTER para limitar la cantidad de aristas que atraviesa la consulta desde un nodo. Dado que las cuentas de FinGraph pueden tener cantidades significativamente más altas de transacciones que otras, puedes usar IS_FIRST para evitar una consulta ineficiente. Esta técnica es especialmente útil cuando no necesitas una enumeración completa de todas las conexiones desde un supernodo.

La siguiente consulta busca cuentas (a2) que reciben transferencias directa o indirectamente de cuentas bloqueadas (a1). La consulta usa IS_FIRST para evitar un rendimiento lento cuando una cuenta tiene muchas transferencias, ya que limita la cantidad de bordes Transfers que se deben tener en cuenta 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;

En este ejemplo, se usan los siguientes elementos:

  • @max_transfers_per_account: Es un parámetro de consulta que especifica la cantidad máxima de aristas Transfers que se deben tener en cuenta para cada cuenta (a1).

  • PARTITION BY SOURCE_NODE_ID(selected_e): Garantiza que el límite de IS_FIRST se aplique de forma independiente para cada cuenta (a1).

  • ORDER BY selected_e.create_time DESC: Especifica que se devuelven las transferencias más recientes.

Muestrea nodos intermedios para optimizar las consultas de múltiples saltos

También puedes mejorar la eficiencia de las consultas usando IS_FIRST para muestrear nodos intermedios en consultas de múltiples saltos. Esta técnica mejora la eficiencia, ya que limita la cantidad de rutas que la búsqueda considera para cada nodo intermedio. Para ello, divide una consulta de múltiples saltos en varias sentencias MATCH separadas por NEXT y aplica IS_FIRST en el punto medio en el que necesitas realizar el muestreo:

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 comprender cómo IS_FIRST optimiza esta consulta, ten en cuenta lo siguiente:

  • La cláusula FILTER IS_FIRST(1) OVER (PARTITION BY a2) se aplica en la primera instrucción MATCH.

  • Para cada nodo de cuenta intermedio (a2), IS_FIRST solo considera la primera arista entrante Transfers (e1), lo que reduce la cantidad de rutas para explorar en la segunda instrucción MATCH.

  • Se mejora la eficiencia general de la consulta de dos saltos porque el segundo MATCH no procesa datos innecesarios, en especial cuando a2 tiene muchas transferencias entrantes.

Usa la ejecución factorizada para optimizar las consultas

Optimiza el rendimiento de las consultas de Spanner Graph factorizando una consulta que recorre un patrón de gráfico y crea resultados intermedios duplicados.

Una consulta de grafo se puede ejecutar en modo factorizado, lo que permite quitar los prefijos de ruta de alta cardinalidad duplicados antes de recorrer el resto de la ruta. Esta optimización mejora el rendimiento de las consultas, ya que reduce las comparaciones de claves repetidas o las exploraciones de tablas hash durante la ejecución de consultas de gráficos. Para factorizar una búsqueda, agrega la sugerencia @{factorized_mode} al recorrido de patrones individual o a nivel de la búsqueda.

Configura factorized_mode en uno de los siguientes valores:

  • factorize_left: Optimiza los recorridos de gráficos en los que muchas rutas convergen en pocos nodos. Spanner Graph elimina las rutas duplicadas por ID de nodo de destino y, luego, recupera las propiedades del nodo para reducir el trabajo redundante.

  • factorize_both: Optimiza las consultas no lineales con varias rutas secundarias que comparten nodos. Spanner Graph evita generar resultados intermedios para cada ruta de forma independiente. En cambio, calcula cada subruta una vez y, luego, las combina, lo que retrasa el producto cartesiano de los resultados hasta el final. Úsalo para patrones como triángulos o ramas.

Para obtener más información sobre las sugerencias de recorrido, consulta Sugerencias de gráfico.

En los siguientes ejemplos, se muestra cómo usar sugerencias de modo factorizado para mejorar el rendimiento de las consultas de gráficos. Para ejecutar el código de muestra, crea el esquema FinGraph en Configurar y consultar Spanner Graph.

Cómo reducir los resultados intermedios en una consulta lineal

Las cuentas fraudulentas suelen recibir un gran volumen de transacciones. Las consultas diseñadas para identificar estas cuentas pueden generar grandes resultados intermedios, muchos de los cuales son duplicados porque convergen en un pequeño conjunto de cuentas de destino. Recuperar las propiedades de los nodos fraudulentos de estos resultados intermedios redundantes requiere cálculos innecesarios, lo que puede ralentizar significativamente el rendimiento de las consultas.

La sugerencia @{factorized_mode} aborda este problema optimizando la ejecución de la consulta. En el siguiente ejemplo, se muestra cómo usar esta sugerencia para recuperar detalles de la cuenta y la transacción. Realiza un seguimiento de las transferencias que se originan en una cuenta bloqueada conocida hacia cuentas potencialmente fraudulentas que se encuentran a entre uno y tres saltos de distancia. La versión sin optimizar de esta consulta está disponible en la segunda pestaña.

Consulta factorizada

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 no optimizada

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;

Cuando la cantidad de nodos de destino distintos a2 es mucho menor que la cantidad de rutas que conducen a a2, esta técnica evita el trabajo redundante. Esto es común en las redes de fraude, en las que los intermediarios transfieren dinero a unos pocos destinatarios. La sugerencia factorizada realiza las siguientes acciones:

  • Encuentra todas las rutas que incluyen de uno a tres bordes a partir de un nodo bloqueado y la suma de las transferencias a lo largo de esas rutas.

  • Determina la cantidad de nodos de destino distintos a2.

  • Recupera el create_time para cada nodo de destino distinto a2 solo una vez.

  • Combina el create_time de cada nodo a2 con la lista de rutas que conducen a a2 para formar el resultado de la consulta.

Diferir la generación de resultados intermedios en consultas complejas no lineales

Las consultas de gráficos suelen tener una estructura compleja, que consta de varios patrones de subrutas lineales. En el siguiente ejemplo, se muestra una consulta que analiza las transacciones que se originan en una cuenta con id igual a 1. Esta consulta categoriza todas las cuentas de destino según su create_time en tres buckets distintos:

  • En los últimos 30 días
  • Hace entre 30 y 90 días
  • Más de 90 días

La consulta devuelve todas las tuplas y el importe total de las transacciones.

La sugerencia @{factorized_mode=factorize_both} optimiza la ejecución de la consulta, ya que evita la materialización temprana de resultados intermedios. Utiliza el hecho de que todas las subrutas en la cláusula MATCH son condicionalmente independientes, dado el valor del nodo inicial a. factorize_both evalúa los dos primeros subrutas de forma independiente y no repite la evaluación de la última subruta.

Consulta factorizada

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 no optimizada

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 una cuenta inicial determinada con id igual a 1, cada arista saliente Transfers conduce a varios nodos de destino independientes. Considera el siguiente ejemplo:

  • M es la cantidad de cuentas (a1) que tienen transferencias en el rango de (-inf, 90 DAYS).

  • N es la cantidad de cuentas (a2) que tienen transferencias en el rango de [90 DAYS, 30 DAYS).

  • K es la cantidad de cuentas (a3) que tienen transferencias en el rango de [30 DAYS, +inf).

La consulta sin optimizar genera resultados intermedios de forma anticipada, ya que calcula el segundo subruta M veces y el tercer subruta M * N veces. Por el contrario, una versión factorizada optimiza la ejecución de la consulta de la siguiente manera:

  • Obtener los nodos de destino M (a1) para la primera subruta y almacenar temporalmente un conjunto de nodos a1

  • Obtener nodos de destino N (a2) para el segundo subruta solo una vez y almacenar temporalmente un conjunto de nodos a2

  • Obtener los nodos de destino K (a3) del último subruta solo una vez

  • Realiza un producto cruzado entre los resultados intermedios temporales para crear el producto de los registros de M x N x K.

¿Qué sigue?