Fastway
Voltar ao blog

UUID v4, v7 ou ULID: qual ID o seu banco deveria usar

Rodrigo Krohling
·8 min de leitura

Escolher formato de identificador parece decisão de cinco minutos e vira investigação de performance dezoito meses depois. O motivo é que a propriedade que todo mundo avalia — unicidade — é aquela em que qualquer opção serve, e a propriedade que ninguém avalia — ordenação — é a que decide como seu banco se comporta em escala.

Unicidade é problema resolvido

Um UUID v4 tem 122 bits aleatórios. A chance de colisão é desprezível em qualquer sentido que importe: você precisaria gerar bilhões por segundo durante décadas para a probabilidade valer uma linha de código.

Então, se você está comparando formatos por risco de colisão, pare. Todos são seguros. Gere alguns e olhe para eles — as diferenças interessantes são visíveis nas próprias strings.

O problema é onde as linhas novas caem

A maioria dos bancos guarda as linhas numa árvore B ordenada pela chave primária. O InnoDB faz isso literalmente — a tabela é o índice. O PostgreSQL mantém o heap separado, mas ainda mantém uma árvore B para a chave primária.

Com uma chave sequencial, todo insert cai na borda direita da árvore. A página em que você escreve é a mesma em que acabou de escrever, então ela já está em memória, enche direitinho, e a árvore cresce numa direção só.

Com uma chave aleatória, todo insert cai numa posição imprevisível. Três coisas se seguem, e elas se acumulam:

Cache miss. A página de que você precisa está espalhada em algum lugar de um índice que pode ser bem maior que a RAM. Todo insert vira uma leitura de disco em potencial antes de poder ser uma escrita.

Divisão de página. Inserir no meio de uma página cheia a divide em duas. Divisões são caras e deixam as duas metades parcialmente vazias.

Fragmentação. Todas essas páginas meio vazias significam que o índice ocupa muito mais espaço do que os dados exigem, o que significa que menos dele cabe em cache, o que causa mais misses. O problema se alimenta.

Nada disso aparece em desenvolvimento. Aparece quando o índice ultrapassa a memória, que é um limiar que você cruza uma vez, sem aviso, em produção.

O UUID v7 resolve

O UUID v7 mantém o formato de 128 bits e a formatação com hífens de sempre, e muda o que vai nos primeiros 48 bits: um timestamp Unix em milissegundos. Os bits restantes são aleatórios.

A consequência é que valores v7 gerados depois ordenam depois dos gerados antes, tanto como string quanto como bytes. Os inserts voltam a cair na borda da árvore. Você ganha o comportamento de insert sequencial sem um gerador central de sequência, então qualquer número de serviços pode cunhar IDs de forma independente e a ordenação continua valendo.

Dois ganhos menores vêm de graça. Dá para ler a data de criação a partir do ID, o que é ocasionalmente útil ao depurar. E consultas por faixa de tempo podem usar a chave primária.

ULID: a mesma ideia, outra roupa

O ULID é anterior ao v7 e resolve o mesmo problema: 48 bits de timestamp seguidos de 80 bits de aleatoriedade. As diferenças são de apresentação, e importam mais do que se imagina:

  • 26 caracteres, base32 de Crockford, contra 36 com hífens. Mais curto em URL e em log.
  • Sem hífen, então clique duplo seleciona o identificador inteiro.
  • Insensível a maiúsculas, e o alfabeto exclui I, L, O e U para evitar erro de transcrição e palavrão acidental.
  • Ordenável lexicograficamente como texto, o que é genuinamente prático quando IDs caem num arquivo de log ou numa chave do Redis.

O custo é ecossistema. UUID tem tipo de coluna nativo, funções nativas e suporte de primeira classe em todo driver, ORM e ferramenta de administração. ULID normalmente viaja num char(26) e precisa de biblioteca. E se você guarda um ULID como texto onde um UUID seriam 16 bytes, você também acabou de dobrar o tamanho de todo índice que o toca.

E o v1?

O UUID v1 também é ordenado por tempo, então parece que deveria servir. Dois problemas.

Ele embute o endereço MAC da máquina, o que vaza detalhe de infraestrutura em toda URL. E os bytes do timestamp estão dispostos com o mais significativo por último, então os valores não ordenam cronologicamente sem embaralhar bytes. Alguns bancos oferecem uma função de reordenação exatamente por isso. Se você está escolhendo hoje, o v7 é o que o v1 tentava ser.

Uma decisão que se sustenta

Use UUID v7 para chave primária em sistema novo. É a resposta correta e entediante: localidade de índice, sem coordenação entre serviços, suporte nativo do banco e um formato que ninguém precisa aprender.

Use ULID quando o ID vai ser lido, digitado ou clicado por humanos, ou onde 10 caracteres a menos por identificador fazem diferença em milhões de linhas de log. Aceite que você vai carregar uma biblioteca.

Use UUID v4 quando o ID não pode revelar nada. Um v7 ou um ULID contam a quem os tiver quando o registro foi criado — normalmente inofensivo, ocasionalmente não. Tokens de redefinição de senha, links de convite e URLs de compartilhamento devem permanecer totalmente aleatórios, e sinceramente deveriam ser gerados como tokens opacos em vez de UUIDs.

Use um inteiro auto-incremento quando há um banco, um escritor e nenhum motivo para os IDs serem imprevisíveis. É menor e mais rápido que todas as opções acima. Identificadores distribuídos resolvem um problema de distribuição; se você não tem esse problema, não compre o formato de ID que o conserta.

Duas coisas para acertar, escolha o que escolher

Guarde como bytes, não como texto. Um UUID tem 16 bytes. Guardado como string de 36 caracteres ele fica com mais que o dobro, na chave primária, repetido em todo índice secundário e toda chave estrangeira. Numa tabela grande isso é a diferença entre um índice que cabe em memória e um que não cabe.

Nunca exponha um inteiro sequencial que você não pretendia expor. Se /pedidos/1042 é uma URL válida, /pedidos/1041 também é, e a única coisa entre um usuário e o pedido de outra pessoa é a sua checagem de autorização. IDs aleatórios ou ordenados por tempo não são mecanismo de controle de acesso, mas removem o convite.

O padrão em tudo isso: unicidade é fácil e todo mundo acerta. Localidade é invisível até ficar cara, e é por ela que vale escolher.