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áfico do Spanner. O design do esquema pode ser iterativo, por isso recomendamos que você primeiro identifique padrões críticos de consulta 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 pelas arestas conectadas para alcançar outros nós. A travessia de arestas é uma operação fundamental no gráfico do Spanner, melhorar a eficiência da travessia de borda é essencial para o desempenho do seu aplicativo.

É possível atravessar uma aresta em duas direções: a travessia do nó de origem para o nó de destino é chamada de travessia de aresta para frente, enquanto a travessia do nó de destino para o nó de origem é chamada de travessia de aresta reversa.

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

Para melhorar o desempenho de 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 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 transferir arestas invertidas com eficiência, crie uma restrição de chave estrangeira entre entre a borda e o nó de destino. Essa chave externa cria automaticamente índice secundário na borda codificadas 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;

Otimizar a travessia de borda reversa usando o índice secundário

Se você não quiser criar uma chave estrangeira 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);

Não permitir bordas pendentes

Uma borda pendurada é uma borda que 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.

Não permitir essa opção 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.
  • Criar uma restrição de chave externa nas bordas para garantir que o nó de destino de uma borda sempre existe.

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 automaticamente bordas ao excluir um nó

Quando você usa a intercalação ou uma chave estrangeira 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): excluindo um um nó com bordas vai 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

  • Excluir bordas 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 Crie 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 PersonOwnAccount bordas de entrada no nó Account que está sendo excluído. Para mais informações, consulte Chaves estrangeiras.

Excluir cascata para bordas 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 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 nó ou propriedades de borda 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ê está trabalhando com gráficos grandes, porque a passagem de todos os nós e bordas pode ser muito ineficiente.

Acelerar a filtragem de nós por propriedade

Para acelerar a filtragem por propriedades do nó, crie índices secundários em 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 AccountByEmail
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

Quando você atravessa uma borda enquanto filtra suas propriedades, pode acelerar a consulta criando um índice secundário nas propriedades da borda e intercalando o índice para o nó de origem.

Por exemplo, a consulta a seguir encontra contas que pertencem a uma determinada pessoa depois de 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.

Acelere a travessia de borda reversa com filtragem nas 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.

A consulta de exemplo a seguir realiza a travessia de borda reversa com filtragem ativada propriedades de borda:

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 com um índice secundário, use uma das opções a seguir 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 melhor desempenho porque as bordas invertidas são classificados por account_id e create_time, o que permite que o mecanismo de consulta encontrar com eficiência arestas para account_id que atendam à condição create_time. No entanto, se padrões de consulta diferentes filtrarem por diferentes propriedades, cada uma delas pode exigir um índice separado, que pode adicionar sobrecarga.

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

    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, ela precisa ler todas as bordas do nó de destino e filtrar as propriedades de borda.

É possível combinar essas abordagens seguindo estas diretrizes:

  • Use as propriedades de borda em colunas de índice se elas forem usadas em consultas críticas para o desempenho.
  • Adicione as propriedades usadas em consultas menos sensíveis à performance no das colunas de armazenamento.

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

Os tipos de nó e borda geralmente são 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.

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

Melhorar o gerenciamento de esquemas

Gerenciar uma entrada separada se o gráfico tiver muitos tipos de nó e borda para cada um deles pode ser 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

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

Se precisar alterar o esquema com frequência, recomendamos que você modele a digite uma propriedade para contornar limitações na frequência do esquema atualizações.

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 do SavingsAccount e InvestmentAccount pertencem a um Person, supondo que a conta são 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 este for um consulta que tem alto 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 mudar durante o ciclo de vida do nó, siga estas etapas:

  1. Inclua a propriedade como parte da chave do elemento do nó.
  2. Adicione o tipo de nó à tabela de entrada da 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 ao 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 bordas

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 geralmente é usado para dados que têm uma limitação vida útil ou relevância, como informações da sessão, caches temporários ou ou de sistemas operacionais de contêineres. 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 a interdição:

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, o intercalamento precisará ser definido 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, a transferência o histórico também será 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

É possível usar a mesma tabela de entrada para definir mais de um nó e borda no 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, use a tabela de entrada Account para definir a Account. e a borda PersonOwnAccount, conforme mostrado no exemplo de esquema a seguir. 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