Prácticas recomendadas para diseñar un esquema de grafo de Spanner

En este documento, se describe cómo crear consultas eficientes con las prácticas recomendadas para diseñar esquemas de Spanner Graph. Puedes iterar en el diseño de tu esquema, por lo que te recomendamos que primero identifiques los patrones de consulta críticos para guiar tu diseño de esquema.

Para obtener información general sobre las prácticas recomendadas de diseño de esquemas de Spanner, consulta Prácticas recomendadas para el diseño de esquemas.

Optimiza el recorrido de los bordes

El recorrido de aristas es el proceso de navegar por un grafo siguiendo sus aristas, comenzando en un nodo en particular y moviéndose a lo largo de aristas conectadas para llegar a otros nodos. El esquema define la dirección del borde. El recorrido de aristas es una operación fundamental en Spanner Graph, por lo que mejorar la eficiencia del recorrido de aristas es clave para el rendimiento de tu aplicación.

Puedes atravesar un borde en dos direcciones:

  • Recorrido de aristas hacia adelante: Sigue las aristas salientes del nodo de origen.

  • Recorrido de aristas inverso: Sigue los bordes entrantes del nodo de destino.

Dada una persona, la siguiente consulta de ejemplo realiza el recorrido de aristas hacia adelante de los bordes Owns:

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

Dada una cuenta, la siguiente consulta de ejemplo realiza un recorrido de aristas inverso de los bordes Owns:

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

Optimiza el recorrido de borde hacia adelante con intercalación

Para mejorar el rendimiento del recorrido de aristas hacia adelante, intercala la tabla de entrada de aristas en la tabla de entrada de nodos de origen para colocar aristas con nodos de origen. La intercalación es una técnica de optimización del almacenamiento en Spanner que coloca físicamente las filas de la tabla secundaria con sus filas principales correspondientes en el almacenamiento. Para obtener más información sobre la intercalación, consulta Descripción general de los esquemas.

En el siguiente ejemplo, se muestran estas prácticas recomendadas:

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;

Optimiza el recorrido de aristas inverso con una clave externa

Para recorrer de manera eficiente los bordes inversos, crea una restricción de clave externa forzosa entre el borde y el nodo de destino. Esta clave externa forzosa crea un índice secundario en el borde con la clave de las claves de nodo de destino. El índice secundario se usa automáticamente durante la ejecución de la consulta.

En el siguiente ejemplo, se muestran estas prácticas recomendadas:

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;

Optimiza el recorrido de aristas inverso con el índice secundario

Si no deseas crear una clave externa forzosa en el borde, por ejemplo, debido a la integridad de datos estricta que aplica, puedes crear directamente un índice secundario en la tabla de entrada de borde, como se muestra en el siguiente ejemplo:

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

Optimiza el recorrido de los bordes con claves externas informativas

Si tu situación tiene cuellos de botella de rendimiento de escritura que son causados por claves foráneas impuestas, como cuando tienes actualizaciones frecuentes de nodos de concentrador que tienen muchos bordes conectados, considera usar claves foráneas informativas. El uso de claves externas informativas en las columnas de referencia de una tabla de aristas ayuda al optimizador de consultas a descartar análisis redundantes de tablas de nodos. Sin embargo, como las claves foráneas informativas no requieren índices secundarios en la tabla de aristas, no mejoran la velocidad de las búsquedas cuando una consulta intenta encontrar aristas con nodos finales. Para obtener más información, consulta Comparación de tipos de claves externas.

Es importante comprender que, si tu aplicación no puede garantizar la integridad referencial, el uso de claves foráneas informativas para la optimización de consultas podría generar resultados incorrectos.

En el siguiente ejemplo, se crea una tabla con una clave externa informativa en la columna 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;

Si la intercalación no es una opción, puedes marcar ambas referencias de borde con claves foráneas informativas, como en el siguiente ejemplo:

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

No permitas bordes colgantes

Un borde colgante es un borde que conecta menos de dos nodos. Un borde colgante puede ocurrir cuando se borra un nodo sin quitar sus bordes asociados o cuando se crea un borde sin vincularlo correctamente a sus nodos.

La no autorización de bordes colgantes proporciona los siguientes beneficios:

  • Aplica la integridad de la estructura del gráfico.
  • Mejora el rendimiento de las consultas, ya que evita el trabajo adicional de filtrar los bordes donde no existen extremos.

No permitas bordes colgantes con restricciones referenciales

Para no permitir bordes colgantes, especifica restricciones en ambos extremos:

  • Intercala la tabla de entrada de borde en la tabla de entrada del nodo de origen. Este enfoque garantiza que el nodo de origen de un borde siempre exista.
  • Crea una restricción de clave externa forzosa en los bordes para asegurarte de que el nodo de destino de un borde siempre exista.

En el siguiente ejemplo, se usa la intercalación y una clave externa forzosa para aplicar la integridad referencial:

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;

Usa ON DELETE CASCADE para quitar automáticamente los bordes cuando borres un nodo

Cuando usas la intercalación o una clave foránea impuesta para no permitir bordes colgantes, usa la cláusula ON DELETE para controlar el comportamiento cuando quieras borrar un nodo con bordes que aún están conectados. Para obtener más información, consulta Cómo borrar de forma escalonada las tablas intercaladas y Acciones de claves externas.

Puedes usar ON DELETE de las siguientes maneras:

  • ON DELETE NO ACTION (o omitir la cláusula ON DELETE): No se podrá borrar un nodo con bordes.
  • ON DELETE CASCADE: Si borras un nodo, se quitan automáticamente los bordes asociados en la misma transacción.

Eliminación en cascada de aristas que conectan diferentes tipos de nodos

  • Borra los bordes cuando se borra el nodo de origen. Por ejemplo,INTERLEAVE IN PARENT Person ON DELETE CASCADE borra todos los bordes PersonOwnAccount salientes del nodo Person que se borra. Para obtener más información, consulta Crea tablas intercaladas.

  • Borra los bordes cuando se borra el nodo de destino. Por ejemplo, CONSTRAINT FK_Account FOREIGN KEY(account_id) REFERENCES Account(id) ON DELETE CASCADE borra todos los bordes PersonOwnAccount entrantes al nodo Account que se borra.

Borra la cascada para los bordes que conectan el mismo tipo de nodos

Cuando los nodos de origen y destino de un borde tienen el mismo tipo y el borde está intercalado en el nodo de origen, puedes definir ON DELETE CASCADE solo para el nodo de origen o el de destino (pero no para ambos).

Para quitar los bordes colgantes en ambos casos, crea una clave externa forzosa en la referencia del nodo de origen del borde en lugar de intercalar la tabla de entrada del borde en la tabla de entrada del nodo de origen.

Recomendamos el intercalamiento para optimizar el recorrido de borde hacia adelante. Asegúrate de verificar el impacto en tus cargas de trabajo antes de continuar. Consulta el siguiente ejemplo, que usa AccountTransferAccount como la tabla de entrada de borde:

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

Cómo filtrar por propiedades de nodos o aristas con índices secundarios

Los índices secundarios son esenciales para el procesamiento eficiente de consultas. Admiten búsquedas rápidas de nodos y aristas en función de valores de propiedades específicos, sin tener que recorrer toda la estructura del gráfico. Esto es importante cuando trabajas con gráficos grandes, ya que recorrer todos los nodos y aristas puede ser muy ineficiente.

Acelera el filtrado de nodos por propiedad

Para acelerar el filtrado por propiedades de nodos, crea índices secundarios en las propiedades. Por ejemplo, la siguiente consulta encuentra cuentas para un sobrenombre determinado. Sin un índice secundario, se analizan todos los nodos Account para que coincidan con los criterios de filtrado.

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

Para acelerar la consulta, crea un índice secundario en la propiedad filtrada, como se muestra en el siguiente ejemplo:

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

Sugerencia: Usa índices filtrados por NULL para las propiedades dispersas. Para obtener más información, consulta Cómo inhabilitar la indexación de valores NULL.

Acelera el recorrido de borde hacia adelante con el filtrado de propiedades de borde

Cuando recorres un borde mientras filtras sus propiedades, puedes acelerar la consulta si creas un índice secundario en las propiedades del borde y entrelazas el índice en el nodo de origen.

Por ejemplo, la siguiente consulta encuentra las cuentas que pertenecen a una persona determinada después de un período determinado:

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;

De forma predeterminada, esta consulta lee todos los bordes de la persona especificada y, luego, filtra aquellos bordes que satisfacen la condición en create_time.

En el siguiente ejemplo, se muestra cómo mejorar la eficiencia de las consultas mediante la creación de un índice secundario en la referencia del nodo de fuente de borde (id) y la propiedad de borde (create_time). Intercala el índice debajo de la tabla de entrada del nodo de origen para ubicarlo junto con el nodo de origen.

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;

Con este enfoque, la consulta puede encontrar de manera eficiente todos los bordes que satisfacen la condición en create_time.

Acelera el recorrido de borde inverso con el filtrado de propiedades de borde

Cuando recorres un borde inverso mientras filtras sus propiedades, puedes acelerar la consulta creando un índice secundario con el nodo de destino y las propiedades del borde para filtrar.

En la siguiente consulta de ejemplo, se realiza un recorrido de aristas inverso con filtrado en las propiedades de arista:

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;

Para acelerar esta consulta con un índice secundario, usa una de las siguientes opciones:

  • Crea un índice secundario en la referencia del nodo de destino perimetral (account_id) y la propiedad perimetral (create_time), como se muestra en el siguiente ejemplo:

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

    Este enfoque proporciona un mejor rendimiento porque los bordes inversos se ordenan por account_id y create_time, lo que permite que el motor de consulta encuentre bordes de manera eficiente para account_id que satisfagan la condición en create_time. Sin embargo, si diferentes patrones de consulta filtran en diferentes propiedades, cada propiedad podría requerir un índice independiente, lo que puede aumentar la sobrecarga.

  • Crea un índice secundario en la referencia del nodo de destino de la red perimetral (account_id) y almacena la propiedad de la red perimetral (create_time) en una columna de almacenamiento, como se muestra en el siguiente ejemplo:

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

    Este enfoque puede almacenar varias propiedades. Sin embargo, la consulta debe leer todos los bordes del nodo de destino y, luego, filtrar las propiedades de los bordes.

Para combinar estos enfoques, sigue estos lineamientos:

  • Usa propiedades de borde en las columnas de índice si se usan en consultas críticas para el rendimiento.
  • En el caso de las propiedades que se usan en consultas menos sensibles al rendimiento, agrégalas a las columnas de almacenamiento.

Modela tipos de nodos y vínculos con etiquetas y propiedades

Los tipos de nodos y aristas suelen modelarse con etiquetas. Sin embargo, también puedes usar propiedades para modelar tipos. Considera un ejemplo en el que hay muchos tipos de cuentas diferentes, como BankAccount, InvestmentAccount y RetirementAccount. Puedes almacenar las cuentas en tablas de entrada separadas y modelarlas como etiquetas independientes, o bien almacenarlas en una sola tabla de entrada y usar una propiedad para diferenciar entre los tipos.

Para comenzar el proceso de modelado, modela los tipos con etiquetas. Considera usar propiedades en las siguientes situaciones.

Mejora la administración de esquemas

Si tu gráfico tiene muchos tipos diferentes de nodos y aristas, puede ser difícil administrar una tabla de entrada independiente para cada uno. Para facilitar la administración del esquema, modela el tipo como una propiedad.

Tipos de modelos en una propiedad para administrar tipos que cambian con frecuencia

Cuando modelas tipos como etiquetas, agregar o quitar tipos requiere cambios en el esquema. Si realizas demasiadas actualizaciones de esquemas en un período breve, es posible que Spanner limite el procesamiento de las actualizaciones de esquemas en cola. Para obtener más información, consulta Limita la frecuencia de las actualizaciones de esquemas.

Si necesitas cambiar el esquema con frecuencia, te recomendamos que modeles el tipo en una propiedad para evitar las limitaciones en la frecuencia de las actualizaciones del esquema.

Acelera las consultas

Los tipos de modelado con propiedades pueden acelerar las consultas cuando el patrón de nodos o aristas hace referencia a varias etiquetas. En la siguiente consulta de ejemplo, se encuentran todas las instancias de SavingsAccount y InvestmentAccount que pertenecen a un Person, suponiendo que los tipos de cuenta se modelan con etiquetas:

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

El patrón de nodos acct hace referencia a dos etiquetas. Si esta es una consulta crítica para el rendimiento, considera modelar Account con una propiedad. Este enfoque podría proporcionar un mejor rendimiento de las consultas, como se muestra en el siguiente ejemplo de consulta. Te recomendamos que compares ambas consultas.

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

Almacena el tipo en la clave del elemento del nodo para acelerar las consultas

Para acelerar las consultas con filtrado en el tipo de nodo cuando un tipo de nodo se modela con una propiedad y el tipo no cambia durante el ciclo de vida del nodo, sigue estos pasos:

  1. Incluye la propiedad como parte de la clave del elemento del nodo.
  2. Agrega el tipo de nodo en la tabla de entrada de aristas.
  3. Incluye el tipo de nodo en las claves de referencia de arista.

En el siguiente ejemplo, se aplica esta optimización al nodo Account y al borde 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
  );

Configura el TTL en nodos y aristas

El tiempo de actividad (TTL) de Spanner es un mecanismo que admite el vencimiento y la eliminación automáticos de datos después de un período especificado. Por lo general, se usa para datos que tienen una vigencia o relevancia limitada, como la información de la sesión, el almacenamiento en caché temporal o los registros de eventos. En estos casos, el TTL ayuda a mantener el tamaño y el rendimiento de la base de datos.

En el siguiente ejemplo, se usa el TTL para borrar cuentas 90 días después de su cierre:

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

Si la tabla de nodos tiene un TTL y una tabla de aristas intercalados, el intercalado se debe definir con ON DELETE CASCADE. Del mismo modo, si la tabla de nodos tiene un TTL y una tabla de aristas hace referencia a ella a través de una clave externa, la clave externa se debe definir con ON DELETE CASCADE para mantener la integridad referencial o definirse como una clave externa informativa para permitir la existencia de aristas sueltas.

En el siguiente ejemplo, AccountTransferAccount se almacena durante un máximo de diez años mientras una cuenta permanece activa. Cuando se borra una cuenta, también se borra el historial de transferencias.

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

Cómo combinar tablas de entrada de nodos y bordes

Puedes usar la misma tabla de entrada para definir más de un nodo y borde en tu esquema.

En las siguientes tablas de ejemplo, los nodos Account tienen una clave compuesta (owner_id, account_id). Hay una definición de borde implícita, el nodo Person con la clave (id) es propietario del nodo Account con la clave compuesta (owner_id, account_id) cuando id es igual a owner_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);

En este caso, puedes usar la tabla de entrada Account para definir el nodo Account y el borde PersonOwnAccount, como se muestra en el siguiente ejemplo de esquema. Para garantizar que todos los nombres de las tablas de elementos sean únicos, el ejemplo le asigna el alias Owns a la definición de la tabla de aristas.

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

¿Qué sigue?