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 gráficos do Spanner. É possível iterar no design do esquema. Por isso, recomendamos que você primeiro identifique padrões de consulta críticos 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 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 percorrendo as arestas conectadas para alcançar outros nós. A direção da aresta é definida pelo esquema. A travessia de arestas é uma operação fundamental no Spanner Graph. Por isso, melhorar a eficiência dela é essencial para o desempenho do aplicativo.

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

  • percurso de borda de encaminhamento: segue as bordas de saída do nó de origem.

  • percurso de borda inversa: segue as bordas de entrada do nó de destino.

Considerando uma pessoa, a consulta de exemplo a seguir realiza a travessia de arestas diretas de arestas Owns:

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

Considerando uma conta, a consulta de exemplo a seguir realiza a travessia de arestas inversas de arestas Owns:

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

Otimizar a travessia de borda direta usando intercalação

Para melhorar o desempenho da travessia de arestas diretas, intercale a tabela de entrada de arestas na tabela de entrada de nós de origem para alocar arestas com nós de origem. A intercalação é uma técnica de otimização de armazenamento no Spanner que coloca fisicamente as linhas da tabela filha com as linhas pai correspondentes no armazenamento. Para mais informações sobre 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 arestas inversas usando chave externa

Para percorrer arestas invertidas com eficiência, crie uma restrição de chave estrangeira aplicada entre a aresta e o nó de destino. Essa chave externa aplicada cria um índice secundário na aresta com chaves dos nós 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;

Otimizar a travessia de arestas inversas usando um índice secundário

Se você não quiser criar uma chave externa obrigatória na aresta, por exemplo, devido à integridade estrita dos dados que ela impõe, crie diretamente um índice secundário na tabela de entrada da aresta, 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 AccountOwnedByPerson
ON PersonOwnAccount (account_id), INTERLEAVE IN Account;

INTERLEAVE IN declara uma relação de localidade de dados entre o índice secundário e a tabela em que ele está intercalado (Account, no exemplo). Com o entrelaçamento, as linhas do índice secundário AccountOwnedByPerson são colocalizadas com as linhas correspondentes da tabela Account. Para mais informações sobre intercalação, consulte Relacionamentos de tabelas pai-filho. Para mais informações sobre a intercalação de índices, consulte Índices e intercalação.

Otimizar a travessia de arestas usando chaves estrangeiras informativas

Se o seu cenário tiver gargalos de desempenho de gravação causados por chaves estrangeiras obrigatórias, como quando você tem atualizações frequentes em nós de hub com muitas arestas conectadas, considere usar chaves estrangeiras informativas. Usar chaves estrangeiras informativas nas colunas de referência de uma tabela de arestas ajuda o otimizador de consultas a descartar verificações redundantes de tabelas de nós. No entanto, como as chaves estrangeiras informativas não exigem índices secundários na tabela de arestas, elas não melhoram a velocidade das pesquisas quando uma consulta tenta encontrar arestas usando nós finais. Para mais informações, consulte Comparação entre chave externa externas.

É importante entender que, se o aplicativo não puder garantir a integridade referencial, o uso de chaves estrangeiras informativas para otimização de consultas poderá levar a resultados incorretos.

O exemplo a seguir cria uma tabela com uma chave externa informativa na coluna 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;

Se o intercalamento não for uma opção, marque as duas referências de aresta com chaves estrangeiras informativas, como no exemplo a seguir:

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

Não permitir bordas soltas

Uma aresta pendente é uma aresta que conecta menos de dois nós. Uma aresta solta pode ocorrer quando um nó é excluído sem remover as arestas associadas ou quando uma aresta é criada sem vinculá-la corretamente aos nós.

Não permitir arestas pendentes oferece os seguintes benefícios:

  • Impõe a integridade da estrutura do gráfico.
  • Melhora o desempenho da consulta evitando o trabalho extra de filtrar arestas em que os endpoints não existem.

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

Para não permitir arestas soltas, especifique restrições nos dois endpoints:

  • Intercale a tabela de entrada de aresta 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 obrigatória nas arestas para garantir que o nó de destino de uma aresta sempre exista.

O exemplo a seguir usa intercalação e uma chave externa aplicada para impor a 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 automaticamente as arestas ao excluir um nó

Ao usar o intercalamento ou uma chave externa forçada para não permitir 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 Exclusão em cascata para tabelas intercaladas e Ações de chave externa.

É possível usar ON DELETE das seguintes maneiras:

  • ON DELETE NO ACTION (ou omitir a cláusula ON DELETE): a exclusão de um nó com arestas vai falhar.
  • ON DELETE CASCADE: ao excluir um nó, as arestas associadas são removidas automaticamente na mesma transação.

Exclusão em 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.

  • Exclua as 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 de entrada no nó Account que está sendo excluído.

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 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 arestas soltas nos dois casos, crie uma chave externa obrigatória na referência do nó de origem da aresta em vez de intercalar a tabela de entrada da aresta na tabela de entrada do nó de origem.

Recomendamos o intercalamento para otimizar o percurso da borda direta. Verifique o impacto nas suas cargas de trabalho antes de continuar. Confira o exemplo a seguir, que usa AccountTransferAccount como a tabela de entrada de aresta:

--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 o processamento eficiente de consultas. 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 percorrer todos os nós e arestas pode ser muito ineficiente.

Acelerar a filtragem de nós por propriedade

Para acelerar a filtragem por propriedades de nós, 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 a seguir:

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.

Acelere a travessia da borda de encaminhamento com a filtragem nas propriedades da 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 aquelas 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 da aresta (id) e na propriedade da aresta (create_time). Intercale o índice na tabela de entrada do nó de origem para alocar 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 inversa com filtragem nas propriedades de borda

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

A consulta de exemplo a seguir realiza a travessia de arestas inversa com filtragem nas propriedades de arestas:

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 de borda (account_id) e na propriedade de borda (create_time), 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 PersonOwnAccountByCreateTime
    ON PersonOwnAccount (account_id, create_time);
    

    Essa abordagem oferece melhor desempenho porque as arestas invertidas são classificadas por account_id e create_time, o que permite que o mecanismo de consulta encontre arestas de account_id que atendam à condição em create_time de maneira eficiente. No entanto, se diferentes padrões de consulta filtrarem 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 da borda (create_time) em uma coluna de armazenamento, 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 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 nas colunas de armazenamento.

Modelar tipos de nós e arestas com rótulos e propriedades

Os tipos de nós e arestas são geralmente modelados com rótulos. No entanto, também é possível usar propriedades para modelar tipos. 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 de rótulos. Considere usar propriedades nos seguintes cenários.

Melhorar o gerenciamento de esquemas

Se o gráfico tiver muitos tipos diferentes de nós e arestas, gerenciar uma tabela de entrada separada para cada um pode ficar difícil. 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

Ao modelar tipos como rótulos, adicionar ou remover tipos exige mudanças no esquema. Se você fizer muitas atualizações de esquema em um curto período, o Spanner poderá limitar o processamento das atualizações de esquema em fila. Para mais informações, consulte Limitar a frequência de 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 de 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. A consulta de exemplo 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 com desempenho crítico, considere modelar Account usando uma propriedade. Essa abordagem pode oferecer um desempenho melhor da consulta, conforme mostrado no exemplo a seguir. Recomendamos que você faça o comparativo 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 de nó para acelerar as consultas

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

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

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 TTL em nós e arestas

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

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

Se a tabela de nós tiver um TTL e uma tabela de arestas intercalada, a intercalação precisará 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 arestas usando uma chave estrangeira, a chave externa precisará ser definida com ON DELETE CASCADE para manter a integridade referencial ou como uma chave externa informativa para permitir a existência de uma aresta solta.

No exemplo a seguir, AccountTransferAccount é armazenado por até dez anos enquanto uma conta permanece ativa. Quando uma conta é excluída, o histórico de transferências 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 nós e arestas

É possível usar a mesma tabela de entrada para definir mais de um nó e uma 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 chave (id) é proprietário do nó Account com 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, use a tabela de entrada Account para definir o nó Account e a aresta PersonOwnAccount, como mostrado no exemplo de esquema a seguir. Para garantir que todos os nomes de tabelas de elementos sejam exclusivos, o exemplo atribui o alias Owns à definição da tabela de arestas.

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