Práticas recomendadas para projetar um esquema de gráfico do Spanner

Este documento descreve as práticas recomendadas para projetar um esquema de gráfico do Spanner, com foco em consultas eficientes, travessia de arestas otimizada e técnicas eficazes de gerenciamento de dados.

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

Escolher um design de esquema

O design do esquema afeta a performance do gráfico. Os tópicos a seguir ajudam você a escolher uma estratégia eficaz.

Designs com e 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 mudanças de definição pouco frequentes. O esquema impõe a definição do gráfico, e as propriedades são compatíveis com todos os tipos de dados do Spanner.

  • Um design sem esquema infere a definição do gráfico dos dados, oferecendo mais flexibilidade sem exigir mudanças de esquema. Os rótulos e propriedades dinâmicos não são aplicados por padrão. As propriedades precisam ser valores JSON válidos.

A seguir, resumimos as principais diferenças entre o gerenciamento de dados com e sem esquema. Além disso, considere suas consultas de gráfico para ajudar a decidir qual tipo de esquema usar.

Recurso Gerenciamento de dados esquematizados Gerenciamento de dados sem esquema
Como 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 nos dados. No entanto, o Spanner Graph não inspeciona os dados para inferir a definição.
Atualizando a definição do gráfico Requer uma mudança no esquema do gráfico do Spanner. Adequado quando a definição é bem definida e muda com pouca frequência. Não é necessário mudar o esquema do Spanner Graph.
Como aplicar a definição de gráfico Um esquema de gráfico de propriedades impõe os tipos de nós permitidos para uma aresta. Ele também aplica as propriedades e os tipos de propriedades permitidos de um nó ou tipo de aresta do gráfico. Não é aplicada por padrão. É possível usar restrições de verificação para garantir a integridade dos dados de rótulo e propriedade.
Tipos de dados de propriedade Compatível com qualquer tipo de dados do Spanner, por exemplo, timestamp. Propriedades dinâmicas precisam ser um valor JSON válido.

Escolher um design de esquema com base em consultas de gráfico

Projetos esquematizados e sem esquema geralmente oferecem desempenho comparável. A exceção é quando as consultas usam padrões de caminho quantificados que abrangem vários tipos de nós ou arestas. Nesse caso, um design sem esquema oferece melhor desempenho. Isso acontece porque os projetos sem esquema armazenam todos os dados em tabelas de nós e arestas únicas, o que minimiza as verificações de tabelas. Por outro lado, os projetos esquematizados usam tabelas separadas para cada tipo de nó e aresta. Portanto, as consultas que abrangem vários tipos precisam verificar e combinar dados de todas as tabelas correspondentes.

Confira a seguir exemplos de consultas que funcionam bem com designs sem esquema e uma consulta que funciona bem com os dois designs:

Design sem esquema

As consultas a seguir têm melhor desempenho com um design sem esquema porque usam padrões de caminho 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 intermediários 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 intermediários para caminhos mais longos. Isso permite que os caminhos atravessem nós intermediários 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;
    
  • Essa consulta encontra caminhos de um a três saltos entre nós Account usando arestas do tipo Owns em qualquer direção (-[:Owns]-). Como os caminhos podem atravessar arestas em qualquer direção e os nós intermediários 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 consulta a seguir tem desempenho comparável com designs esquematizados e sem esquema. O caminho quantificado, (:Account)-[:Transfer]->{1,3}(:Account), envolve um tipo de nó, Account, e um tipo de aresta, Transfer. Como o caminho envolve apenas um tipo de nó e um tipo de aresta, o desempenho é comparável para os dois projetos. Embora os nós intermediários não sejam rotulados explicitamente, o padrão os restringe a serem nós Account. O nó Person aparece fora desse caminho quantificado.

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

Otimizar a performance do esquema do gráfico do Spanner

Depois de escolher usar um esquema do Spanner Graph esquematizado ou sem esquema, é possível otimizar a performance dele das seguintes maneiras:

Otimizar a travessia de arestas

O percurso de arestas é o processo de navegação em um gráfico seguindo as arestas dele, começando em um nó específico e movendo-se ao longo das arestas conectadas para alcançar outros nós. O esquema define a direção da borda. A travessia de arestas é uma operação fundamental no Spanner Graph. Por isso, melhorar a eficiência dela pode aumentar significativamente o desempenho do aplicativo.

É possível percorrer uma aresta em duas direções:

  • O percurso de borda de encaminhamento segue as bordas de saída do nó de origem.
  • O percurso de borda invertido segue as bordas de entrada do nó de destino.

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

A consulta de exemplo a seguir realiza a travessia de arestas de Owns para uma determinada pessoa:

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

A consulta de exemplo a seguir realiza a travessia de borda inversa das bordas Owns para uma determinada conta:

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

Otimizar a travessia da borda de encaminhamento

Para melhorar o desempenho do percurso da borda direta, otimize o percurso da origem até a borda e da borda até o destino.

  • Para otimizar a travessia da origem até a borda, intercale a tabela de entrada de borda na tabela de entrada do nó de origem usando a cláusula INTERLEAVE IN PARENT. A intercalação é uma técnica de otimização de armazenamento no Spanner que coloca as linhas da tabela filha com as linhas mãe correspondentes no armazenamento. Para mais informações sobre intercalação, consulte Visão geral dos esquemas.

  • Para otimizar a travessia da borda até o destino, crie uma restrição de chave estrangeira entre a borda e o nó de destino
    . Isso impõe a restrição de origem para destino, o que pode melhorar a performance eliminando verificações da tabela de destino. Se as chaves externas aplicadas causarem gargalos de desempenho de gravação (por exemplo, ao atualizar nós de hub), use uma chave externa informativa.

Os exemplos a seguir mostram como usar o intercalamento com uma restrição chave externa obrigatória e outra informativa.

Chave externa aplicada

Neste exemplo de tabela de borda, 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 borda, 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;

Otimizar a travessia de borda inversa

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

Para otimizar a travessia de arestas inversas, faça o seguinte:

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

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

  • Armazene as propriedades de aresta no índice.

Este exemplo mostra um índice secundário para otimizar a travessia de arestas inversas na 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 aresta no índice.

Para mais informações sobre intercalação de índices, consulte Í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;

Usar índices secundários para filtrar propriedades

Um índice secundário permite a pesquisa eficiente de nós e arestas com base em valores de propriedade específicos. O uso de um índice ajuda a evitar uma verificação completa da tabela e é especialmente útil para gráficos grandes.

Acelerar a filtragem de nós por propriedade

A consulta a seguir encontra contas para um apelido especificado. Como ele não usa um índice secundário, todos os nós Account precisam ser verificados 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 do 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);

Acelerar a filtragem de arestas por propriedade

É possível usar um índice secundário para melhorar o desempenho da filtragem de arestas com base em valores de propriedade.

Travessia de borda para frente

Sem um índice secundário, essa consulta precisa verificar todas as arestas de uma pessoa para encontrar as 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 a seguir melhora a eficiência da consulta criando 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;

Travessia de borda inversa

Sem um índice secundário, a seguinte consulta de travessia de aresta reversa precisa ler todas as arestas antes de 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 a seguir melhora a eficiência da consulta criando 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;

Evitar bordas penduradas

Uma aresta que conecta zero ou um nó, uma aresta pendente, pode comprometer a eficiência da consulta do Spanner Graph e a integridade da estrutura do gráfico. Uma aresta pendente pode ocorrer se você excluir um nó sem excluir as arestas associadas. Uma aresta solta também pode ocorrer se você criar uma aresta, mas o nó de origem ou de destino não existir. Para evitar arestas soltas, incorpore o seguinte no esquema do gráfico do Spanner:

Usar restrições referenciais

É possível usar o intercalamento e chaves estrangeiras aplicadas nos dois endpoints para evitar arestas soltas seguindo estas etapas:

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

  2. Crie uma restrição chave externa obrigatória nas arestas para garantir que o nó de destino de uma aresta sempre exista. Embora as chaves externas aplicadas evitem arestas soltas, elas tornam a inserção e a exclusão de arestas mais caras.

O exemplo a seguir usa uma chave externa aplicada e intercala a tabela de entrada de aresta na tabela de entrada do nó de origem usando a cláusula INTERLEAVE IN PARENT. Juntos, o uso de uma chave externa aplicada e a intercalação também podem ajudar a otimizar o percurso de borda direta.

  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;

Excluir arestas com ON DELETE CASCADE

Ao usar o intercalamento ou uma chave externa forçada para evitar arestas soltas, use a cláusula ON DELETE CASCADE no esquema de grafo do Spanner para excluir as arestas associadas de um nó na mesma transação que exclui o nó. Para mais informações, consulte Exclusão em cascata para tabelas intercaladas e Ações de chave externa.

Exclusão em cascata para arestas que conectam diferentes tipos de nós

Os exemplos a seguir mostram como usar ON DELETE CASCADE no esquema do Spanner Graph para excluir arestas soltas ao excluir um nó de origem ou destino. Em ambos os casos, o tipo do nó excluído e o tipo do nó conectado a ele por uma aresta são diferentes.

Nó de origem

Use o intercalamento para excluir arestas soltas quando o nó de origem for excluído. O exemplo a seguir mostra como usar o intercalamento para excluir as arestas de saída quando o nó de origem (Person) é excluído. Para mais informações, consulte Criar 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 chave externa para excluir arestas soltas quando o nó de destino for excluído. O exemplo a seguir mostra como usar uma chave externa com ON DELETE CASCADE em uma tabela de arestas para excluir arestas de entrada quando o nó de destino (Account) é excluído:

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

Exclusão em cascata para arestas que conectam o mesmo tipo de nós

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

Para evitar arestas soltas nesses cenários, não intercale na tabela de entrada do nó de origem. Em vez disso, crie duas chaves estrangeiras obrigatórias nas referências de nós de origem e destino.

O exemplo a seguir usa AccountTransferAccount como a tabela de entrada de arestas. Ele define duas chaves estrangeiras, uma em cada nó de extremidade 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);

Configurar o time to live (TTL) em nós e arestas

Com o TTL, é possível expirar e remover dados após um período especificado. É possível usar o TTL no seu esquema para manter o tamanho e a performance do banco de dados removendo dados com vida útil ou relevância limitadas. Por exemplo, é possível configurar para remover informações de sessão, caches temporários ou registros de eventos.

O exemplo a seguir usa o TTL para excluir contas 90 dias após o 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));

Ao definir uma política de TTL em uma tabela de nós, é necessário configurar como as arestas relacionadas são processadas para evitar arestas soltas não intencionais:

  • Para tabelas de arestas intercaladas:se uma tabela de arestas for intercalada na tabela de nós, você poderá definir a relação de intercalação com ON DELETE CASCADE. Isso garante que, quando o TTL exclui um nó, as arestas intercaladas associadas também sejam excluídas.

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

    • Para excluir automaticamente as arestas quando o nó referenciado for excluído por TTL, use ON DELETE CASCADE na chave externa. Isso mantém a integridade referencial.
    • Para permitir que as arestas permaneçam após a exclusão do nó referenciado (criando uma aresta pendente), defina a chave externa como uma chave estrangeira informativa.

No exemplo a seguir, a tabela de arestas AccountTransferAccount está sujeita a duas políticas de exclusão de dados:

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

Mesclar tabelas de entrada de nós e arestas

Para otimizar seu esquema, defina um nó e as arestas de entrada ou saída dele em uma única tabela. Essa abordagem oferece os seguintes benefícios:

  • Menos tabelas: reduz o número de tabelas no esquema, o que simplifica o gerenciamento de dados.

  • Melhoria na performance de consultas: elimina a travessia que usa junções em uma tabela de arestas separada.

Essa técnica funciona bem quando a chave primária 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 poderá ser uma chave externa que faz referência à tabela Person. Essa 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);

É possível usar a tabela Account para definir o nó Account e a aresta Owns de entrada. Isso é mostrado na seguinte instrução CREATE PROPERTY GRAPH. Na cláusula EDGE TABLES, você atribui o alias Owns à tabela Account. Isso acontece porque cada elemento no esquema do gráfico precisa 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
    );

A seguir