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

En este documento, se describen las prácticas recomendadas para diseñar un esquema de grafo de Spanner, con un enfoque en las consultas eficientes, el recorrido optimizado de los bordes y las técnicas eficaces de administración de datos.

Para obtener información sobre el diseño de esquemas de Spanner (no de Spanner Graph), consulta Prácticas recomendadas de diseño de esquemas.

Elige un diseño de esquema

El diseño de tu esquema afecta el rendimiento del gráfico. Los siguientes temas te ayudarán a elegir una estrategia eficaz.

Diseños con esquema y sin esquema

  • Un diseño esquematizado almacena la definición del grafo en el esquema de Spanner Graph, que es adecuado para grafos estables con cambios de definición poco frecuentes. El esquema aplica la definición del gráfico, y las propiedades admiten todos los tipos de datos de Spanner.

  • Un diseño sin esquema infiere la definición del grafo a partir de los datos, lo que ofrece más flexibilidad sin necesidad de cambiar el esquema. Las etiquetas y propiedades dinámicas no se aplican de forma predeterminada. Las propiedades deben ser valores JSON válidos.

A continuación, se resumen las principales diferencias entre la administración de datos con esquema y sin esquema. También considera tus consultas de gráficos para ayudarte a decidir qué tipo de esquema usar.

Función Administración de datos esquematizados Administración de datos sin esquema
Almacena la definición del gráfico La definición del grafo se almacena en el esquema de Spanner Graph. La definición del grafo es evidente a partir de los datos. Sin embargo, Spanner Graph no inspecciona los datos para inferir la definición.
Actualización de la definición del gráfico Requiere un cambio de esquema de Spanner Graph. Es adecuado cuando la definición está bien definida y cambia con poca frecuencia. No se necesita ningún cambio en el esquema del grafo de Spanner.
Aplicación de la definición del gráfico Un esquema de gráfico de propiedades aplica los tipos de nodos permitidos para una arista. También aplica las propiedades y los tipos de propiedades permitidos de un tipo de nodo o borde del gráfico. No se aplica de forma predeterminada. Puedes usar restricciones de verificación para aplicar la integridad de los datos de etiquetas y propiedades.
Tipos de datos de propiedad Admite cualquier tipo de datos de Spanner, por ejemplo, timestamp. Las propiedades dinámicas deben ser un valor JSON válido.

Elige un diseño de esquema basado en consultas de grafos

Los diseños esquematizados y sin esquema suelen ofrecer un rendimiento comparable. Sin embargo, cuando las búsquedas usan patrones de ruta cuantificados que abarcan varios tipos de nodos o aristas, un diseño sin esquema ofrece un mejor rendimiento.

El modelo de datos subyacente es una razón clave para esto. Un diseño sin esquema almacena todos los datos en tablas de nodos y bordes únicos, lo que DYNAMIC LABEL aplica. Las consultas que atraviesan varios tipos se ejecutan con análisis de tabla mínimos.

En cambio, los diseños esquematizados suelen usar tablas separadas para cada tipo de nodo y borde, por lo que las consultas que abarcan varios tipos deben analizar y combinar datos de todas las tablas correspondientes.

A continuación, se muestran ejemplos de consultas que funcionan bien con diseños sin esquema y una consulta de ejemplo que funciona bien con ambos diseños:

Diseño sin esquemas

Las siguientes consultas se ejecutan mejor con un diseño sin esquema porque usan patrones de ruta cuantificados que pueden coincidir con varios tipos de nodos y aristas:

  • El patrón de ruta cuantificado de esta búsqueda usa varios tipos de aristas (Transfer o Withdraw) y no especifica tipos de nodos intermedios para rutas de más de un salto.

    GRAPH FinGraph
    MATCH p = (:Account {id:1})-[:Transfer|Withdraw]->{1,3}(:Account)
    RETURN TO_JSON(p) AS p;
    
  • El patrón de ruta cuantificado de esta búsqueda encuentra rutas de uno a tres saltos entre los nodos Person y Account, con varios tipos de aristas (Owns o Transfers), sin especificar tipos de nodos intermedios para rutas más largas. Esto permite que las rutas atraviesen nodos intermedios de varios tipos. Por ejemplo, (:Person)-[:Owns]->(:Account)-[:Transfers]->(:Account).

    GRAPH FinGraph
    MATCH p = (:Person {id:1})-[:Owns|Transfers]->{1,3}(:Account)
    RETURN TO_JSON(p) AS p;
    
  • El patrón de ruta cuantificado de esta búsqueda encuentra rutas de uno a tres saltos entre los nodos Person y Account, sin especificar ninguna etiqueta de borde. Al igual que la consulta anterior, permite que las rutas atraviesen nodos intermedios de varios tipos.

    GRAPH FinGraph
    MATCH p = (:Person {id:1})-[]->{1,3}(:Account)
    RETURN TO_JSON(p) AS p;
    
  • Esta búsqueda encuentra rutas de uno a tres saltos entre nodos Account con bordes de tipo Owns en cualquier dirección (-[:Owns]-). Dado que las rutas pueden atravesar bordes en cualquier dirección y no se especifican nodos intermedios, una ruta de dos saltos puede pasar por nodos de diferentes tipos. Por ejemplo, (:Account)-[:Owns]-(:Person)-[:Owns]-(:Account).

    GRAPH FinGraph
    MATCH p = (:Account {id:1})-[:Owns]-{1,3}(:Account)
    RETURN TO_JSON(p) AS p;
    

Ambos diseños

La siguiente consulta se ejecuta de manera comparable con los diseños esquematizados y sin esquema. Su ruta cuantificada, (:Account)-[:Transfer]->{1,3}(:Account), incluye un tipo de nodo, Account, y un tipo de borde, Transfer. Dado que la ruta de acceso involucra solo un tipo de nodo y un tipo de borde, el rendimiento es comparable para ambos diseños. Aunque los nodos intermedios no se etiquetan de forma explícita, el patrón los restringe para que sean nodos Account. El nodo Person aparece fuera de esta ruta cuantificada.

GRAPH FinGraph
MATCH p = (:Person {id:1})-[:Owns]->(:Account)-[:Transfer]->{1,3}(:Account)
RETURN TO_JSON(p) AS p;

Optimiza el rendimiento del esquema de Spanner Graph

Después de elegir usar un esquema de Spanner Graph con esquema o sin esquema, puedes optimizar su rendimiento de las siguientes maneras:

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 las 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 su eficiencia puede aumentar significativamente el rendimiento de tu aplicación.

Puedes recorrer un borde en dos direcciones:

  • El recorrido de borde hacia adelante sigue los bordes salientes del nodo fuente.
  • El recorrido de borde inverso sigue los bordes entrantes del nodo de destino.

Ejemplos de consultas de recorrido de borde hacia adelante y hacia atrás

La siguiente consulta de ejemplo realiza el recorrido de borde hacia adelante de los bordes Owns para una persona determinada:

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

La siguiente consulta de ejemplo realiza el recorrido inverso de los bordes Owns para una cuenta determinada:

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

Optimiza el recorrido del borde delantero

Para mejorar el rendimiento del recorrido de bordes hacia adelante, optimiza el recorrido desde la fuente hasta el borde y desde el borde hasta el destino.

  • Para optimizar el recorrido de la fuente al borde, intercala la tabla de entrada del borde en la tabla de entrada del nodo fuente con la cláusula INTERLEAVE IN PARENT. La intercalación es una técnica de optimización del almacenamiento en Spanner que coloca las filas de la tabla secundaria junto 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.

  • Para optimizar el recorrido del borde al destino, crea una restricción de clave externa entre el borde y el nodo de destino
    . Esto aplica la restricción de borde a destino, lo que puede mejorar el rendimiento, ya que elimina los análisis de la tabla de destino. Si las claves externas aplicadas causan cuellos de botella en el rendimiento de escritura (por ejemplo, cuando se actualizan nodos centrales), usa una clave externa informativa en su lugar.

En los siguientes ejemplos, se muestra cómo usar la intercalación con una restricción de clave externa obligatoria y una informativa.

Clave externa aplicada

En este ejemplo de tabla de borde, PersonOwnAccount hace lo siguiente:

  • Se intercalan en la tabla de nodos fuente Person.

  • Crea una clave externa aplicada a la tabla de nodos de destino Account.

CREATE TABLE Person (
  id               INT64 NOT NULL,
  name             STRING(MAX),
) PRIMARY KEY (id);

CREATE TABLE Account (
  id               INT64 NOT NULL,
  create_time      TIMESTAMP,
  close_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;

Clave externa informativa

En este ejemplo de tabla de borde, PersonOwnAccount hace lo siguiente:

  • Se intercalan en la tabla de nodos fuente Person.

  • Crea una clave externa informativa para la tabla de nodos de destino Account.

CREATE TABLE Person (
  id               INT64 NOT NULL,
  name             STRING(MAX),
) PRIMARY KEY (id);

CREATE TABLE Account (
  id               INT64 NOT NULL,
  create_time      TIMESTAMP,
  close_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) NOT ENFORCED
) PRIMARY KEY (id, account_id),
  INTERLEAVE IN PARENT Person ON DELETE CASCADE;

Optimiza el recorrido de borde inverso

Optimiza el recorrido de borde inverso, a menos que tus consultas solo usen el recorrido hacia adelante, ya que las consultas que involucran el recorrido inverso o bidireccional son comunes.

Para optimizar el recorrido de borde inverso, puedes hacer lo siguiente:

  • Crea un índice secundario en la tabla de aristas.

  • Intercala el índice en la tabla de entrada del nodo de destino para ubicar los bordes junto con los nodos de destino.

  • Almacena las propiedades de los bordes en el índice.

En este ejemplo, se muestra un índice secundario para optimizar el recorrido de borde inverso para la tabla de borde PersonOwnAccount:

  • La cláusula INTERLEAVE IN coloca los datos del índice junto con la tabla del nodo de destino Account.

  • La cláusula STORING almacena las propiedades de los bordes en el índice.

Para obtener más información sobre los índices intercalados, consulta Índices y entrelazado.

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)
STORING (create_time),
INTERLEAVE IN Account;

Usa índices secundarios para filtrar propiedades

Un índice secundario permite una búsqueda eficiente de nodos y aristas en función de valores de propiedad específicos. Usar un índice ayuda a evitar un análisis de tabla completo y es especialmente útil para los gráficos grandes.

Acelera el filtrado de nodos por propiedad

La siguiente consulta busca cuentas para un apodo especificado. Como no usa un índice secundario, se deben analizar todos los nodos Account para encontrar los resultados coincidentes:

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

Crea un índice secundario en la propiedad filtrada de tu esquema para acelerar el proceso de filtrado:

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

Acelera el filtrado de bordes por propiedad

Puedes usar un índice secundario para mejorar el rendimiento de las aristas de filtrado según los valores de las propiedades.

Recorrido de borde hacia adelante

Sin un índice secundario, esta consulta debe analizar todas las aristas de una persona para encontrar las que coinciden con el filtro create_time:

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;

El siguiente código mejora la eficiencia de la consulta creando un índice secundario en la referencia del nodo fuente de la arista (id) y la propiedad de la arista (create_time). La consulta también define el índice como un elemento secundario intercalado de la tabla de entrada del nodo fuente, lo que coloca el índice junto con el nodo fuente.

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;

Recorrido de borde inverso

Sin un índice secundario, la siguiente consulta de recorrido de borde inverso debe leer todos los bordes antes de poder encontrar a la persona que posee la cuenta especificada después del create_time especificado:

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;

El siguiente código mejora la eficiencia de la consulta creando un índice secundario en la referencia del nodo de destino del borde (account_id) y la propiedad del borde (create_time). La consulta también define el índice como el hijo intercalado de la tabla del nodo de destino, lo que coloca el índice junto al nodo de destino.

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 AccountOwnedByPersonByCreateTime
ON PersonOwnAccount (account_id, create_time),
INTERLEAVE IN Account;

Cómo evitar los bordes colgantes

Una arista que conecta cero o un nodo, una arista colgante, puede comprometer la eficiencia de las consultas de Spanner Graph y la integridad de la estructura del gráfico. Se puede producir un borde aislado si borras un nodo sin borrar sus bordes asociados. También puede ocurrir un borde aislado si creas un borde, pero no existe su nodo de origen o destino. Para evitar bordes colgantes, incorpora lo siguiente en tu esquema de Spanner Graph:

Usa restricciones referenciales

Puedes usar la intercalación y las claves externas aplicadas en ambos extremos para evitar bordes colgantes. Para ello, sigue estos pasos:

  1. Intercala la tabla de entrada de borde en la tabla de entrada del nodo fuente para garantizar que siempre exista el nodo fuente de un borde.

  2. Crea una restricción de clave externa aplicada en los bordes para garantizar que siempre exista el nodo de destino de un borde. Si bien las claves externas aplicadas evitan los bordes colgantes, hacen que la inserción y el borrado de bordes sean más costosos.

En el siguiente ejemplo, se usa una clave externa aplicada y se intercalan los datos de la tabla de entrada de aristas en la tabla de entrada del nodo fuente con la cláusula INTERLEAVE IN PARENT. En conjunto, el uso de una clave externa aplicada y la intercalación también pueden ayudar a optimizar el recorrido de borde hacia adelante.

  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;

Borra bordes con ON DELETE CASCADE

Cuando usas la intercalación o una clave externa obligatoria para evitar bordes colgantes, usa la cláusula ON DELETE CASCADE en tu esquema de Spanner Graph para borrar los bordes asociados de un nodo en la misma transacción que borra el nodo. Para obtener más información, consulta Eliminación en cascada para tablas intercaladas y Acciones de claves externas.

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

En los siguientes ejemplos, se muestra cómo usar ON DELETE CASCADE en tu esquema de Spanner Graph para borrar aristas pendientes cuando borras un nodo de origen o destino. En ambos casos, el tipo del nodo borrado y el tipo del nodo conectado a él por una arista son diferentes.

Nodo fuente

Usa la intercalación para borrar las aristas colgantes cuando se borra el nodo fuente. En el siguiente ejemplo, se muestra cómo usar la intercalación para borrar las aristas salientes cuando se borra el nodo fuente (Person). Para obtener más información, consulta Crea tablas intercaladas.

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

Nodo de destino

Usa una restricción de clave externa para borrar los bordes colgantes cuando se borra el nodo de destino. En el siguiente ejemplo, se muestra cómo usar una clave externa con ON DELETE CASCADE en una tabla de aristas para borrar las aristas entrantes cuando se borra el nodo de destino (Account):

CONSTRAINT FK_Account FOREIGN KEY(account_id)
  REFERENCES Account(id) ON DELETE CASCADE

Eliminación en cascada para las aristas que conectan el mismo tipo de nodos

Cuando los nodos de origen y destino de una arista son del mismo tipo y la arista se entrelaza en el nodo de origen, puedes definir ON DELETE CASCADE para el nodo de origen o el de destino, pero no para ambos.

Para evitar bordes colgantes en estas situaciones, no intercales en la tabla de entrada del nodo fuente. En su lugar, crea dos claves externas aplicadas en las referencias de los nodos de origen y destino.

En el siguiente ejemplo, se usa AccountTransferAccount como la tabla de entrada de borde. Define dos claves externas, una en cada nodo final de la arista de transferencia, ambas con la acción ON DELETE CASCADE.

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

Configura el tiempo de actividad (TTL) en nodos y aristas

El TTL te permite quitar datos después de un período especificado. Puedes usar el TTL en tu esquema para mantener el tamaño y el rendimiento de la base de datos, ya que quita los datos que tienen una vida útil o relevancia limitadas. Por ejemplo, puedes configurarlo para que quite la información de la sesión, las cachés temporales o los registros de eventos.

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

Cuando defines una política de TTL en una tabla de nodos, debes configurar cómo se controlan las aristas relacionadas para evitar aristas no deseadas:

  • Para las tablas de borde intercaladas: Si una tabla de borde está intercalada en la tabla de nodos, puedes definir la relación de intercalación con ON DELETE CASCADE. Esto garantiza que, cuando el TTL borra un nodo, también se borren sus aristas intercaladas asociadas.

  • Para las tablas de borde con claves externas: Si una tabla de borde hace referencia a la tabla de nodos con una clave externa, tienes dos opciones:

    • Para borrar automáticamente las aristas cuando el TTL borra el nodo al que se hace referencia, usa ON DELETE CASCADE en la clave externa. Esto mantiene la integridad referencial.
    • Para permitir que los bordes permanezcan después de que se borre el nodo al que se hace referencia (lo que crea un borde colgante), define la clave externa como una clave externa informativa.

En el siguiente ejemplo, la tabla de borde AccountTransferAccount está sujeta a dos políticas de eliminación de datos:

  • Una política de TTL borra los registros de transferencia que tienen más de diez años.
  • La cláusula ON DELETE CASCADE borra todos los registros de transferencia asociados con una fuente cuando se borra esa cuenta.
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 aristas

Para optimizar tu esquema, define un nodo y sus bordes entrantes o salientes dentro de una sola tabla. Este enfoque ofrece los siguientes beneficios:

  • Menos tablas: Reduce la cantidad de tablas en tu esquema, lo que simplifica la administración de datos.

  • Mejora del rendimiento de las consultas: Se elimina el recorrido que usa uniones a una tabla de borde separada.

Esta técnica funciona bien cuando la clave primaria de una tabla también define una relación con otra tabla. Por ejemplo, si la tabla Account tiene una clave primaria compuesta (owner_id, account_id), la parte owner_id puede ser una clave externa que haga referencia a la tabla Person. Esta estructura permite que la tabla Account represente tanto el nodo Account como la arista entrante del nodo Person.

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

Puedes usar la tabla Account para definir tanto el nodo Account como su borde Owns entrante. Esto se muestra en la siguiente instrucción CREATE PROPERTY GRAPH. En la cláusula EDGE TABLES, le asignas el alias Owns a la tabla Account. Esto se debe a que cada elemento del esquema del gráfico debe tener un nombre único.

  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?