Chave primária: inteiro, UUID ou nenhuma
A discussão costuma ser entre inteiro e UUID, e costuma ser decidida pelo tamanho — 4 bytes contra 16. O tamanho é a parte menos interessante. O que custa a sério é outra coisa, e quase nunca aparece na conversa.
1. O custo que ninguém mede: onde a linha aterra no índice
Uma chave primária é um índice, e um índice é uma árvore ordenada. Com chaves sequenciais, cada inserção nova aterra sempre na mesma ponta da árvore — a página mais à direita, que por isso está permanentemente em memória. Mil inserções tocam numa página.
Com UUID aleatório, cada inserção aterra num sítio imprevisível. Mil inserções tocam em mil páginas, e o conjunto de páginas que a base precisa de ter em memória deixa de ser a última: passa a ser o índice inteiro.
Em dez mil linhas não se nota, porque o índice todo cabe em memória. Em dez milhões nota-se muito, e nota-se como escritas lentas sem causa visível — o tipo de problema que só se percebe a olhar para os índices, e não no código.
É isto que o UUIDv7 e o ULID resolvem: têm o tempo nos bits da frente, portanto são aleatórios o suficiente para não serem adivinhados e ordenados o suficiente para aterrarem todos na mesma ponta. Têm os 16 bytes do UUID sem o custo que torna o UUIDv4 caro.
2. O argumento a favor do UUID que se sustenta
Não é a segurança, e não é a escala. É este: o identificador pode ser gerado antes de falar com a base.
Isso resolve problemas reais que o inteiro não resolve. Uma aplicação que funciona sem rede e sincroniza depois. Um pedido que se pode repetir sem duplicar, porque traz o identificador consigo. Dados que vêm de três sítios e têm de se juntar sem colidir.
Se nenhum desses casos é o seu — se há um único escritor e a base é que atribui o número — está a pagar o custo do UUID sem comprar nada com ele.
3. A enumeração, com este site como exemplo
Os identificadores dos artigos aqui são públicos e sequenciais: /blog/artigo/675. Qualquer pessoa percebe que houve um 674, deduz quantos artigos existem e pode percorrer o intervalo todo.
Aqui isso é inofensivo, e vale a pena ser preciso quanto ao motivo: é um blogue público, onde tudo o que existe é para ser lido. E os artigos escondidos respondem 404 exactamente como os que nunca existiram, portanto percorrer o intervalo também não os distingue — foi um efeito lateral de esconder em vez de apagar.
Numa fatura, numa conta de utilizador ou numa encomenda, a mesma fuga é séria: revela volume, revela crescimento, e torna a enumeração trivial. Mas a correção não é necessariamente trocar a chave primária — é não pôr a chave primária no endereço. Uma coluna à parte, indexada e aleatória, resolve sem mexer na chave.
4. O caso de que quase ninguém fala: não haver chave artificial
A pergunta que se faz tarde é se a tabela precisa de um identificador próprio. Às vezes a chave já existe nos dados. Neste site, a tabela de votos não tem id nenhum:
@@id([postId, voterKey])A chave primária é o par artigo-votante. E a consequência é melhor do que parece: «um voto por pessoa por artigo» deixa de ser uma regra escrita em código que alguém pode esquecer de verificar, e passa a ser impossível de escrever. Não há caminho, nem erro, nem condição de corrida que produza dois votos da mesma pessoa no mesmo artigo.
O mesmo na tabela que liga artigos a etiquetas, com @@id([postId, tagId]): uma etiqueta repetida no mesmo artigo não é um erro que se apanhe — é um estado que a base não representa.
Esta é a melhor chave primária que há: a que transforma uma regra de negócio numa impossibilidade estrutural. E é de graça, porque o índice que ela cria é o que essas consultas já precisavam.
5. O que eu escolheria
Inteiro para tudo o que é interno e tem um único escritor. É mais pequeno, é legível num registo de erros, e dá a localidade de graça.
UUIDv7 ou ULID quando o identificador precisa de nascer fora da base. Nunca UUIDv4 como chave primária numa tabela que vai crescer, a não ser com o custo de localidade aceite de olhos abertos.
Chave composta natural sempre que os dados já a tiverem. É a única das opções que, além de identificar, garante uma regra.
6. E aqui, honestamente
Todas as tabelas deste site usam inteiro automático. Não foi uma decisão de desenho: os dados vieram de um dump de MySQL, os números já andavam em endereços desde 2009, e mudá-los partia todas as ligações que existem por aí. É herança, não escolha.
As duas chaves compostas, essas sim, foram deliberadas — e são as únicas duas tabelas onde uma regra de integridade não depende de o código se lembrar dela.
O resumo
O tamanho da chave é a parte que menos importa; a localidade no índice é a que custa. O UUID compra a capacidade de gerar o identificador fora da base — e se não for disso que precisa, não compra nada. E antes de escolher entre os três, vale a pena a pergunta que dispensa a escolha: os dados já têm uma chave?