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

Este documento descreve como criar consultas eficientes usando as práticas recomendadas para projetar esquemas de grafos do Spanner. Você pode iterar o design do esquema. Por isso, recomendamos que você primeiro identifique os padrões de consulta essenciais para orientar o design do esquema.

Para informações gerais sobre as práticas recomendadas de design de esquema do Spanner, consulte Práticas recomendadas de design de esquema.

Otimizar a travessia de borda

A transversal de arestas é o processo de navegação por um gráfico seguindo as arestas, começando em um nó específico e se movendo ao longo de arestas conectadas para alcançar outros nós. A direção da borda é definida pelo esquema. A travessia de arestas é uma operação fundamental no Spanner Graph. Portanto, melhorar a eficiência da travessia de arestas é fundamental para o desempenho do aplicativo.

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

  • Transposição de borda para frente: segue as bordas de saída do nó de origem.

  • Transversal de borda reversa: segue as bordas de entrada do nó de destino.

Dada uma pessoa, a consulta de exemplo a seguir realiza a travessia de arestas para frente de arestas Owns:

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

Dada uma conta, a consulta de exemplo a seguir realiza a travessia reversa de arestas de Owns:

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

Otimizar a travessia de borda para frente usando a intercalação

Para melhorar a performance da travessia de borda para frente, intercale a tabela de entrada de borda na tabela de entrada do nó de origem para colocar bordas com nós de origem. A intercalação é uma técnica de otimização de armazenamento no Spanner que coloca fisicamente as linhas de tabelas filhas com as linhas mãe correspondentes no armazenamento. Para mais informações sobre a intercalação, consulte Visão geral dos esquemas.

O exemplo a seguir demonstra essas práticas 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;

Otimizar a travessia de borda reversa usando chave externa

Para percorrer as arestas reversas com eficiência, crie uma restrição de chave estrangeira entre a aresta e o nó de destino. Essa chave externa cria automaticamente um índice secundário na borda com chave pelas chaves do nó de destino. O índice secundário é usado automaticamente durante a execução da consulta.

O exemplo a seguir demonstra essas práticas 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;

Otimização da travessia de borda reversa usando o índice secundário

Se você não quiser criar uma chave externa na borda, por exemplo, devido à integridade rígida de dados que ela impõe, crie um índice secundário diretamente na tabela de entrada da borda, conforme mostrado no exemplo a seguir:

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

Desativar bordas soltas

Uma borda solta conecta menos de dois nós. Uma aresta pendente pode ocorrer quando um nó é excluído sem remover as arestas associadas ou quando uma aresta é criada sem ser vinculada corretamente aos nós.

A proibição de bordas soltas oferece os seguintes benefícios:

  • Aplica a integridade da estrutura do gráfico.
  • Melhora a performance da consulta evitando o trabalho extra para filtrar arestas em que os endpoints não existem.

Não permitir bordas soltas usando restrições referenciais

Para não permitir bordas soltas, especifique restrições nos dois terminais:

  • Intercalar a tabela de entrada de borda na tabela de entrada do nó de origem. Essa abordagem garante que o nó de origem de uma aresta sempre exista.
  • Crie uma restrição chave externa nas bordas para garantir que o nó de destino de uma borda sempre exista.

O exemplo a seguir usa intercalação e uma chave externa para aplicar integridade 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;

Use ON DELETE CASCADE para remover arestas automaticamente ao excluir um nó

Quando você usa o intercalamento ou uma chave externa para impedir arestas soltas, use a cláusula ON DELETE para controlar o comportamento quando quiser excluir um nó com arestas ainda anexadas. Para mais informações, consulte Excluir em cascata para tabelas intercaladas e Excluir em cascata com chaves externas.

É possível usar ON DELETE das seguintes maneiras:

  • ON DELETE NO ACTION (ou omitindo a cláusula ON DELETE): a exclusão de um nó com arestas falhará.
  • ON DELETE CASCADE: a exclusão de um nó remove automaticamente as arestas associadas na mesma transação.

Excluir cascata para arestas que conectam diferentes tipos de nós

  • Exclua as arestas quando o nó de origem for excluído. Por exemplo,INTERLEAVE IN PARENT Person ON DELETE CASCADE exclui todas as arestas PersonOwnAccount de saída do nó Person que está sendo excluído. Para mais informações, consulte Criar tabelas intercaladas.

  • Excluir arestas quando o nó de destino for excluído. Por exemplo, CONSTRAINT FK_Account FOREIGN KEY(account_id) REFERENCES Account(id) ON DELETE CASCADE exclui todas as arestas PersonOwnAccount recebidas no nó Account que está sendo excluído. Para mais informações, consulte Chaves externas.

Excluir cascata para arestas que conectam o mesmo tipo de nós

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

Para remover automaticamente as bordas soltas em ambos os casos, crie uma chave externa na referência do nó de origem da borda em vez de intercalar a tabela de entrada de bordas na tabela de entrada do nó de origem.

Recomendamos o intercalamento para otimizar a travessia de borda para frente. Verifique o impacto nas suas cargas de trabalho antes de continuar. Confira o exemplo abaixo, que usa AccountTransferAccount como a tabela de entrada de borda:

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

Filtrar por propriedades de nó ou aresta com índices secundários

Os índices secundários são essenciais para um processamento de consulta eficiente. Eles oferecem suporte a pesquisas rápidas de nós e arestas com base em valores de propriedade específicos, sem precisar percorrer toda a estrutura do gráfico. Isso é importante quando você trabalha com gráficos grandes, porque a travessia de todos os nós e arestas pode ser muito ineficiente.

Acelerar a filtragem de nós por propriedade

Para acelerar a filtragem por propriedades do nó, crie índices secundários nas propriedades. Por exemplo, a consulta a seguir encontra contas para um determinado apelido. Sem um índice secundário, todos os nós Account são verificados para corresponder aos critérios de filtragem.

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

Para acelerar a consulta, crie um índice secundário na propriedade filtrada, conforme mostrado no exemplo abaixo:

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

Dica:use índices filtrados por NULL para propriedades esparsas. Para mais informações, consulte Desativar a indexação de valores NULL.

Acelerar a travessia de borda para frente com filtragem em propriedades de borda

Ao percorrer uma aresta enquanto filtra as propriedades dela, é possível acelerar a consulta criando um índice secundário nas propriedades da aresta e intercalando o índice no nó de origem.

Por exemplo, a consulta a seguir encontra contas de uma determinada pessoa após um determinado período:

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;

Por padrão, essa consulta lê todas as arestas da pessoa especificada e filtra as arestas que atendem à condição em create_time.

O exemplo a seguir mostra como melhorar a eficiência da consulta criando um índice secundário na referência do nó de origem de borda (id) e na propriedade de borda (create_time). Intercale o índice na tabela de entrada do nó de origem para colocar 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;

Com essa abordagem, a consulta pode encontrar com eficiência todas as arestas que atendem à condição em create_time.

Acelerar a travessia de borda reversa com filtragem em propriedades de borda

Ao percorrer uma borda reversa enquanto filtra as propriedades dela, é possível acelerar a consulta criando um índice secundário usando o nó de destino e as propriedades de borda para filtragem.

O exemplo de consulta a seguir realiza a travessia reversa de arestas com filtragem nas propriedades de aresta:

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 essa consulta usando um índice secundário, use uma das seguintes opções:

  • Crie um índice secundário na referência do nó de destino da borda (account_id) e na propriedade de borda (create_time), conforme mostrado no exemplo abaixo:

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

    Essa abordagem oferece um desempenho melhor porque as arestas reversas são ordenadas por account_id e create_time, o que permite que o mecanismo de consulta encontre com eficiência as arestas para account_id que atendem à condição em create_time. No entanto, se diferentes padrões de consulta forem filtrados em propriedades diferentes, cada propriedade poderá exigir um índice separado, o que pode aumentar a sobrecarga.

  • Crie um índice secundário na referência do nó de destino da borda (account_id) e armazene a propriedade de borda (create_time) em uma coluna de armazenamento, conforme mostrado no exemplo abaixo:

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

    Essa abordagem pode armazenar várias propriedades. No entanto, a consulta precisa ler todas as arestas do nó de destino e filtrar as propriedades de aresta.

É possível combinar essas abordagens seguindo estas diretrizes:

  • Use propriedades de borda em colunas de índice se elas forem usadas em consultas críticas para o desempenho.
  • Para propriedades usadas em consultas menos sensíveis ao desempenho, adicione-as às colunas de armazenamento.

Modelar tipos de nó e aresta com rótulos e propriedades

Os tipos de nó e aresta são comumente modelados com rótulos. No entanto, também é possível usar propriedades para tipos de modelo. Considere um exemplo em que há muitos tipos diferentes de contas, como BankAccount, InvestmentAccount e RetirementAccount. Você pode armazenar as contas em tabelas de entrada separadas e modelá-las como rótulos separados ou armazenar as contas em uma única tabela de entrada e usar uma propriedade para diferenciar os tipos.

Comece o processo de modelagem com os tipos com rótulos. Considere usar propriedades nos seguintes cenários.

Melhorar o gerenciamento de esquemas

Se o gráfico tiver muitos tipos diferentes de nó e aresta, pode ser difícil gerenciar uma tabela de entrada separada para cada um. Para facilitar o gerenciamento de esquemas, modele o tipo como uma propriedade.

Tipos de modelo em uma propriedade para gerenciar tipos que mudam com frequência

Quando você modela tipos como rótulos, a adição ou remoção de tipos exige mudanças no esquema. Se você executar muitas atualizações de esquema em um curto período, o Spanner poderá limitar o processamento de atualizações de esquema em fila. Para mais informações, consulte Limitar a frequência das atualizações de esquema.

Se você precisar mudar o esquema com frequência, recomendamos modelar o tipo em uma propriedade para contornar as limitações na frequência das atualizações do esquema.

Acelerar consultas

A modelagem de tipos com propriedades pode acelerar as consultas quando o padrão de nó ou aresta faz referência a vários rótulos. O exemplo de consulta a seguir encontra todas as instâncias de SavingsAccount e InvestmentAccount pertencentes a um Person, supondo que os tipos de conta sejam modelados com rótulos:

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

O padrão de nó acct faz referência a dois rótulos. Se essa for uma consulta crítica para o desempenho, considere modelar Account usando uma propriedade. Essa abordagem pode melhorar a performance da consulta, conforme mostrado no exemplo de consulta a seguir. Recomendamos que você faça a comparação das duas consultas.

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

Armazenar o tipo na chave do elemento do nó para acelerar as consultas

Para acelerar as consultas com filtragem no tipo de nó quando um tipo de nó é modelado com uma propriedade e o tipo não muda ao longo da vida útil do nó, siga estas etapas:

  1. Inclua a propriedade como parte da chave do elemento do nó.
  2. Adicione o tipo de nó na tabela de entrada de borda.
  3. Inclua o tipo de nó nas chaves de referência de borda.

O exemplo a seguir aplica essa otimização ao nó Account e à aresta 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
  );

Configurar o TTL em nós e arestas

O time to live (TTL) do Spanner é um mecanismo que oferece suporte à expiração e remoção automática de dados após um período especificado. Isso é usado com frequência para dados com vida útil ou relevância limitada, como informações de sessão, caches temporários ou registros de eventos. Nesses casos, o TTL ajuda a manter o tamanho e o desempenho do banco de dados.

O exemplo a seguir usa o TTL para excluir contas 90 dias após o fechamento:

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

Se a tabela de nós tiver um TTL e uma tabela de borda intercalada, a intercalagem precisa ser definida com ON DELETE CASCADE. Da mesma forma, se a tabela de nós tiver um TTL e for referenciada por uma tabela de aresta por meio de uma chave externa, a chave externa precisa ser definida com ON DELETE CASCADE.

No exemplo abaixo, AccountTransferAccount é armazenado por até 10 anos enquanto a conta permanece ativa. Quando uma conta é excluída, o histórico de transferência também é excluído.

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 bordas e nós

Você pode usar a mesma tabela de entrada para definir mais de um nó e aresta no seu esquema.

Nas tabelas de exemplo a seguir, os nós Account têm uma chave composta (owner_id, account_id). Há uma definição de aresta implícita, o nó Person com a chave (id) é proprietário do nó Account com a chave composta (owner_id, account_id) quando id é 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);

Nesse caso, você pode usar a tabela de entrada Account para definir o nó Account e a borda PersonOwnAccount, conforme mostrado no exemplo de esquema abaixo. Para garantir que todos os nomes de tabela de elementos sejam exclusivos, o exemplo atribui o alias Owns à definição da tabela de bordas.

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