Contexto
Estou projetando um banco de dados (no PostgreSQL 9.6) que armazena dados de um aplicativo distribuído. Devido à natureza distribuída do aplicativo, não posso usar números inteiros de incremento automático ( SERIAL
) como minha chave primária devido a possíveis condições de corrida.
A solução natural é usar um UUID ou um identificador globalmente exclusivo. O Postgres vem com um tipo embutidoUUID
, que é um ajuste perfeito.
O problema que tenho com o UUID está relacionado à depuração: é uma sequência não amigável ao ser humano. O identificador não ff53e96d-5fd7-4450-bc99-111b91875ec5
diz nada, enquanto ACC-f8kJd9xKCd
, embora não seja garantido que seja único, diz que estou lidando com um ACC
objeto.
De uma perspectiva de programação, é comum depurar consultas de aplicativos relacionadas a vários objetos diferentes. Suponha que o programador procure erradamente um ACC
objeto (conta) na ORD
tabela (pedido). Com um identificador legível por humanos, o programador identifica instantaneamente o problema, enquanto usava UUIDs, ele passava algum tempo descobrindo o que estava errado.
Não preciso da exclusividade "garantida" dos UUIDs; Eu não preciso de algum espaço para a geração de chaves, sem conflitos, mas UUID é um exagero. Além disso, no pior cenário, não seria o fim do mundo se ocorresse uma colisão (o banco de dados a rejeita e o aplicativo pode se recuperar). Portanto, considerando as vantagens e desvantagens, um identificador menor, porém amigável ao ser humano, seria a solução ideal para o meu caso de uso.
Identificando objetos de aplicativo
O identificador que criei tem o seguinte formato:, {domain}-{string}
onde {domain}
é substituído pelo domínio do objeto (conta, pedido, produto) e {string}
é uma sequência gerada aleatoriamente. Em alguns casos, pode até fazer sentido inserir a {sub-domain}
antes da sequência aleatória. Vamos ignorar o comprimento {domain}
e {string}
com o objetivo de garantir a exclusividade.
O formato pode ter um tamanho fixo se ajudar no desempenho da indexação / consulta.
O problema
Sabendo que:
- Eu quero ter chaves primárias com um formato como
ACC-f8kJd9xKCd
. - Essas chaves primárias farão parte de várias tabelas.
- Todas essas chaves serão usadas em várias junções / relacionamentos, em um banco de dados 6NF.
- A maioria das tabelas terá um tamanho médio a grande (média de ~ 1 milhão de linhas; as maiores com ~ 100 milhões de linhas).
Em relação ao desempenho, qual é a melhor maneira de armazenar essa chave?
Abaixo estão quatro soluções possíveis, mas como tenho pouca experiência com bancos de dados, não tenho certeza qual (se houver) é a melhor.
Soluções consideradas
1. Armazene como string ( VARCHAR
)
(O Postgres não faz diferença entre CHAR(n)
e VARCHAR(n)
, por isso estou ignorando CHAR
).
Após algumas pesquisas, descobri que a comparação de strings com VARCHAR
, especialmente em operações de junção, é mais lenta do que usando INTEGER
. Isso faz sentido, mas é algo com que eu deveria me preocupar nessa escala?
2. Armazenar como binário ( bytea
)
Diferentemente do Postgres, o MySQL não possui um UUID
tipo nativo . Existem várias postagens explicando como armazenar um UUID usando um BINARY
campo de 16 bytes , em vez de um campo de 36 bytes VARCHAR
. Essas postagens me deram a idéia de armazenar a chave como binária ( bytea
no Postgres).
Isso economiza tamanho, mas estou mais preocupado com o desempenho. Tive pouca sorte em encontrar uma explicação sobre qual comparação é mais rápida: binária ou de string. Eu acredito que as comparações binárias são mais rápidas. Se estiverem, bytea
provavelmente é melhor do que VARCHAR
, mesmo que o programador agora precise codificar / decodificar os dados todas as vezes.
Posso estar errado, mas acho que ambos bytea
e VARCHAR
compararei (igualdade) byte por byte (ou caractere por caractere). Existe uma maneira de "pular" essa comparação passo a passo e simplesmente comparar "a coisa toda"? (Acho que não, mas não custa checar).
Acho que armazenar bytea
é a melhor solução, mas me pergunto se existem outras alternativas que estou ignorando. Além disso, a mesma preocupação que expressei na solução 1 é verdadeira: a sobrecarga nas comparações é suficiente para me preocupar?
"Soluções criativas
Eu vim com duas soluções muito "criativas" que podem funcionar, mas não tenho certeza até que ponto (ou seja, se eu tiver problemas para dimensioná-las para mais de algumas milhares de linhas em uma tabela).
3. Armazene como UUID
com um "rótulo" anexado
O principal motivo para não usar UUIDs é para que os programadores possam depurar melhor o aplicativo. Mas e se pudermos usar os dois: o banco de dados armazena todas as chaves UUID
apenas como s, mas envolve o objeto antes / depois das consultas.
Por exemplo, o programador pede ACC-{UUID}
, o banco de dados ignora a ACC-
parte, busca os resultados e retorna todos eles como {domain}-{UUID}
.
Talvez isso seja possível com alguma invasão com procedimentos ou funções armazenadas, mas algumas perguntas vêm à mente:
- Isso (remover / adicionar o domínio em cada consulta) é uma sobrecarga substancial?
- Isso é possível?
Eu nunca usei procedimentos ou funções armazenados antes, então não tenho certeza se isso é possível. Alguém pode lançar alguma luz? Se eu puder adicionar uma camada transparente entre o programador e os dados armazenados, parece uma solução perfeita.
4. (O meu favorito) Armazene como IPv6 cidr
Sim, você leu certo. Acontece que o formato do endereço IPv6 resolve meu problema perfeitamente .
- Posso adicionar domínios e subdomínios nos primeiros octetos e usar os restantes como uma sequência aleatória.
- As probabilidades de colisão estão OK. (Eu não usaria 2 ^ 128, mas ainda está OK.)
- Esperamos que as comparações de igualdade sejam otimizadas, para que eu possa obter melhor desempenho do que simplesmente usar
bytea
. - Eu posso realmente fazer algumas comparações interessantes, como
contains
, dependendo de como os domínios e sua hierarquia são representados.
Por exemplo, suponha que eu use o código 0000
para representar o domínio "produtos". A chave 0000:0db8:85a3:0000:0000:8a2e:0370:7334
representaria o produto 0db8:85a3:0000:0000:8a2e:0370:7334
.
A principal questão aqui é: em comparação com bytea
, existe alguma vantagem ou desvantagem no uso do cidr
tipo de dados?
varchar
entre muitos outros problemas. Eu não sabia sobre os domínios da pg, o que é ótimo para aprender. Vejo domínios sendo usados para validar se uma determinada consulta está usando o objeto correto, mas ele ainda depende de ter um índice não inteiro. Não tenho certeza se existe uma maneira "segura" de usar serial
aqui (sem uma etapa de bloqueio).
varchar
. Considere transformá-lo em um FK
integer
tipo e adicione uma tabela de pesquisa. Dessa forma, você pode ter legibilidade humana e protegerá seu composto PK
contra anomalias de inserção / atualização (colocando um domínio inexistente).
text
é preferível ao invés varchar
. Veja em depesz.com/2010/03/02/charx-vs-varcharx-vs-varchar-vs-text e postgresql.org/docs/current/static/datatype-character.html
ACC-f8kJd9xKCd
. ← ← Parece ser um trabalho para a boa e velha chave primária composta .