Skip to content

Instantly share code, notes, and snippets.

@rponte
Last active October 30, 2024 19:58
Show Gist options
  • Save rponte/bf362945a1af948aa04b587f8ff332f8 to your computer and use it in GitHub Desktop.
Save rponte/bf362945a1af948aa04b587f8ff332f8 to your computer and use it in GitHub Desktop.
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!

@rponte
Copy link
Author

rponte commented Dec 21, 2022

Mais 2 artigos que discutem sobre "ULID vs UUID":

  1. IDS : INTEGER VS UUID VS ULID
  2. ULIDs and Primary Keys

@rponte
Copy link
Author

rponte commented Dec 22, 2022

This article explains why UUIDs can cause write amplification in Postgres, and it also shows that favoring a UUID format with sequential or time-based over UUIDv4 (random) helps the database in write&read workloads:

https://www.2ndquadrant.com/en/blog/sequential-uuid-generators/

[...] sequential UUIDs generators significantly reduce the write amplification and make the I/O patterns way more sequential and it may also improve the read access pattern.

@rponte
Copy link
Author

rponte commented Dec 22, 2022

This article shows that UUIDv4 is not a problem to YugabyteDB due to its storage: https://dev.to/yugabyte/install-extensions-from-pgdg-repo-to-yugabytedb-example-with-sequentialuuids-1dio

You must think about the consequence in a Distributed SQL database before using a time-based UUID. Thanks to the LSM-Tree storage, YugabyteDB doesn't have the problems that sequential_uuids tries to solve (WAL write amplification, B-Tree fragmentation and clustering factor). If you want a UUID, then the one from pgcrypto (already installed in YugabyteDB) gen_random_uuid() is probably the right one.

@rponte
Copy link
Author

rponte commented Jan 5, 2023

O Artigo The Wild World of Unique Identifiers (UUID, ULID, etc) lista diversas alternativas aa UUID (algumas delas já comentadas por aqui):

@rponte
Copy link
Author

rponte commented Jan 6, 2023

O Zalando RESTful API and Event Guidelines recomenda o uso de UUID como ID em endpoints somente se necessário: SHOULD only use UUIDs if necessary [144]

@arsaccol
Copy link

Que artigo massa. Como meio que iniciante, nunca tinha pensado na ideia de usar inteiros normais como IDs internas e UUIDs como IDs externas, opacas, expostas ao mundo exterior, mas realmente faz total sentido.

@rponte
Copy link
Author

rponte commented Nov 7, 2023

Artigo da empresa Buidkite sobre a experiência deles migrando do modelo hibrído (PK como int/bigint + coluna external_id como UUIDv4) para uso de UUIDv7 como PK:

Um ponto interessante, é que eles tentaram criar sua própria UUIDv7 (time-ordered UUID) mas compatível&versionado como um UUIDv4 devido a retro-compatibilidade com seus clientes.

@rponte
Copy link
Author

rponte commented Dec 26, 2023

TSIDs strike the perfect balance between integers and UUIDs for most databases

[...] This comparison is performed in the context of a typical B2B or B2C SaaS application backed by a single SQL database (spanning one or a few nodes), which describes the vast majority of applications built today

Feature Auto-incr. Integers UUIDs TSIDs
Key Type Variable size integer 128-bit integer 64-bit integer
Uniqueness Unique within a database Universally unique Unique across nodes
Predictability Predictable sequence Unpredictable Unpredictable
Space Efficiency High(small size) Low(large size) Moderate(larger than integers but smaller than UUIDs)
Data locality High(sequential increment) Low(random order) High(time-sorted with random component)
Performance High(efficient indexing, inserts, reads) Poor(inefficient inserts, scattered indexes, read penalty) High(similar to integers)
Readability High(simple numbers) Low(32 character strings) Moderate(13 character strings)
Chronological Sorting Yes, implicit(based on sequence) No inherent order Yes, time-sorted(based on time component)
Multi-node Generation Not feasible Easily feasible Feasible with node IDs
Security (Inference Risk) High(German Tank Problem) Low(no inference) Low(no inference)
Ease of Implementation High(natively supported) Moderate(varies by database) Low(least support, requires function implementation, managing node IDs)
Scalability Varies(limited by integer type) High(no practical limit) High(at least ~70 years, limited by timestamp size)
Migration Flexibility Moderate(can change to larger integer type) Low(hard to change key type) High(drop-in compatible with integers)

@rafaelpontezup
Copy link

@rponte
Copy link
Author

rponte commented Feb 5, 2024

I have decided to orchestrate a benchmark war between four different methods of storing a primary key:

  1. use a text field to store UUIDs
  2. use PostgreSQL’s native uuid data type
  3. use the new uuidv7 code currently in CommitFest which we’re hoping will be in PostgreSQL 17 (i think we might still be waiting on. something related to the approval process for the official standard)
  4. use the classic, efficient, fast, sql-standard bigint generated as identity data type.

The challenge is simple: insert one million rows into a large table, while concurrently querying it, AS FAST AS YOU CAN!!!

@rponte
Copy link
Author

rponte commented Mar 21, 2024

@rponte
Copy link
Author

rponte commented Apr 22, 2024

PostgreSQL and UUID as primary key - by @maciejwalkowiak

This article does not focus on "if UUID is the right format for a key", but how to use UUID as a primary key with PostgreSQL efficiently.

@fabiolimace
Copy link

Ultimate Guide to Identifiers

This is one of the best articles on IDs in general. It is well written and well researched. It also provides some useful hints. The author also coined the term multiple factor identifier.

@kelvincesar
Copy link

kelvincesar commented Apr 26, 2024

https://supabase.com/blog/choosing-a-postgres-primary-key
image
image

Achei interessante esse xid:

Comparison

Name Binary Size String Size Features
UUID 16 bytes 36 chars configuration free, not sortable
shortuuid 16 bytes 22 chars configuration free, not sortable
Snowflake 8 bytes up to 20 chars needs machine/DC configuration, needs central server, sortable
MongoID 12 bytes 24 chars configuration free, sortable
xid 12 bytes 20 chars configuration free, sortable

@rponte
Copy link
Author

rponte commented May 6, 2024

Are UUIDv4s Sabotaging Your App? Indexes Under Fire

While UUIDs deliver uniqueness, their randomness comes at a storage size cost. This can put significant pressure on cache efficiency, indexing, sequential I/O, JOINs, and sorting for databases trying to service read queries.

  • Indexing overhead — The randomness of UUIDs as keys destroys index locality and clustering. This leads to increased index sizes, more levels in b-trees, and reduced cache efficiency. Index scans for queries have to traverse larger indexes and more blocks.
  • Memory/cache pressure — Larger indexes mean fewer can be cached in available memory and buffer caches. More expensive I/O results to fetch index pages for scans. Random access patterns also reduce sequentially pre-fetched blocks.
  • Table scans — For large table scans, the clustering inefficiencies of UUID indexes increase the blocks and I/O needed to scan entire tables. A lack of locality hurts sequential block access.
  • Join performance — When joining on UUID columns, the randomness causes a loss of locality that particularly slows nested loop join performance. Compute resources are wasted comparing random values.

@rponte
Copy link
Author

rponte commented May 8, 2024

@fabiolimace
Copy link

RFC 9562 is published.

This specification defines UUIDs (Universally Unique IDentifiers), also known as GUIDs (Globally Unique IDentifiers), and a Uniform Resource Name namespace for UUIDs.

RFC 4122 is now obsolete.

@rponte
Copy link
Author

rponte commented May 9, 2024

Thanks for sharing all those links and articles here, @fabiolimace ❤️ You're amazing!

@rponte
Copy link
Author

rponte commented Sep 18, 2024

Video curto (4min) no canal do "Waldemar Neto - Dev Lab" sobre uso de UUIDs como PKs : Cuidado com UUID em bancos relacionais!

@mariomeyrelles
Copy link

Vi agora essa análise e vou compartilhar quando for necessário (ou seja, sempre!)

@rafaelpontezup
Copy link

rafaelpontezup commented Sep 20, 2024

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