Modelo de dados e esquema

Resumo do modelo de dados

Um banco de dados do Cloud Spanner pode conter uma ou mais tabelas. Elas parecem tabelas de bancos de dados relacionais, já que são estruturadas com linhas, colunas e valores, e contêm chaves primárias. Os dados no Cloud Spanner são fortemente tipados: você precisa definir um esquema para cada banco de dados, e esse esquema precisa especificar os tipos de dados de cada coluna em cada tabela. Entre os tipos de dados permitidos, estão os escalares e de matrizes. Eles são explicados com mais detalhes em Tipos de dados. Também é possível definir um ou mais índices secundários em uma tabela.

Relacionamentos de tabelas pai e filho

Há duas maneiras de definir relações pai-filho no Cloud Spanner: intercalação de tabelas e chaves externas.

A intercalação de tabelas do Cloud Spanner é uma boa opção para muitas relações pai-filho em que a chave primária da tabela filha inclui as colunas de chave primária da tabela pai. A localização mútua das linhas filho com as linhas pai pode melhorar significativamente o desempenho. Por exemplo, se você tiver uma tabela Customers e uma Invoices, e seu aplicativo frequentemente busca todas as faturas de um determinado cliente, defina Invoices como uma tabela filha de Customers. Ao fazer isso, você declara um relacionamento de localidade de dados entre duas tabelas logicamente independentes: o Cloud Spanner é instruído a armazenar fisicamente uma ou mais linhas de Invoices com uma linha Customers.

Para uma discussão mais aprofundada sobre intercalação, consulte Como criar tabelas intercaladas abaixo.

As chaves externas são uma solução pai-filho mais geral e abordam casos de uso adicionais. Elas não estão limitadas a colunas de chave primária, e as tabelas podem ter várias relações de chave externa, como pai em alguns relacionamentos e filho em outros. No entanto, uma relação de chave externa não sugere a co-localização das tabelas na camada de armazenamento.

Para mais informações sobre chaves externas e a comparação delas com tabelas intercaladas, consulte Visão geral de chaves externas.

Chaves primárias

Como informar ao Cloud Spanner quais linhas Invoices precisam ser armazenadas com quais linhas Customers? Faça isso usando a chave primária dessas tabelas. Toda tabela precisa ter uma chave primária, e essa chave pode ser composta por zero ou mais colunas da tabela. Se você declarar uma tabela para ser filha de outra, as colunas da chave primária da mãe precisarão ser o prefixo da chave primária da secundária. Isso significa que, se a chave primária de uma tabela mãe for composta por N colunas, a chave primária de cada uma das tabelas filhas também precisará ser composta das mesmas N colunas, na mesma ordem e começando pela mesma coluna.

O Cloud Spanner armazena linhas classificadas em ordem por valores de chave primária. As linhas filhas são inseridas entre linhas mães que compartilham o mesmo prefixo de chave primária. Essa inserção de filhas entre as mães ao longo da dimensão da chave primária é chamada de intercalação, e as tabelas filha também são chamadas de tabelas intercaladas. Consulte abaixo uma ilustração de linhas intercaladas, em Como criar tabelas intercaladas.

Resumidamente, o Cloud Spanner pode colocalizar fisicamente linhas de tabelas relacionadas. Os exemplos de esquema abaixo mostram o aspecto desse layout físico.

Como escolher uma chave primária

A chave primária identifica exclusivamente cada uma das linhas em uma tabela. Para atualizar ou excluir linhas existentes em uma tabela, é preciso que a tabela tenha uma chave primária composta de uma ou mais colunas. Uma tabela sem colunas de chave primária pode ter apenas uma linha. Muitas vezes, o aplicativo já tem um campo que é uma escolha natural para uso como chave primária. Por exemplo, no exemplo de tabela Customers acima, pode haver um CustomerId fornecido pelo aplicativo que serve bem como a chave primária. Em outros casos, pode ser necessário gerar uma chave primária ao inserir a linha, como um valor INT64 único que você gera.

Em todos os casos, tenha cuidado para não criar pontos de acesso com a escolha da chave primária. Por exemplo, se você inserir registros com um número inteiro monotônico crescente como a chave, a inserção sempre ocorrerá no final do espaço da chave. Isso não é desejável, porque o Cloud Spanner divide os dados entre servidores por intervalos de chaves, o que significa que as inserções serão direcionadas para um único servidor, criando um ponto de acesso. Há técnicas que podem espalhar a carga em vários servidores e evitar pontos de acesso:

Divisões de banco de dados

É possível definir hierarquias de relações de mãe e filha entre tabelas com até sete camadas. Isso significa que você pode colocalizar linhas de sete tabelas logicamente independentes. Se o tamanho dos dados nas tabelas for pequeno, o banco de dados provavelmente poderá ser tratado por um único servidor do Cloud Spanner. Mas o que acontece quando as tabelas relacionadas crescem e começam a atingir os limites de recursos de um servidor individual? O Cloud Spanner é um banco de dados distribuído. Isso significa que, à medida que o banco de dados cresce, o Cloud Spanner divide os dados em blocos denominados "divisões". As divisões individuais podem se mover de maneira independente umas da outras e serem atribuídas a servidores diferentes, que podem estar em locais físicos distintos. Uma divisão é definida como um intervalo de linhas em uma tabela de nível superior (em outras palavras, não intercalada), em que as linhas são ordenadas pela chave primária. As chaves de início e término desse intervalo são chamadas de "limites de divisão". O Cloud Spanner adiciona e remove automaticamente os limites de divisão, o que altera o número de divisões no banco de dados.

O Cloud Spanner divide os dados com base na carga: ele adiciona limites de divisão automaticamente quando detecta alta carga de leitura ou gravação espalhada entre muitas chaves em uma divisão. Você tem algum controle sobre como seus dados são divididos porque o Cloud Spanner só pode definir limites de divisão entre as linhas de tabelas que estão na raiz de uma hierarquia (ou seja, tabelas que não são intercaladas em uma tabela mãe). Além disso, as linhas de uma tabela intercalada não podem ser divididas da linha correspondente na tabela mãe porque as linhas da tabela intercalada são armazenadas na ordem da chave primária classificada juntamente com a linha da tabela mãe que compartilha o mesmo prefixo da chave primária. Consulte uma ilustração sobre linhas intercaladas em Como criar uma hierarquia de tabelas intercaladas. Assim, os relacionamentos de tabela mãe e filha que você define, junto aos os valores de chave primária definidos para linhas de tabelas relacionadas, permitem controlar a maneira como os dados são divididos em segundo plano.

Divisão baseada em carga

Como um exemplo de como o Cloud Spanner executa a divisão baseada em carga para reduzir os pontos de acesso de leitura, imagine que seu banco de dados contém uma tabela com 10 linhas que são lidas com mais frequência do que todas as outras linhas na tabela. Enquanto essa tabela está na raiz da hierarquia do banco de dados (em outras palavras, não é uma tabela intercalada), o Cloud Spanner pode adicionar limites de divisão entre cada uma dessas 10 linhas para que cada uma delas seja manipulada por um servidor diferente, em vez de permitir que todas as leituras dessas linhas consumam os recursos de um único servidor.

Como regra geral, se você seguir as práticas recomendadas para o projeto do esquema, o Cloud Spanner poderá reduzir os pontos de acesso em leituras que segmentam linhas de uma tabela não intercalada. Dessa maneira, a capacidade de leitura melhora a cada poucos minutos até você saturar os recursos na instância ou se deparar com casos em que nenhum novo limite de divisão pode ser adicionado porque há uma divisão que cobre apenas uma única linha e as tabelas filhas intercaladas dela.

Exemplos de esquema

Os exemplos de esquema abaixo mostram como criar tabelas do Cloud Spanner com e sem relações mãe e filha e ilustrar os layouts físicos correspondentes de dados.

Como criar uma tabela

Imagine que você esteja criando um aplicativo de música e precise de uma tabela simples que armazene linhas de dados do cantor:

Tabela

Visualização lógica de linhas em uma tabela simples de cantores. A coluna da chave primária aparece à esquerda da linha em negrito.

Observe que a tabela contém uma coluna de chave primária, SingerId, que aparece à esquerda da linha em negrito, e que as tabelas são organizadas por linhas, colunas e valores.

É possível definir a tabela com um esquema do Cloud Spanner como este:

CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

Observe o seguinte sobre o esquema do exemplo:

  • Singers é uma tabela na raiz da hierarquia do banco de dados (porque não é definida como filha de outra tabela).
  • As colunas de chave primária geralmente são anotadas com NOT NULL, mesmo que seja possível omitir essa anotação se você quiser permitir valores NULL em colunas-chave (veja mais em Colunas-chave).
  • As colunas que não estão incluídas na chave primária são chamadas de colunas não chave e podem ter uma anotação NOT NULL opcional.
  • As colunas que usam o tipo STRING ou BYTES precisam ser definidas com um comprimento, o que representa o número máximo de caracteres Unicode que podem ser armazenados no campo. Veja mais detalhes em Tipos de dados escalares.

O que parece o layout físico das linhas na tabela Singers? O diagrama abaixo mostra as linhas da tabela Singers armazenadas por chave primária contígua (a ordem de classificação). Ou seja, "Singers(1)", "Singers(2)", e assim por diante, em que "Singers(1)" representa a linha na tabela Singers codificada por 1.

Exemplo de linhas de uma tabela armazenada em ordem de chave contígua.

Layout físico de linhas na tabela "Singers", com um limite de divisão de exemplo que resulta em divisões processadas por servidores diferentes.

O diagrama acima também ilustra possíveis limites de divisão, que podem ocorrer entre qualquer linha de Singers, já que Singers está na raiz da hierarquia do banco de dados. Ele também ilustra um exemplo de limite de divisão entre as linhas codificadas por Singers(3) e Singers(4), com os dados das divisões resultantes atribuídos a servidores diferentes. Isso significa que, conforme essa tabela cresce, é possível que linhas dos dados de Singers sejam armazenadas em locais diferentes.

Como criar várias tabelas

Imagine que agora você queira adicionar alguns dados básicos sobre os álbuns de cada cantor ao aplicativo de música:

Tabela

Visualização lógica de linhas em uma tabela "Albums". As colunas da chave primária aparecem à esquerda da linha em negrito

A chave primária de Albums é composta por duas colunas: SingerId e AlbumId, para associar cada álbum à sua cantora. O exemplo de esquema a seguir define as tabelas Albums e Singers na raiz da hierarquia do banco de dados, o que torna essas tabelas irmãs:

-- Schema hierarchy:
-- + Singers (sibling table of Albums)
-- + Albums (sibling table of Singers)
CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId);

O layout físico das linhas de Singers e Albums é semelhante ao diagrama, com linhas da tabela Albums armazenadas por chave primária contígua, em seguida, linhas de Singers armazenadas por chave primária contígua:

Layout físico das linhas: as linhas de

Layout físico de linhas das tabelas "Singers" e "Albums", ambas na raiz da hierarquia do banco de dados.

Uma observação importante sobre o esquema acima: o Cloud Spanner não pressupõe relações de localidade de dados entre as tabelas Singers e Albums, porque são tabelas de nível superior. À medida que o banco de dados cresce, o Cloud Spanner pode adicionar limites de divisão entre qualquer uma das linhas mostradas acima. Isso significa que as linhas da tabela Albums podem ter uma divisão diferente das linhas da tabela Singers, e as duas divisões poderiam se mover independentemente umas das outras.

Dependendo das necessidades do seu aplicativo, convém permitir que os dados Albums sejam localizados em divisões diferentes dos dados Singers. No entanto, se seu aplicativo frequentemente precisa recuperar informações sobre todos os álbuns de um determinado cantor, crie Albums como uma tabela filha de Singers, que posiciona as linhas das duas tabelas ao longo da dimensão da chave primária. O próximo exemplo explica isso com mais detalhes.

Como criar tabelas intercaladas

Imagine que, ao projetar seu aplicativo de música, você perceba que ele precisa acessar com frequência as linhas das tabelas Singers e Albums para uma determinada chave primária (por exemplo, sempre que você acessar a linha Singers(1), também precisa acessar as linhas Albums(1, 1) e Albums(1, 2)). Em outras palavras, Singers e Albums precisam ter uma forte relação de localidade de dados.

É possível declarar essa relação de localidade de dados criando Albums como uma tabela filha, ou "intercalada", de Singers. Conforme mencionado em Chaves primárias, uma tabela intercalada é uma tabela que é declarada filha de outra porque é necessário que as linhas da tabela filha sejam armazenadas fisicamente junto com a linha mãe associada. Conforme mencionado acima, o prefixo da chave primária de uma tabela filha precisa ser a chave primária da tabela mãe.

A linha em negrito no esquema abaixo mostra como criar Albums como uma tabela intercalada de Singers.

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

Observações sobre este esquema:

  • SingerId, que é o prefixo da chave primária da tabela filha Albums, também é a chave principal da tabela mãe Singers. Isso não será necessário se Singers e Albums estiverem no mesmo nível da hierarquia, mas é exigido neste esquema porque Albums é declarado como uma tabela filha de Singers.
  • A anotação ON DELETE CASCADE significa que quando uma linha da tabela mãe é excluída, as respectivas linhas filhas nesta tabela também são excluídas automaticamente. Ou seja, todas as linhas que começam com a mesma chave primária. Se uma tabela filha não tiver essa anotação ou se a anotação for ON DELETE NO ACTION, exclua as linhas filha antes de excluir a linha mãe.
  • As linhas intercaladas são ordenadas primeiro por linhas da tabela mãe e, em seguida, por linhas contíguas da tabela filha que compartilham a chave primária da tabela mãe, ou seja, "Singers(1)", depois "Albums(1, 1)", então "Albums(1, 2)" e assim por diante.
  • O relacionamento de localidade de dados de cada cantor e os dados de álbum seria preservado se esse banco de dados fosse dividido, já que as divisões só podem ser inseridas entre as linhas da tabela Singers.
  • A linha pai já precisa existir para que seja possível inserir linhas filho. A linha pai pode já existir no banco de dados ou pode ser inserida antes da inserção das linhas filho na mesma transação.

Layout físico das linhas: as linhas de

Layout físico de linhas da tabela "Singers" e da tabela filha "Albums".

Como criar uma hierarquia de tabelas intercaladas

A relação mãe e filha entre Singers e Albums pode ser estendido para mais tabelas descendentes. Por exemplo, é possível criar uma tabela intercalada chamada Songs como filha de Albums para armazenar a lista de faixas de cada álbum:

Tabela

Visualização lógica de linhas em uma tabela Songs. As colunas da chave primária aparecem à esquerda da linha em negrito

Songs precisa ter uma chave primária composta por todas as chaves primárias das tabelas que estão acima dela na hierarquia, ou seja, SingerId e AlbumId.

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
--     + Songs (interleaved table, child table of Albums)
CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE TABLE Songs (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  TrackId      INT64 NOT NULL,
  SongName     STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId, TrackId),
  INTERLEAVE IN PARENT Albums ON DELETE CASCADE;

A visualização física de linhas intercaladas mostra que a relação de localidade de dados entre um cantor e seus respectivos álbuns e dados de músicas é preservada:

Visualizações físicas de linhas: as músicas são intercaladas em

Layout físico das linhas das tabelas "Singers", "Albums" e "Songs", que formam uma hierarquia de tabelas intercaladas.

Em resumo, uma tabela mãe e todas as tabelas filhas e descendentes formam uma hierarquia de tabelas no esquema. Ainda que cada tabela na hierarquia seja logicamente independente, a intercalação física delas pode melhorar o desempenho, efetivamente juntando previamente as tabelas e permitindo que você acesse linhas relacionadas e minimize os acessos do disco.

Se possível, vincule dados em tabelas intercaladas por chave principal. Todas as linhas intercaladas têm a garantia de serem armazenadas fisicamente na mesma divisão da sua linha mãe. Por isso, o Cloud Spanner pode realizar junções por chave primária localmente, minimizando o acesso ao disco e o tráfego de rede. No exemplo a seguir, Singers e Albums são unidos na chave primária, SingerId:

SELECT s.FirstName, a.AlbumTitle
FROM Singers AS s JOIN Albums AS a ON s.SingerId = a.SingerId;

As tabelas intercaladas no Cloud Spanner não são obrigatórias, mas são recomendadas para tabelas com fortes relacionamentos de localidade de dados. Evite as tabelas intercaladas se houver uma possibilidade de que o tamanho de uma única linha e dos descendentes dela se torne maior do que alguns poucos GB.

Colunas de chave

As chaves de uma tabela não podem mudar. Não é possível adicionar ou remover uma coluna de chave a uma tabela existente.

Como armazenar NULLs

Colunas de chave primária podem ser definidas para armazenar NULLs. Se quiser armazenar NULLs em uma coluna de chave primária, omita a cláusula NOT NULL dessa coluna no esquema.

Veja um exemplo de como omitir a cláusula NOT NULL na coluna da chave principal SingerId. Como SingerId é a chave primária, pode haver no máximo uma linha na tabela Singers que armazena NULL nessa coluna.

CREATE TABLE Singers (
  SingerId   INT64,
  FirstName  STRING(1024),
  LastName   STRING(1024),
) PRIMARY KEY (SingerId);

A propriedade anulável da coluna da chave primária precisa coincidir entre as instruções da tabela mãe e as da tabela filha. Neste exemplo, Albums.SingerId INT64 NOT NULL não é permitido. A declaração de chave precisa omitir a cláusula NOT NULL porque Singers.SingerId a omite.

CREATE TABLE Singers (
  SingerId   INT64,
  FirstName  STRING(1024),
  LastName   STRING(1024),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,  -- NOT ALLOWED!
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

Tipos não permitidos

Os itens a seguir não podem ser do tipo ARRAY:

  • colunas de chave de uma tabela
  • colunas de chave de um índice

Como desenvolver um design para multilocação

Caso estejam sendo armazenados dados pertencentes a diferentes clientes, talvez seja interessante fornecer multilocação. Por exemplo, um serviço de música pode querer armazenar a gravadora de cada álbum separadamente.

Multilocação clássica

A maneira clássica de desenvolver um design para multilocação é criar um banco de dados separado para cada cliente. Neste exemplo, cada banco de dados tem sua própria tabela Singers:

Banco de dados 1: Ackworth Records
SingerId FirstName LastName
1MarcRichards
2CatalinaSmith
Banco de dados 2: Cama Records
SingerId FirstName LastName
3MarcRichards
4GabrielWright
Banco de dados 3: Eagan Records
SingerId FirstName LastName
1BenjaminMartinez
2HannahHarris

A maneira recomendada de desenvolver um design para multilocação no Cloud Spanner é usar um valor de chave primária diferente para cada cliente. Inclua uma coluna de chave CustomerId ou de uma chave semelhante nas suas tabelas. Se você tornar CustomerId a primeira coluna de chave, os dados de cada cliente têm uma boa localização. O Cloud Spanner divide automaticamente os dados por todos os nós com base no tamanho deles e nos padrões de carga. Neste exemplo, há uma única tabela Singers para todos os clientes:

Cloud Spanner multitenancy database
CustomerId SingerId FirstName LastName
11MarcRichards
12CatalinaSmith
23MarcRichards
24GabrielWright
31BenjaminMartinez
32HannahHarris

Se forem necessários bancos de dados separados para cada locatário, é preciso estar ciente das seguintes restrições:

  • limites para a quantidade de bancos de dados por instância e de tabelas por banco de dados. Dependendo do número de clientes, pode não ser possível ter bancos de dados ou tabelas separados.
  • Adicionar novas tabelas e índices não intercalados pode levar muito tempo. Pode não ser possível alcançar o desempenho que você quer se o design do esquema depender da adição de novas tabelas e índices.

Se for preciso criar bancos de dados separados, pode ser mais interessante distribuir suas tabelas entre eles de maneira que cada banco tenha um número baixo de alterações de esquema por semana.

Se você criar tabelas e índices separados para cada cliente do seu aplicativo, não coloque todas as tabelas e índices no mesmo banco de dados. Em vez disso, divida-os entre vários bancos de dados para reduzir os problemas de desempenho com a criação de um grande número de índices. Há também limites para o número de tabelas e índices por banco de dados.