UUID como PK – direto ao ponto

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