Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Não use UUID como PK nas tabelas do seu banco de dados

Pretende usar UUID como PK em vez de Int/BigInt no seu banco de dados? Pense novamente...

TL;TD

Não use UUID como PK nas tabelas do seu banco de dados.

Um pouco mais de detalhes

Usar UUID como tipo numa PK (Primary Key) em vez de Int/BigInt em bancos de dados relacionais (RDBMS) parece ter se tornado comum em aplicações nesse mundo de APIs REST, microsserviços e sistemas distribuídos, afinal temos algumas boas vantagens nessa abordagem:

  • segurança: IDs opacos para expor em APIs REST;
  • geração de IDs descentralizados: assim browsers, clients e serviços, apps mobile e outros bancos podem gerar IDs únicos;
  • ideal em DBs distribuídos ou com múltiplos nodes de escrita;
  • são ótimos para tabelas temporárias ou ao fazer merge entre bancos;
  • super útil em migração entre DBs (evita colisão);
  • seu uso em batch processing pode melhorar substancialmente o throughput;
  • comuns em cenários de replicação de dados;

Vantagens existem e são várias, especialmente em cenários distribuídos, mas há um custo: impacto direto na escrita e na leitura do seu banco de dados.

Apesar desse custo depender de N fatores como banco de dados e versão utilizada, setup e tuning, workload, hardware etc, não dá para ignorar que ao usar UUID como PK nós estamos tentando resolver um problema de 4 bytes (32 bits) com 16 bytes (128 bits)! É 4x mais problema para inserir, ler e armazenar!

Conhecer e entender as principais devantagens (trade-offs) é importante antes de bater o martelo. A verdade, é que praticamente todos os RDBMS modernos apresentam algum tipo de problema ou limitação no uso de UUID como PK, apesar da maioria destes problemas poderem ser contornados ou minimizados através de analise, setup ou tuning apropriado. E é justamente nesse momento de fazer essa analise e tuning que o papel de um DBA no time brilha.

Para não me alongar mais, segue alguns desses trade-offs:

O problema não é o uso do tipo UUID em si, mas sim utilizá-lo como chave primária em tabelas do banco de dados. O que estou querendo dizer, é que, desenhar uma feature seguindo deliberadamente essa abordagem costuma ser responsável e fazer muito sentido, mas adotá-la cegamente para TODAS as tabelas do seu schema é muito perigoso!

Muitas vezes, esse tipo de design é utilizado como um "shortcut" (atalho) para facilitar a vida dos devs na hora de expor suas entidades em APIs REST, mas que joga todo o onus da manutenção para o time de infra, DBAs e muitas vezes para própria empresa, como comprar mais disco ou substituir hardware. Além disso, um ID opaco só resolve parte do problema de segurança, pois ainda se faz necessário validações de acesso e propriedade dos dados, que geralmente é a parte mais chata de se implementar.

Não me entenda errado, não é que esse tipo de solução não funcione, ela vai funcionar, mas como meu amigo Raul Oliveira me disse uma vez:

Eh como fazer caminhada plantando bananeira. Da pra fazer, vai concluir, gastar mais energia. Mas eh uma boa ideia?

Perceba que é muito fácil enumerar as vantagens das tecnologias e no uso de técnicas, pois elas estão escancaradas em todos os lugares. Mas na minha opinião, um bom arquiteto(a) ou dev(a) senior não escolhe tecnologias apenas por suas vantagens, mas principalmente por suas desvantagens. Ele(a) precisa saber o que está perdendo ao tomar uma decisão!

Contudo, é dificil entender e pesar o custo e impacto das desvantagens sem um contexto, por isso...

Contextos importam

Em 2012 o Instagram precisou distriuir seu banco de dados (fazer sharding) para melhorar performance e throughput do site, e, em vez de adotar UUID eles resolveram criar um próprio ID de 64bits. Eles não fizeram isso à toa, eles estavam cientes do custo imposto pelo uso de UUID na epoca e dentro do contexto deles.

Um pouco antes, em 2010, o Twitter também precisou gerar IDs únicos entre suas instâncias de MySQL e o banco de dados Cassandra, e para isso optou por um serviço distribuído de geração de IDs, que por sinal foi criado por eles e recebeu o nome de Snowflake. Assim como o Instagram, a equipe do Twitter seguiu por esse caminho pois era importante que os IDs gerados fossem ordenáveis e tivessem o tamanho de 64 bits.

Nesse mundo de microsserviços e sistemas distribuídos, geralmente cada serviço possui um banco isolado e independente que possui um schema pequeno, enxuto e com baixa volumetria de dados, o que acaba por minimizar as chances de problemas ao adotar UUID como PK! Afinal, o estilo arquitetural escolhido já distribui por natureza a massa de dados entre as dezenas ou milhares de serviços. Mas não se engane, se há chances do volume de dados crescer em um intervalo curto de tempo então talvez seja melhor refletir e discutir com seu DBA sobre sua adoção.

Em muitos cenários, nem todas as tabelas precisam ser expostas para sistemas externos, portanto ao adotar UUID como PK atente-se a dar preferência somente às tabelas que precisam mostrar a cara pro mundo a fora, dessa forma minimiza-se o impacto no restante do sistema.

Favoreça um modelo hibrido

Nem oito nem oitenta, já dizia minha mãe.

Na minha opinião, se possível, favoreça o uso de Int/BigInt para IDs internos do banco de dados (PKs e FKs), e use uma coluna do tipo UUID como ID externo (por exemplo external_id). Essa forma hibrida possibilita que seu sistema continue tirando o melhor proveito do seu RDBMS ao mesmo tempo que possibilita ter um ID opaco (segurança).

Essa abordagem não só minimiza o impacto no uso de UUID como também oferece vantagens interessantes:

  • permite ter um ID opaco para expor em APIs REST;
  • não precisamos necessariamente de um index na coluna;
  • podemos usar um index do tipo HASH em vez de BTREE (funciona melhor para queries de comparação por igualdade);
  • não “espalhamos” o UUID pelas FKs de outras tabelas;
  • ocupamos menos espaço em disco e memoria, afinal os indices param de referenciar UUIDs;
  • com menos dados conseguimos operar nosso workload em memoria (e isso por si só já é uma melhoria brutal);
  • podemos fazer tuning apropriado na coluna de acordo com nosso workload;
  • permitimos que o banco trabalhe melhor via PK/FK sequencial em JOINs, agregações e ordenações;
  • excelente para schemas existentes ou legados;

Provavelmente existem outras vantagens nessa abordagem, mas meu pouco conhecimento sobre RDBMS não me permite pensar mais longe nesse momento. De qualquer forma, não esqueça de consultar seu DBA, fazer alguns testes de carga e entender os limites da sua aplicação!

Concluindo

Provavelmente eu falei alguma groselha, então não se acanhe em me corrigir ou dar um toque!

Embora eu tenha sugerido o modelo hibrido, você não precisa adotá-lo ou mesmo considerar que usar UUID como PK seja errado, pois não é! Se está funcionando para você então está tudo bem, continue utilizando, afinal no seu contexto fez (e ainda faz) sentido seguir essa abordagem. O importante aqui é que os trade-offs estejam claros em cima da mesa, caso contrário em algum momento eles podem voltar para assombrar você e sua equipe!

Acredito que existem outras vantagens e desvantagens na adoção de UUID como chave primária, afinal esse tipo de problema não é de hoje, então, caso você lembre de mais algum pró ou mesmo contra, ou um outro contexto interessante não deixe de comentar e compartilhar sua experiência. Com certeza eu posso aprender muito mais e melhor com a sua experiência e de outros.

Enfim, resolvi escrever esse gist por causa dessa thread no twitter e para ajudar o "Rafael do futuro"a não esquecer detalhes sobre este tópico!

@fabriziomello

This comment has been minimized.

Copy link

@fabriziomello fabriziomello commented Dec 28, 2020

Ótimo post... aquela solução do Instagram é sensacional... gerar 1024 ids por shard a cada milisegundo... e o Write Amplification no PostgreSQL vai ocorrer independente do tipo de dado que for usado em um índice #ficaadica

Apenas a título de conhecimento;

  1. O Tomas Vondra um dos core commiter do PostgreSQL há algum tempo implementou uma pequena extensão para gerar UUIDs sequencias, vale a pena conferir: https://github.com/tvondra/sequential-uuids

  2. Há algum tempo o Fábio Telles escreveu dois artigos sobre chaves artificiais no PostgreSQL inclusive comparando desempenho:
    https://www.savepoint.blog.br/2018/02/03/chaves-artificiais-no-postgresql/
    http://www.savepoint.blog.br/2018/02/17/chaves-artificiais-no-postgresql-desempenho/

@rponte

This comment has been minimized.

Copy link
Owner Author

@rponte rponte commented Dec 29, 2020

opa @fabriziomello, obrigado pelo feeback e pelos links, são muitos bons!

Não entendo tanto de RDBMS e PostgreSQL como você, mas o que entendi do artigo sobre Write Amplification é que ele se intensifica muito mais por causa do uso de UUID em vez de Int/BigInt, o que acaba degradando a performance e deteriorando o throughput do banco.

Sobre a extensão para gerar UUID no PostgreSQL que você comentou, ela é bem bacana, mas se não estou enganado alguns outros RDBMS já fornecem algo do tipo, como o MS SQL Server. Apesar dessa extensão ser bem útil, um dos motivos para adotar UUID em sistemas distribuídos é que eles podem ser gerados de forma não centralizada (por várias outras apps em vez do próprio banco), então nesse tipo de cenário a geração de UUID do banco resolve parcialmente o problema.

um abraço e obrigado mais uma vez!

@Roriz

This comment has been minimized.

Copy link

@Roriz Roriz commented Dec 29, 2020

Muito bom o artigo,

Apenas para título de conhecimento tbm:

@fabiolimace

This comment has been minimized.

Copy link

@fabiolimace fabiolimace commented Dec 30, 2020

Texto muito bom!

Acabei de fazer alguns testes baseados no artigo de Fabio Telles. Eu queria comparar os UUIDs aleatórios com os UUIDs baseados em data/hora. Todos os testes inserem 10 milhões de valores em cada tabela (10x mais que no artigo).

Os resultados foram estes:

BTREE
seq_btree       26611.672 ms         26 s 611 ms
uuid1_btree     68953.038 ms    01 m 08 s 953 ms (versão data/hora ou v1)
uuid4_btree    220832.651 ms    03 m 40 s 832 ms (versão aleatória ou v4)

HASH
seq_hash         9747.586 ms         09 s 747 ms
uuid1_hash      52524.319 ms         52 s 524 ms
uuid4_hash      40606.623 ms         40 s 606 ms

Depois refiz os testes, mas dessa vez gerei previamente todos os UUIDs em tabelas auxiliares para evitar a geração de UUIDs durante a inserção. Os novos resultados foram estes:

BTREE
seq_btree       15787.937 ms         15 s 787 ms
uuid1_btree     24258.682 ms         24 s 258 ms (versão data/hora ou v1)
uuid4_btree    202155.698 ms    03 m 22 s 155 ms (versão aleatória ou v4)

HASH
seq_hash         6331.119 ms         06 s 331 ms
uuid1_hash       6038.697 ms         06 s 038 ms
uuid4_hash       6386.878 ms         06 s 386 ms

Apenas a título de conhecimento também, existem algumas bibliotecas que implementam um UUID ordenável por data/hora não padronizado:

Para aproveitar os supostos benefícios dos "Ordered UUIDs", está sendo elaborada uma proposta de alteração da RFC-4122 para incluir um tipo de UUID ordenável por data/hora. Caso alguém se interesse e queira acompanhar (stargazers), o link do draft é este: https://github.com/uuid6/uuid6-ietf-draft

@ErickWendel

This comment has been minimized.

Copy link

@ErickWendel ErickWendel commented Dec 30, 2020

Sensacional!!

@6a6f6a6f

This comment has been minimized.

Copy link

@6a6f6a6f 6a6f6a6f commented Dec 30, 2020

Texto muito massa! Alguns pontos sobre segurança que são importantes de notar, é que UUID != sinônimo de segurança em qualquer sistema, acho que essa vibe do UUID como standard para REST (ou outros coisos para APIs) veio por conta da popularidade do EntityFramework e outros frameworks famosos utilizarem o padrão de Guid.NewGuid() como PK - para trazer algum tipo de sentimento de segurança.

Algumas vezes vejo a utilização de UUIDs como forma de inibir IDOR em sistemas web, mas delegar segurança para um valor não-deterministico a uma entidade que fica exposta em algum canto sem autorização é um erro gravíssimo.

@daniel-tavares-osf

This comment has been minimized.

Copy link

@daniel-tavares-osf daniel-tavares-osf commented Dec 30, 2020

Caramba. Que texto muuito massa. Mind Opener.

Tu faria uma versão em inglês dele pra gente poder compartilhar pra geral around the world?

@Leo-Neves

This comment has been minimized.

Copy link

@Leo-Neves Leo-Neves commented Dec 30, 2020

Caramba. Que texto muuito massa. Mind Opener.

Tu faria uma versão em inglês dele pra gente poder compartilhar pra geral around the world?

Super apoio uma versão in English

@nihey

This comment has been minimized.

Copy link

@nihey nihey commented Dec 30, 2020

Bem legal o texto, como um usuário frequente de UUID no PostgreSQL me levantou alguns questionamentos.

Só teria alguns pontos que acabo discordando do texto.

TL; DR;
Não use UUID como PK nas tabelas do seu banco de dados.

É uma conclusão precipitada, existem vários casos em que UUIDs fazem bastante sentido, principalmente se estiver gerando dados de uma forma distribuída, ou sincronizando vários Databases, como apontou nos detalhes. Acho que é melhor falar que talvez não seja bom usar ele se não for realmente usufruir dos benefícios que ele traz.

Por experiência pessoal, para a grande maioria das aplicações o uso ou não do UUID indifere na performance, simplesmente porque o workload dela não vai ser significativo para impactar na performance, se preocupar com isso em uma aplicação menor seria fazer BDUF. É bem mais provável que o código que esteja sendo rodado em JavaScript, Python ou outra linguagem de alto nível traga um overhead maior na aplicação do que o tipo de dado usado para armazenar as PKs. Provavelmente vai sentir a diferença de verdade quando estiver trabalhando com alguns milhares ou dezenas de milhares de inserts por segundo.

Ainda assim, acho que é válido o questionamento de que nem toda tabela precisa ser indexada por UUID, especialmente se não precisar de geração de dados de forma distribuída ou pensar em sincronizar tabelas no futuro.

@yanjustino

This comment has been minimized.

Copy link

@yanjustino yanjustino commented Jan 2, 2021

Antes de tudo, parabéns pelo artigo, Rafael!

Seu artigo me lembrou uma situação na qual presenciei em uma Secretaria de Fazenda Estadual, num contexto de um novo projeto de storage de NFe. Nas Notas eletrônicas as chaves de Acesso são números de 360 bites e na época uma solução que estava sendo projetada era utilizar o UUID como PK. Argumentava-se a diminuição de bits da chave, o que seria uma vantagem.

O cenário da solução envolvia um RDBMS Oracle e um volume diário de 2 milhões de notas. Felizmente revertemos o conceito para adotar a própria chave de Acesso que possui uma decomposição lógica, diferente do UUID, que facilita um ordem lógica "natural". Além disso, decompomos a chave para ter uma variabilidade de consultas e realizar reduções da massa consultada!

@rponte

This comment has been minimized.

Copy link
Owner Author

@rponte rponte commented Jan 2, 2021

Muito bacana @yanjustino, obrigado pelo relato e feedback!

O uso de uma natural-key em vez de uma key sequencial ou UUID pode ajudar bastante em cenários específicos, como esse que você relatou. Parabéns!

Antes de tudo, parabéns pelo artigo, Rafael!

Seu artigo me lembrou uma situação na qual presenciei em uma Secretaria de Fazenda Estadual, num contexto de um novo projeto de storage de NFe. Nas Notas eletrônicas as chaves de Acesso são números de 360 bites e na época uma solução que estava sendo projetada era utilizar o UUID como PK. Argumentava-se a diminuição de bits da chave, o que seria uma vantagem.

O cenário da solução envolvia um RDBMS Oracle e um volume diário de 2 milhões de notas. Felizmente revertemos o conceito para adotar a própria chave de Acesso que possui uma decomposição lógica, diferente do UUID, que facilita um ordem lógica "natural". Além disso, decompomos a chave para ter uma variabilidade de consultas e realizar reduções da massa consultada!

@diegosilva13

This comment has been minimized.

Copy link

@diegosilva13 diegosilva13 commented Jan 5, 2021

Muito massa o artigo!!

@fabriziomello

This comment has been minimized.

Copy link

@fabriziomello fabriziomello commented Jan 7, 2021

opa @fabriziomello, obrigado pelo feeback e pelos links, são muitos bons!

Show!!!

Não entendo tanto de RDBMS e PostgreSQL como você, mas o que entendi do artigo sobre Write Amplification é que ele se intensifica muito mais por causa do uso de UUID em vez de Int/BigInt, o que acaba degradando a performance e deteriorando o throughput do banco.

O problema de Write Amplification que o PostgreSQL possui é porque quando uma linha é alterada TODOS os índices precisam ser alterados. Isso porque não implementaos índices secundários, ou seja, índices que apontam para o índice da Primary Key (Ex: InnoDB no MySQL). Veja o case do Uber (controverso em alguns pontos) quando decidiu sair do PostgreSQL para MySQL:

https://eng.uber.com/postgres-to-mysql-migration
https://use-the-index-luke.com/blog/2016-07-29/on-ubers-choice-of-databases

Essa é a forma como o PostgreSQL implementou o MVCC e por isso necessitamos de tarefas de manutenção contínua como o AutoVacuum.

Entretanto após isso a comunidade reagiu positivamente e evoluiu a arquitetura do PostgreSQL, então apartir da versão 12 lançamos o Table Access Method que é análogo ao MySQL Storage Engines, o que está permitindo criar novos modos de armazenamento plugáveis como o ZHeap (novo formato de armazenamento que minimiza muito Write Amplification e elimina necessidade do Autovacuum além de adicionar UNDO logs) e também o Zedstore (armazenamento colunar).

Sobre a extensão para gerar UUID no PostgreSQL que você comentou, ela é bem bacana, mas se não estou enganado alguns outros RDBMS já fornecem algo do tipo, como o MS SQL Server. Apesar dessa extensão ser bem útil, um dos motivos para adotar UUID em sistemas distribuídos é que eles podem ser gerados de forma não centralizada (por várias outras apps em vez do próprio banco), então nesse tipo de cenário a geração de UUID do banco resolve parcialmente o problema.

Realmente resolve parcialmente esse problema... entretanto o uso de UUID tem seus prós e contras assim como toda decisão arquitetural.

@rafaelpontezup

This comment has been minimized.

@fabriziomello

This comment has been minimized.

Copy link

@fabriziomello fabriziomello commented Jan 14, 2021

Re-Introducing Hash Indexes in PostgreSQL

Bem lembrado... antigamente os índices hash não eram crash-safe então não era recomendado para uso em produção, porém apartir da 10 isso mudou... ;-)

@rponte

This comment has been minimized.

Copy link
Owner Author

@rponte rponte commented Feb 3, 2021

Link público no meu blog pessoal para facilitar alcance e indexação desse conteúdo :-)

@rafaelpontezup

This comment has been minimized.

Copy link

@rafaelpontezup rafaelpontezup commented Mar 2, 2021

Aparentemente as novas versões do MongoDB começaram a migrar do UUID v3 para UUID v4, o que tem gerado algumas dores de cabeça para muitos devs no mundo Java. Se você sua Mongo então vale a pena ficar atento as boas práticas com relação a geração de UUID no seu sistema ou driver utilizado:

Complex UUID scenarios

You might run into problems when working with UUIDs in multi-platform/multi-language environments because accessing your database from different programming languages using different MongoDB drivers might not always be safe, as we will demonstrate below.

Furthermore, you should be careful when working with UUID data types in third-party MongoDB management software because only a few MongoDB tools take due care in handling UUID data types.

MongoDB drivers usually convert UUIDs between their Binary database representation and the language specific UUID data type. Initially the encoding method was platform specific and wasn’t consistently implemented across different MongoDB drivers.
[...]

Essa dica foi dada pelo Charles Luxinger em um post no seu Linkedin.

@rafaelpontezup

This comment has been minimized.

Copy link

@rafaelpontezup rafaelpontezup commented Apr 28, 2021

Usar IDs sequenciais (e não opacos) não necessariamente é um problema de segurança para todos os tipos de sistemas ou features. Por exemplo, o StackOverflow usa um ID sequencial para seus usuários, que por sinal é aberto:

@rafaelpontezup

This comment has been minimized.

Copy link

@rafaelpontezup rafaelpontezup commented Apr 29, 2021

Ainda hoje o Twitter utiliza IDs de 64bits gerados por eles pelos mesmos motivos de 2010. Está tudo na documentação do desenvolvedor.

@fabriziomello

This comment has been minimized.

Copy link

@fabriziomello fabriziomello commented Apr 29, 2021

E parece que a própria Sony criou a sua versão do Snowflake do Twitter, o SonyFlake

@fabiolimace

This comment has been minimized.

Copy link

@fabiolimace fabiolimace commented Apr 29, 2021

Este documento contém uma comparação de vários identificadores ordenáveis por data e hora de criação: sortable-id-comparisons.md. O identificadores Snowflake e SonyFlake estão na lista.

O documento faz parte de uma pesquisa feita para propor novas versões de identificadores únicos baseados em tempo.

@rponte

This comment has been minimized.

Copy link
Owner Author

@rponte rponte commented Apr 30, 2021

Este documento contém uma comparação de vários identificadores ordenáveis por data e hora de criação: sortable-id-comparisons.md. O identificadores Snowflake e SonyFlake estão na lista.

O documento faz parte de uma pesquisa feita para propor novas versões de identificadores únicos baseados em tempo.

que massa, Fabio! obrigado por compartilhar!

@fabriziomello

This comment has been minimized.

Copy link

@fabriziomello fabriziomello commented May 1, 2021

@rponte Comecei a brincar aqui um pouco na criação de uma extensão PostgreSQL (WIP) para gerar esses Time-based Unique Identifiers dos links que o @fabiolimace compartilhou conosco.

https://github.com/fabriziomello/unique_id

... obviamente estou aceitando ajuda ;-)

@rponte

This comment has been minimized.

Copy link
Owner Author

@rponte rponte commented May 10, 2021

Artigo bacana sobre Clustered Index escrito pelo Vlad Mihalcea. No artigo tem 2 pontos importantes sobre clustered indexes que tem a ver com uso de UUID:

  1. Clustered Index column size;
  2. Clustered Index column monotonicity
@rponte

This comment has been minimized.

Copy link
Owner Author

@rponte rponte commented Jun 10, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment