Práticas recomendadas para criar um esquema do Spanner Graph

Este documento descreve as práticas recomendadas para criar um esquema do Spanner Graph, com foco em consultas eficientes, travessia de arestas otimizada e técnicas de gestão de dados eficazes.

Para informações sobre a criação de esquemas do Spanner (não esquemas do Spanner Graph), consulte as Práticas recomendadas de criação de esquemas.

Escolha um design de esquema

O design do esquema afeta o desempenho do gráfico. Os seguintes tópicos ajudam a escolher uma estratégia eficaz.

Designs esquematizados versus designs sem esquema

  • Um design esquematizado armazena a definição do gráfico no esquema do Spanner Graph, que é adequado para gráficos estáveis com alterações de definição pouco frequentes. O esquema aplica a definição do gráfico e as propriedades suportam todos os tipos de dados do Spanner.

  • Um design sem esquema infere a definição do gráfico a partir dos dados, oferecendo mais flexibilidade sem exigir alterações ao esquema. As etiquetas e as propriedades dinâmicas não são aplicadas por predefinição. As propriedades têm de ser valores JSON válidos.

O seguinte resume as principais diferenças entre a gestão de dados com esquema e sem esquema. Além disso, considere as suas consultas de gráficos para ajudar a decidir que tipo de esquema usar.

Funcionalidade Gestão de dados esquematizados Gestão de dados sem esquema
Armazenar a definição do gráfico A definição do gráfico é armazenada no esquema do gráfico do Spanner. A definição do gráfico é evidente a partir dos dados. No entanto, o Spanner Graph não inspeciona os dados para inferir a definição.
A atualizar a definição do gráfico Requer uma alteração do esquema do Spanner Graph. Adequado quando a definição está bem definida e muda com pouca frequência. Não é necessária nenhuma alteração ao esquema do Spanner Graph.
Impor definição de gráfico Um esquema de gráfico de propriedades aplica os tipos de nós permitidos para uma aresta. Também aplica as propriedades e os tipos de propriedades permitidos de um nó ou um tipo de aresta do gráfico. Não aplicada por predefinição. Pode usar as restrições de verificação para aplicar a integridade dos dados de etiquetas e propriedades.
Tipos de dados de propriedades Suportar qualquer tipo de dados do Spanner, por exemplo, timestamp. As propriedades dinâmicas têm de ser um valor JSON válido.

Escolha um design de esquema com base em consultas de grafos

Os designs esquematizados e sem esquema oferecem frequentemente um desempenho comparável. No entanto, quando as consultas usam padrões de caminhos quantificados que abrangem vários tipos de nós ou arestas, um design sem esquema oferece um melhor desempenho.

O modelo de dados subjacente é um motivo fundamental para isto. Um design sem esquema armazena todos os dados em tabelas de nós e arestas únicas, o que DYNAMIC LABEL aplica. As consultas que atravessam vários tipos são executadas com o mínimo de análises de tabelas.

Por outro lado, os esquemas estruturados usam normalmente tabelas separadas para cada tipo de nó e aresta, pelo que as consultas que abrangem vários tipos têm de analisar e combinar dados de todas as tabelas correspondentes.

Seguem-se exemplos de consultas que funcionam bem com designs sem esquema e um exemplo de consulta que funciona bem com ambos os designs:

Design sem esquemas

As seguintes consultas têm um melhor desempenho com um design sem esquema porque usam padrões de caminhos quantificados que podem corresponder a vários tipos de nós e arestas:

  • O padrão de caminho quantificado desta consulta usa vários tipos de arestas (Transfer ou Withdraw) e não especifica tipos de nós intermédios para caminhos com mais de um salto.

    GRAPH FinGraph
    MATCH p = (:Account {id:1})-[:Transfer|Withdraw]->{1,3}(:Account)
    RETURN TO_JSON(p) AS p;
    
  • O padrão de caminho quantificado desta consulta encontra caminhos de um a três saltos entre os nós Person e Account, usando vários tipos de arestas (Owns ou Transfers), sem especificar tipos de nós intermédios para caminhos mais longos. Isto permite que os caminhos atravessem nós intermédios de vários tipos. Por exemplo, (:Person)-[:Owns]->(:Account)-[:Transfers]->(:Account).

    GRAPH FinGraph
    MATCH p = (:Person {id:1})-[:Owns|Transfers]->{1,3}(:Account)
    RETURN TO_JSON(p) AS p;
    
  • O padrão de caminho quantificado desta consulta encontra caminhos de um a três saltos entre os nós Person e Account, sem especificar etiquetas de arestas. Semelhante à consulta anterior, permite que os caminhos atravessem nós intermédios de vários tipos.

    GRAPH FinGraph
    MATCH p = (:Person {id:1})-[]->{1,3}(:Account)
    RETURN TO_JSON(p) AS p;
    
  • Esta consulta encontra caminhos de um a três saltos entre nós Account usando arestas do tipo Owns em qualquer direção (-[:Owns]-). Uma vez que os caminhos podem atravessar arestas em qualquer direção e os nós intermédios não são especificados, um caminho de dois saltos pode passar por nós de diferentes tipos. Por exemplo, (:Account)-[:Owns]-(:Person)-[:Owns]-(:Account).

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

Ambos os designs

A seguinte consulta tem um desempenho comparável com designs esquematizados e sem esquema. O respetivo caminho quantificado, (:Account)-[:Transfer]->{1,3}(:Account), envolve um tipo de nó, Account, e um tipo de aresta, Transfer. Uma vez que o caminho envolve apenas um tipo de nó e um tipo de aresta, o desempenho é comparável para ambos os designs. Embora os nós intermédios não sejam etiquetados explicitamente, o padrão restringe-os a nós Account. O nó Person aparece fora deste caminho quantificado.

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

Otimize o desempenho do esquema do Spanner Graph

Depois de optar por usar um esquema do Spanner Graph esquematizado ou sem esquema, pode otimizar o respetivo desempenho das seguintes formas:

Otimize a travessia de arestas

A travessia de arestas é o processo de navegação num gráfico seguindo as respetivas arestas, começando num nó específico e movendo-se ao longo das arestas ligadas para alcançar outros nós. O esquema define a direção da aresta. A travessia de arestas é uma operação fundamental no Spanner Graph, pelo que a melhoria da eficiência da travessia de arestas pode melhorar significativamente o desempenho da sua aplicação.

Pode percorrer um limite em duas direções:

  • O deslocamento da extremidade para a frente segue as extremidades de saída do nó de origem.
  • A travessia de arestas inversa segue as arestas de entrada do nó de destino.

Exemplos de consultas de travessia de arestas para a frente e para trás

A seguinte consulta de exemplo executa a travessia de arestas para a frente de arestas Owns para uma determinada pessoa:

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

A seguinte consulta de exemplo executa a travessia de arestas inversa de arestas Owns para uma determinada conta:

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

Otimize o percurso de arestas para a frente

Para melhorar o desempenho da travessia de arestas para a frente, otimize a travessia da origem para a aresta e da aresta para o destino.

  • Para otimizar o percurso da origem para o limite, intercale a tabela de entrada de limites na tabela de entrada de nós de origem usando a cláusula INTERLEAVE IN PARENT. A intercalação é uma técnica de otimização do armazenamento no Spanner que coloca as linhas da tabela secundária com as linhas principais correspondentes no armazenamento. Para mais informações sobre a intercalação, consulte a vista geral dos esquemas.

  • Para otimizar a travessia da aresta para o destino, crie uma restrição de chave externa entre a aresta e o nó de destino
    . Isto aplica a restrição de origem a destino, o que pode melhorar o desempenho eliminando as análises da tabela de destino. Se as chaves externas aplicadas causarem gargalos no desempenho de escrita (por exemplo, ao atualizar nós de hub), use uma chave externa informativa em alternativa.

Os exemplos seguintes mostram como usar a intercalação com uma restrição de chave externa aplicada e uma restrição de chave externa informativa.

Chave externa aplicada

Neste exemplo de tabela de relações, PersonOwnAccount faz o seguinte:

  • Intercala na tabela de nós de origem Person.

  • Cria uma chave externa aplicada à tabela de nós 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;

Chave externa informativa

Neste exemplo de tabela de relações, PersonOwnAccount faz o seguinte:

  • Intercala na tabela de nós de origem Person.

  • Cria uma chave externa informativa para a tabela de nós 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;

Otimize a travessia de arestas invertidas

Otimize a travessia de arestas invertidas, a menos que as suas consultas usem apenas a travessia para a frente, porque as consultas que envolvem a travessia invertida ou bidirecional são comuns.

Para otimizar a travessia de arestas invertidas, pode fazer o seguinte:

  • Crie um índice secundário na tabela de arestas.

  • Intercale o índice na tabela de entrada do nó de destino para colocar as arestas juntamente com os nós de destino.

  • Armazenar as propriedades das arestas no índice.

Este exemplo mostra um índice secundário para otimizar a travessia de arestas inversas para a tabela de arestas PersonOwnAccount:

  • A cláusula INTERLEAVE IN coloca os dados de índice com a tabela de nós de destino Account.

  • A cláusula STORING armazena propriedades de arestas no índice.

Para mais informações sobre a intercalação de índices, consulte o artigo Índices e intercalação.

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;

Use índices secundários para filtrar propriedades

Um índice secundário permite uma pesquisa eficiente de nós e arestas com base em valores de propriedades específicos. A utilização de um índice ajuda a evitar uma análise completa da tabela e é especialmente útil para grafos grandes.

Acelere a filtragem de nós por propriedade

A seguinte consulta que encontra contas para um determinado pseudónimo. Como não usa um índice secundário, todos os nós Account têm de ser analisados para encontrar os resultados correspondentes:

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

Crie um índice secundário na propriedade filtrada no seu esquema para acelerar o processo de filtragem:

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

Acelere a filtragem de arestas por propriedade

Pode usar um índice secundário para melhorar o desempenho da filtragem de arestas com base nos valores das propriedades.

Deslocamento da aresta para a frente

Sem um índice secundário, esta consulta tem de analisar todas as arestas de uma pessoa para encontrar as arestas que correspondem ao 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;

O código seguinte melhora a eficiência da consulta através da criação de um índice secundário na referência do nó de origem da aresta (id) e na propriedade da aresta (create_time). A consulta também define o índice como um filho intercalado da tabela de entrada do nó de origem, que coloca o índice com o nó de origem.

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;

Deslocamento inverso de arestas

Sem um índice secundário, a seguinte consulta de travessia de arestas inversa tem de ler todas as arestas antes de poder encontrar a pessoa proprietária da conta especificada após o 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;

O código seguinte melhora a eficiência da consulta através da criação de um índice secundário na referência do nó de destino da aresta (account_id) e na propriedade da aresta (create_time). A consulta também define o índice como o filho intercalado da tabela de nós de destino, que coloca o índice com o nó 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;

Evite extremidades pendentes

Um limite que liga zero ou um nó, um limite pendente, pode comprometer a eficiência da consulta do Spanner Graph e a integridade da estrutura do gráfico. Pode ocorrer uma aresta pendente se eliminar um nó sem eliminar as respetivas arestas associadas. Também pode ocorrer uma aresta pendente se criar uma aresta, mas o respetivo nó de origem ou de destino não existir. Para evitar arestas pendentes, incorpore o seguinte no esquema do Spanner Graph:

Use restrições referenciais

Pode usar a intercalação e as chaves externas aplicadas em ambos os pontos finais para evitar arestas pendentes seguindo estes passos:

  1. Intercale a tabela de entrada de arestas na tabela de entrada do nó de origem para garantir que o nó de origem de uma aresta existe sempre.

  2. Crie uma restrição de chave externa aplicada em arestas para garantir que o nó de destino de uma aresta existe sempre. Embora as chaves estrangeiras aplicadas impeçam arestas pendentes, tornam a inserção e a eliminação de arestas mais dispendiosas.

O exemplo seguinte usa uma chave externa aplicada e intercala a tabela de entrada de arestas na tabela de entrada do nó de origem através da cláusula INTERLEAVE IN PARENT. Em conjunto, a utilização de uma chave externa aplicada e a intercalação também podem ajudar a otimizar a travessia de arestas para a frente.

  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;

Elimine arestas com ON DELETE CASCADE

Quando usa a intercalação ou uma chave externa aplicada para evitar arestas pendentes, use a cláusula ON DELETE CASCADE no esquema do gráfico do Spanner para eliminar as arestas associadas de um nó na mesma transação que elimina o nó. Para mais informações, consulte os artigos Elimine a cascata para tabelas intercaladas e Ações de chaves externas.

Eliminação em cascata para arestas que ligam diferentes tipos de nós

Os exemplos seguintes mostram como usar ON DELETE CASCADE no esquema do gráfico do Spanner para eliminar arestas pendentes quando elimina um nó de origem ou de destino. Em ambos os casos, o tipo do nó eliminado e o tipo do nó ligado a este por uma aresta são diferentes.

Nó de origem

Use a intercalação para eliminar arestas pendentes quando o nó de origem é eliminado. O exemplo seguinte mostra como usar a intercalação para eliminar as arestas de saída quando o nó de origem (Person) é eliminado. Para mais informações, consulte o artigo Crie tabelas 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

Nó de destino

Use uma restrição de chave externa para eliminar arestas pendentes quando o nó de destino é eliminado. O exemplo seguinte mostra como usar uma chave externa com ON DELETE CASCADE numa tabela de arestas para eliminar arestas recebidas quando o nó de destino (Account) é eliminado:

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

Eliminação em cascata para arestas que ligam o mesmo tipo de nós

Quando os nós de origem e de destino de uma aresta são do mesmo tipo e a aresta está intercalada no nó de origem, pode definir ON DELETE CASCADE para o nó de origem ou de destino, mas não para ambos.

Para evitar arestas pendentes nestes cenários, não entrelace na tabela de entrada do nó de origem. Em alternativa, crie duas chaves externas aplicadas nas referências dos nós de origem e de destino.

O exemplo seguinte usa AccountTransferAccount como a tabela de entrada de arestas. Define duas chaves externas, uma em cada nó final da aresta de transferência, ambas com a ação 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);

Configure o tempo de vida (TTL) em nós e limites

O TTL permite-lhe fazer expirar e remover dados após um período especificado. Pode usar o TTL no seu esquema para manter o tamanho e o desempenho da base de dados removendo dados com uma duração ou uma relevância limitadas. Por exemplo, pode configurá-lo para remover informações de sessões, caches temporárias ou registos de eventos.

O exemplo seguinte usa o TTL para eliminar contas 90 dias após o respetivo encerramento:

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

Quando define uma política de TTL numa tabela de nós, tem de configurar a forma como as arestas relacionadas são processadas para evitar arestas pendentes não intencionais:

  • Para tabelas de arestas intercaladas: se uma tabela de arestas estiver intercalada na tabela de nós, pode definir a relação de intercalação com ON DELETE CASCADE. Isto garante que, quando o TTL elimina um nó, as respetivas arestas intercaladas associadas também são eliminadas.

  • Para tabelas de arestas com chaves externas: se uma tabela de arestas fizer referência à tabela de nós com uma chave externa, tem duas opções:

    • Para eliminar automaticamente arestas quando o nó referenciado é eliminado pelo TTL, use ON DELETE CASCADE na chave externa. Isto mantém a integridade referencial.
    • Para permitir que as arestas permaneçam após a eliminação do nó referenciado (criando uma aresta pendente), defina a chave externa como uma chave externa informativa.

No exemplo seguinte, a tabela de arestas AccountTransferAccount está sujeita a duas políticas de eliminação de dados:

  • Uma política de TTL elimina registos de transferências com mais de dez anos.
  • A cláusula ON DELETE CASCADE elimina todos os registos de transferências associados a uma origem quando essa conta é eliminada.
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));

Unir tabelas de entrada de nós e arestas

Para otimizar o seu esquema, defina um nó e as respetivas arestas de entrada ou saída numa única tabela. Esta abordagem oferece as seguintes vantagens:

  • Menos tabelas: reduz o número de tabelas no seu esquema, o que simplifica a gestão de dados.

  • Desempenho das consultas melhorado: elimina a travessia que usa junções a uma tabela de arestas separada.

Esta técnica funciona bem quando a chave principal de uma tabela também define uma relação com outra tabela. Por exemplo, se a tabela Account tiver uma chave primária composta (owner_id, account_id), a parte owner_id pode ser uma chave externa que faz referência à tabela Person. Esta estrutura permite que a tabela Account represente o nó Account e a aresta de entrada do nó 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);

Pode usar a tabela Account para definir o nó Account e a respetiva aresta Owns de entrada. Isto é apresentado na seguinte CREATE PROPERTY GRAPH declaração. Na cláusula EDGE TABLES, atribui o alias Owns à tabela Account. Isto deve-se ao facto de cada elemento no esquema do gráfico ter de ter um nome exclusivo.

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

O que se segue?