Práticas recomendadas de criação de esquema

Nesta página, descrevemos as práticas recomendadas para criar esquemas do Cloud Spanner com o objetivo de evitar pontos de acesso e carregar dados no Cloud Spanner.

Como escolher uma chave primária para evitar pontos de acesso

Conforme mencionado no Modelo de dados e esquema, tenha cuidado ao escolher uma chave primária para não criar, acidentalmente, pontos de acesso no banco de dados. Uma das causas para a criação acidental de pontos de acesso é o fato de haver uma coluna com valor que aumenta constantemente como a primeira parte da chave, porque isso resulta em todas as inserções ocorrerem no final do espaço da chave. Isso não é desejável, porque o Cloud Spanner divide dados entre servidores por intervalos de chave. Isso significa que todas as inserções serão direcionadas para um único servidor que acabará fazendo todo o trabalho.

Por exemplo, suponha que você queira manter uma última coluna de carimbo de data/hora de acesso nas linhas de dados Users. A definição de tabela a seguir que usa uma chave primária baseada no carimbo de data/hora como a primeira parte da chave é um antipadrão:

-- ANTI-PATTERN: USING A COLUMN WHOSE VALUE MONOTONICALLY INCREASES OR
-- DECREASES AS THE FIRST KEY PART

CREATE TABLE Users (
  LastAccess TIMESTAMP NOT NULL,
  UserId     INT64 NOT NULL,
  ...
) PRIMARY KEY (LastAccess, UserId);

O problema aqui é que as linhas serão gravadas nesta tabela na ordem do carimbo de data/hora do último acesso e, como esses carimbos estão sempre aumentando, eles são sempre escritos no final da tabela. O ponto de acesso é criado porque um único servidor do Cloud Spanner receberá todas as gravações, o que sobrecarrega esse servidor.

O diagrama abaixo ilustra essa armadilha:

Tabela de usuários ordenada pelo carimbo de data/hora com o ponto de acesso correspondente

A tabela Users acima inclui cinco linhas de dados de exemplo. Elas representam cinco usuários diferentes que realizam algum tipo de ação do usuário com cerca de um milésimo de segundo de diferença um do outro. O diagrama também indica a ordem em que as linhas são inseridas. As setas marcadas indicam a ordem das gravações para cada linha. Como as inserções são ordenadas pelo carimbo de data/hora e o valor dele está sempre aumentando, as inserções são sempre adicionadas ao final da tabela e direcionadas para a mesma divisão. Conforme discutido em Modelo de dados e esquema, uma divisão é um conjunto de linhas de uma ou mais tabelas relacionadas que são armazenadas por ordem da chave de linha.

Isso é problemático, porque o Cloud Spanner atribui o trabalho a servidores diferentes em unidades de divisão, de modo que o servidor atribuído a essa divisão específica acaba tratando todas as solicitações de inserção. À medida que a frequência dos eventos de acesso do usuário aumenta, o mesmo ocorre com a frequência das solicitações de inserção para o servidor correspondente. O servidor torna-se propenso a se tornar um ponto de acesso, conforme indicado pela borda vermelha e no segundo plano acima. Observe que, nesta ilustração simplificada, cada servidor lida com o máximo de uma divisão, mas, na realidade, cada servidor do Cloud Spanner pode ser atribuído a mais de uma divisão.

Quando mais linhas são anexadas à tabela, a divisão cresce e, quando atinge o tamanho máximo (aproximadamente 4 GB), o Cloud Spanner cria outra divisão, conforme descrito em Divisão baseada em carga. Novas linhas subsequentes são anexadas a essa nova divisão, e o servidor que é atribuído a ela se torna o novo ponto de acesso potencial.

Quando ocorrem pontos de acesso, as inserções tornam-se lentas e outros trabalhos no mesmo servidor podem ficar mais lentos. Alterar a ordem da coluna LastAccess para a ordem crescente não resolve esse problema porque isso faz com que todas as gravações sejam inseridas no topo da tabela, que ainda envia todas as inserções a um único servidor.

Prática recomendada de projeto do esquema nº 1: não escolha uma coluna com valor que aumenta ou diminui monotonicamente como a primeira parte da chave.

Trocar a ordem das chaves

Uma maneira de espalhar as gravações no espaço da chave é trocar a ordem das chaves para que a coluna que contém o valor crescente ou decrescente monotonicamente não seja a primeira parte da chave:

CREATE TABLE Users (
  UserId     INT64 NOT NULL,
  LastAccess TIMESTAMP NOT NULL,
  ...
) PRIMARY KEY (UserId, LastAccess);

Neste esquema modificado, as inserções agora são ordenadas por UserId, e não pelo carimbo de data/hora do último acesso. Este esquema espalha gravações entre diferentes divisões porque é improvável que um único usuário produza milhares de eventos por segundo.

O diagrama abaixo ilustra as cinco linhas da tabela Users ordenadas pelo UserId em vez de pelo carimbo de data/hora de acesso:

Tabela de usuários ordenada pelo UserId com capacidade de gravação balanceada

Aqui, os dados de Users são fragmentados em três divisões. Cada divisão contém a ordem de mil linhas de valores de UserId ordenados. Essa é uma estimativa razoável de como os dados do usuário podem ser divididos, presumindo que cada linha contenha cerca de 1 MB de dados do usuário e tenha um tamanho de divisão máximo na ordem dos GB. Os eventos do usuário ocorreram com diferença de milésimos de segundo, mas cada evento foi feito por um usuário diferente, então é muito menos provável que a ordem das inserções crie um ponto de acesso em comparação com a ordenação por carimbo de data/hora.

Consulte também as práticas recomendadas relacionadas à ordenação de chaves baseadas em carimbo de data/hora.

Gere o hash da chave única e espalhe as gravações em fragmentos lógicos

Outra técnica comum para espalhar a carga em vários servidores é criar uma coluna que contenha o hash da chave exclusiva real e usar a coluna hash (ou a coluna hash junto com as colunas de chave única) como a chave primária. Esse padrão ajuda a evitar pontos de acesso porque as novas linhas são distribuídas de maneira mais uniforme no espaço da chave.

Você pode usar o valor de hash para criar fragmentos lógicos ou partições em seu banco de dados. Em um banco de dados fisicamente fragmentado, as linhas estão espalhadas em vários bancos de dados. Em um banco de dados logicamente fragmentado, os fragmentos são definidos pelos dados na tabela. Por exemplo, para espalhar gravações na tabela Users em N fragmentos lógicos, seria possível preceder uma coluna de chave ShardId à tabela:

CREATE TABLE Users (
  ShardId     INT64 NOT NULL,
  LastAccess  TIMESTAMP NOT NULL,
  UserId      INT64 NOT NULL,
  ...
) PRIMARY KEY (ShardId, LastAccess, UserId);

Para calcular o ShardId, é gerado o hash de uma combinação das colunas de chave primária e é calculado o módulo N do hash - ShardId = hash(LastAccess and UserId) % N. Sua escolha da função hash e da combinação de colunas determina como as linhas serão espalhadas pelo espaço da chave. O Cloud Spanner criará divisões nas linhas para otimizar o desempenho. Observe que as divisões podem não estar alinhadas com os fragmentos lógicos.

O diagrama abaixo ilustra como o uso de um hash para criar três fragmentos lógicos pode espalhar a taxa de transferência de gravação de maneira mais uniforme entre os servidores:

Tabela de usuários ordenada pelo ShardId com capacidade de gravação balanceada

Aqui, a tabela Users é ordenada por ShardId, que é calculada como uma função hash de colunas de chave. As cinco linhas Users são divididas em três fragmentos lógicos, cada um deles coincidentemente em uma divisão diferente. As inserções são distribuídas uniformemente entre as divisões, o que equilibra a capacidade de gravação para os três servidores que manipulam as divisões.

Sua escolha da função hash determinará o quanto suas inserções serão distribuídas ao longo do intervalo de chaves. Não é preciso um hash criptográfico, embora um hash criptográfico possa ser uma boa escolha. Ao escolher uma função hash, é preciso considerar vários fatores:

  • Menor número de pontos de acesso: uma função que gera mais divisões tende a reduzir os pontos de acesso.
  • Eficiência de leitura: as leituras são mais rápidas quando há menos divisões para serem verificadas.
  • Contagem de nós

Usar um identificador universal exclusivo (UUID, na sigla em inglês)

É possível usar um identificador universal exclusivo (UUID, na sigla em inglês), conforme definido pela RFC 4122 como a chave primária. Recomendamos a versão 4 do UUID porque utiliza valores aleatórios na sequência de bits. A versão 1 do UUID armazena o carimbo de data/hora nos bits de ordem superior e não é recomendada.

Existem várias maneiras de armazenar o UUID como chave primária:

  • em uma coluna STRING(36)
  • em um par de colunas INT64
  • em uma coluna BYTES(16)

Existem algumas desvantagens em usar um UUID:

  • São grandes, usando 16 bytes ou mais. Outras opções para chaves primárias não utilizam tanto armazenamento.
  • Não carregam informações sobre o registro. Por exemplo, uma chave primária de SingerId e AlbumId tem um significado inerente, enquanto um UUID não.
  • A localidade entre os registros relacionados é perdido, e é por essa razão que o uso de um UUID elimina os pontos de acesso.

Fazer reversão em bits dos valores sequenciais

Quando você gera chaves primárias numéricas exclusivas, os bits de ordem superior dos números subsequentes devem ser distribuídos de forma aproximadamente uniforme em todo o espaço de números. Uma maneira de fazer isso é gerar números sequenciais por meios convencionais, e depois realizar a reversão deles em bits para chegar aos valores finais.

A reversão dos bits mantém valores exclusivos nas chaves primárias. É preciso armazenar apenas o valor invertido porque é possível recalcular o valor original no código do aplicativo.

Limitar o tamanho da linha

O tamanho de uma linha deve ser menor que 4 GB para garantir o melhor desempenho. O tamanho de uma linha inclui a linha de nível superior e todas as suas linhas filho e linhas de índice intercaladas. O Cloud Spanner normalmente cria uma nova divisão quando uma divisão existente atinge 4 GB. Além disso, o Cloud Spanner só pode dividir nas linhas de nível superior. Se uma linha for maior que 4 GB, a capacidade de gravação pode ser afetada.

Criar tabelas intercaladas para evitar pontos de acesso

O Cloud Spanner só pode criar divisões nas linhas de nível superior. Considere este exemplo com três tabelas intercaladas:

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
--     + Songs (interleaved table, child table of Albums)

O Cloud Spanner cria divisões que mantêm todos os álbuns e músicas de cada cantor juntos na mesma divisão. Se as músicas de um único cantor se tornarem um ponto de acesso para leituras ou gravações, o Cloud Spanner não poderá dividir a tabela Songs entre os servidores. Se, em vez disso, Songs for uma tabela de nível superior, o Cloud Spanner poderá criar divisões com base nas músicas:

-- Schema hierarchy:
-- + Singers (top-level table)
--   + Albums (interleaved table, child table of Singers)
-- + Songs (top-level table)

Usar a ordem decrescente para as chaves baseadas em carimbo de data/hora

Se você tiver uma tabela para seu histórico que esteja codificada por carimbo de data/hora, considere usar a ordem decrescente para a colunas-chave, se qualquer uma das situações a seguir se aplicar:

  • Se você estiver usando uma tabela intercalada para o histórico, também lerá a linha-pai. Nesse caso, com uma coluna de carimbo data/hora DESC, as entradas de histórico mais recentes são armazenadas adjacentes à linha-pai. Caso contrário, a leitura da linha-pai e seu histórico recente exigirão uma busca no meio para pular o histórico mais antigo.
  • Se você está lendo entradas sequenciais em ordem cronológica inversa, e não sabe exatamente a que distância está indo. Por exemplo, é possível usar uma consulta SQL com LIMIT para ver os N eventos mais recentes ou planejar cancelar a leitura depois de ler um determinado número de linhas. Nesses casos, você quer começar com as entradas mais recentes e ler as entradas mais antigas sequencialmente até que sua condição seja atendida, o que o Cloud Spanner faz com mais eficiência para as chaves de carimbo de data/hora que são armazenadas em ordem decrescente.

Adicione a palavra-chave DESC para tornar a chave de carimbo de data/hora decrescente. Por exemplo:

CREATE TABLE Users (
  UserId     INT64 NOT NULL,
  LastAccess TIMESTAMP NOT NULL,
  ...
) PRIMARY KEY (UserId, LastAccess DESC);

Prática recomendada de criação do esquema nº 2: use a ordem decrescente para as chaves baseadas em carimbo de data/hora.

Usar um índice intercalado em uma coluna em que o valor aumenta ou diminui monotonicamente

De maneira semelhante ao antipadrão da chave primária anterior, também não é uma boa ideia criar índices não intercalados em colunas em que os valores estão aumentando ou diminuindo constantemente, mesmo que não sejam colunas de chave primária.

Por exemplo, suponha que você defina a tabela a seguir, em que LastAccess é uma coluna de chave não primária:

CREATE TABLE Users (
  LastAccess TIMESTAMP,
  UserId     INT64 NOT NULL,
  ...
) PRIMARY KEY (UserId);

Pode parecer conveniente definir um índice na coluna LastAccess para consultar rapidamente o banco de dados em busca dos acessos de usuários "desde a hora X", como este:

-- ANTI-PATTERN: CREATING A NON-INTERLEAVED INDEX ON A COLUMN WHOSE VALUE
-- MONOTONICALLY INCREASES OR DECREASES

CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess)

No entanto, isso resulta na mesma armadilha descrita na prática recomendada anterior, porque os índices são implementados como tabelas em segundo plano, e a tabela de índice resultante usaria uma coluna cujo valor aumenta monotonicamente conforme a primeira parte da chave.

É bom criar um índice intercalado assim, uma vez que as linhas desses índices são intercaladas em linhas pai correspondentes, e é improvável que uma única linha pai produza milhares de eventos por segundo.

Prática recomendada de criação do esquema nº 3: se o valor de uma coluna aumenta ou diminui monotonicamente, não crie um índice não intercalado nela.

A seguir

Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…

Documentação do Cloud Spanner