No contexto padrão de uso de um RDBMS (Relational Database Management System) um ID é tradicionalmente uma coluna do tipo Int/BigInt auto-incremental, contudo, o tal do UUID (Universally Unique IDentifier) vem sendo adotado como PK (chave primária) com uma frequência cada vez maior. Mas será que este é um uso consciente ou indiscriminado? E outra, qual o impacto disso no contexto de performance? Essas são algumas das perguntas que pretendo responder ao longo deste post.
O que é um UUID?
É um identificador único universal de 128 bits definido pela RFC 4122. É um número normalmente representado por 32 dígitos hexadecimais e 4 hífens, totalizando 36 dígitos. Essa representação é visualmente distribuída em cinco grupos de tamanhos predefinidos, sendo eles 8-4-4-4-12. Veja este exemplo:
2C553B92-9D28-4AFA-849B-18617ADBF8BA
Cada caracter hexadecimal possui representação de 4 bits, deste modo podemos calcular o peso de cada grupo representado acima da seguinte forma: 32 + 16 + 16 + (8+8) + 48 = 128 bits
. A representação hexadecimal apresentada possui cinco grupos, porém, internamente o algoritmo de construção do UUID considera seis campos, que no caso do calculo acima foi representado pela soma de dois campos de 8 bits.
Para fecharmos essa fase inicial, é interessante citar que o primeiro digito do terceiro grupo representa a versão do algoritmo UUID utilizado. Logo a representação para este caso seria:
xxxxxxxx-xxxx-Vxxx-xxxx-xxxxxxxxxxxx
Ou seja:
2C553B92-9D28-4AFA-849B-18617ADBF8BA
É um UUID que utiliza o algoritmo versão 4, também conhecido como UUIDv4.
No momento em que escrevo este artigo o UUID possui 5 versões, sendo a 4 ainda é a mais utilizada.
Qual o objetivo do UUID?
O UUID surgiu a partir da necessidade de criar identificadores únicos de registros no contexto de aplicações distribuídas e/ou baseada em microsserviços. Neste modelo de arquitetura altamente concorrente os dados são persistidos em diferentes tecnologias e bancos, como cada qual pode trabalhar a sua maneira, surge então a necessidade de utilizar um identificador único para identificar os registros de forma exclusiva, para isso precisamos de um mecanismo/algoritmo que permita a geração decentralizada destes identificadores, ou seja, cada serviço deve ser capaz de por si só gerar um ID único global antes de transacionar o registro para outro serviço ou persisti-lo em algum local.
Como o identificador possui uma quantidade finita de possibilidades, claramente existe uma possibilidade de haver colisão de hash, ou seja, serem gerados dois identificadores com a mesma sequência, porém, em termos gerais isso é quase impossível de acontecer uma vez que seria necessário gerar cerca de 1 bilhão de UUIDs por segundo por cerca de 85 anos em se tratando do UUIDv4.
UUID como mecanismo de proteção
Em alguns contextos o uso de IDs de incremento automático como PK podem representar um risco de segurança para sua aplicação e/ou negócio? Pois bem, para exemplificar vamos imaginar que você tem dois endpoints públicos na sua API, por exemplo:
GET /posts/3122
GET /users/193
Os dois endpoints acima utilizam IDs incrementais, ou seja, podemos utiliza-los para extrair todos os posts e users desta base, para isso bastaria criarmos um script que chame estes endpoints começando pelo ID 1 e incrementado +1 caso o HTTP status code de retorno seja igual a 200, ou seja, sem esforço algum qualquer pessoa poderia sequestrar toda sua base de posts e users. Em um mundo em que a informação é considerada um ativo de valor, tal falha pode levar seu negócio a ruína total!
Já ao utilizar uma lógica como UUID ou qualquer outra que gere identificadores não sequências você pode evitar este tipo de sequestro. Por exemplo:
GET /users/7921E154-8302-4578-8B63-A5D102571F28
Neste caso não há como incrementar o ID de modo a retornar o próximo registro de users. Isso é a solução para todos os problemas? Claro que não, caso sua API seja aberta e não implemente outros controles como limit rate, o “intruso” pode tentar extrair os dados na força bruta, tudo vai depender da motivação que recai sobre ele.
Complemente sua leitura com esse maravilhoso artigo de Phil Sturgeon: “Auto-Incrementing IDs: Giving your Data Away“.
Vantagens e desvantagens
Você não pensou que era só utilizar UUID e a vida de Dev seria um mar de rosas, ou pensou? Pois é, assim como tudo na vida essa abordagem trás vantagens e desvantagens, sendo assim vamos a algumas delas.
Vantagens
- Geração de IDs de forma decentralizada;
- Geração de IDs únicos não sequências (conforme citado acima isso tem relação direta com aspectos de segurança);
- Evita a colisão de identificadores ao mesclar bases;
- Possibilita conhecer o ID antes mesmo do registro ser persistido no banco;
- Pode otimizar o throughput ao ser utilizado em processos batch;
Desvantagens
- Ao ser utilizado como PK acaba consumindo maior espaço em disco;
- UUID não são ordenáveis (as versões mais recentes de inúmeros bancos de dados já implementa soluções que visam minimizar o impacto do uso de UUIDs como PK);
- Requer ao menos 2 ciclos de CPU para processar o dado uma vez que os processadores atuais trabalham basicamente com palavras de 64 bits;
- Até pouco tempo os UUIDs eram armazenados em campos varchar(36), atualmente os RDBMS oferecem tipos de dados otimizados para trabalhar com UUIDs, porém, ainda sim eles consomem mais espaço e possuem performance reduzida, afinal de contas esse é um tipo de string não classificável, ou seja, o motor terá um trabalho maior para agrupar os dados;
Já vi casos assustadores em que UUIDs foram utilizados para praticamente todas as entidades de uma aplicação, neste contexto o uso indiscriminado e deliberado irá exigir muito da infra que suporta o banco de dados, deste modo essa aplicação naturalmente estará fazendo uso excessivo e desnecessário de recursos, comprometendo não somente o orçamento mais possivelmente a experiência do usuário também.
De forma objetiva e não necessariamente técnica, podemos falar que: em um RDBMS uma PK do tipo Int/BigInt é classificável, agrupável e ordenável, ou seja, o motor do banco realiza todas essas operações e persiste essa PK em disco e carrega uma cópia no buffer (normalmente na memória principal), tornando assim o processo de busca direta e via relacionamento altamente performático, porém, ao usar UUIDs essa performance sofre uma degradação em razão do tamanho da string e ausência do fator de ordenação sequencial.
Caso tenha interesse em maiores detalhes e comparações de performance, sugiro a leitura deste artigo: https://www.percona.com/blog/2019/11/22/uuids-are-popular-but-bad-for-performance-lets-discuss/
Segue alguns links complementares a respeito do assunto:
https://gist.github.com/rponte/bf362945a1af948aa04b587f8ff332f8
https://medium.com/trainingcenter/o-que-é-uuid-porque-usá-lo-ad7a66644a2b