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-111b91875ec5diz nada, enquanto ACC-f8kJd9xKCd, embora não seja garantido que seja único, diz que estou lidando com um ACCobjeto.
De uma perspectiva de programação, é comum depurar consultas de aplicativos relacionadas a vários objetos diferentes. Suponha que o programador procure erradamente um ACCobjeto (conta) na ORDtabela (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 UUIDtipo nativo . Existem várias postagens explicando como armazenar um UUID usando um BINARYcampo 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 ( byteano 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, byteaprovavelmente é melhor do que VARCHAR, mesmo que o programador agora precise codificar / decodificar os dados todas as vezes.
Posso estar errado, mas acho que ambos byteae VARCHARcompararei (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 UUIDcom 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 UUIDapenas 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 0000para representar o domínio "produtos". A chave 0000:0db8:85a3:0000:0000:8a2e:0370:7334representaria 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 cidrtipo de dados?
varcharentre 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 serialaqui (sem uma etapa de bloqueio).
varchar. Considere transformá-lo em um FK integertipo e adicione uma tabela de pesquisa. Dessa forma, você pode ter legibilidade humana e protegerá seu composto PKcontra 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 .