UUID vs ULID vs Nano ID vs IDs autoincrementais
Cinco esquemas comuns de ID comparados no que importa na prática: tamanho, ordenação, chance de colisão, comportamento no banco de dados e o que um ID entrega.
A escolha do formato de ID se resume a três perguntas. Os IDs precisam ser ordenados pelo momento de criação? Vão aparecer em URLs ou em APIs públicas? E o banco de dados vai guardá-los em uma coluna nativa de 128 bits ou como texto? O UUID v7, padronizado na RFC 9562 em maio de 2024, mudou a resposta para muitos projetos: ele oferece a ordenação por tempo que tornou o ULID popular, em um formato que toda coluna UUID e toda biblioteca já aceitam.
Visão geral
| Autoincremento | UUID v4 | UUID v7 | ULID | Nano ID | |
|---|---|---|---|---|---|
| Exemplo | 1042 | f47ac10b-58cc-4372-a567-0e02b2c3d479 | 01926f3a-7c2b-7d4e-9a1f-3b5c8d2e6f10 | 01J9QKMZ1BFN79M7SVBJ6JWVRG | V1StGXR8_Z5jdHi6B-myT |
| Tamanho em binário | 4 ou 8 bytes | 16 bytes | 16 bytes | 16 bytes | Guardado como texto |
| Tamanho em texto | Até 10 ou 19 dígitos | 36 | 36 | 26 | 21 por padrão |
| Bits aleatórios | Nenhum | 122 | Até 74 | 80 | 126 por padrão |
| Ordena pelo momento de criação | Sim | Não | Sim, até o milissegundo | Sim, até o milissegundo | Não |
| Gerado sem o banco de dados | Não | Sim | Sim | Sim | Sim |
| Especificação | Recurso do banco de dados | RFC 9562 | RFC 9562 | Especificação da comunidade | Biblioteca |
| Revela | Quantidade e ordem | Nada | Momento de criação | Momento de criação | Nada |
Como cada um é construído
- Autoincremento é um contador mantido pelo banco de dados. Um
INTde 32 bits com sinal termina em 2.147.483.647, e umBIGINTcom sinal, em 9.223.372.036.854.775.807. A aplicação só conhece o ID depois do insert, a menos que reserve valores de uma sequence antes. - UUID v4 são 122 bits aleatórios mais 6 bits fixos que marcam a versão e a variante.
- UUID v7 começa com um timestamp Unix de 48 bits em milissegundos, seguido da versão, de 12 bits chamados
rand_a, da variante e de 62 bits chamadosrand_b. A RFC 9562 permite que um gerador use parte desses bits como contador, para que IDs criados no mesmo milissegundo continuem saindo em ordem. O gerador deste site faz isso com os 12 bits derand_a. - ULID é um timestamp de 48 bits em milissegundos mais 80 bits aleatórios, escrito como 26 caracteres em Base32 de Crockford: 10 para o tempo e 16 para a parte aleatória. O alfabeto deixa de fora I, L, O e U e não diferencia maiúsculas de minúsculas. No modo monotônico, um ULID criado no mesmo milissegundo que o anterior pega a parte aleatória desse ULID e soma um a ela.
- Nano ID é uma string de caracteres aleatórios, por padrão 21, tirados do alfabeto de 64 caracteres
A-Z a-z 0-9 _ -. Cada caractere carrega 6 bits, o que dá 126 bits. Tanto o tamanho quanto o alfabeto podem ser alterados, e o número de bits aleatórios muda junto.
Um ULID e um UUID são, os dois, números de 128 bits escritos de formas diferentes. 01J9QKMZ1BFN79M7SVBJ6JWVRG e 01926f3a-7c2b-7d4e-9a1f-3b5c8d2e6f10 são o mesmo valor, em Base32 e em hexadecimal. Por isso um ULID pode ser guardado em uma coluna UUID nativa, como o uuid do PostgreSQL, e mantém a sua ordenação ali. Um ULID que não nasceu como UUID v7, porém, tem bits arbitrários onde deveriam estar a versão e a variante, então um validador de UUID estrito pode rejeitá-lo.
Chance de colisão, calculada
Para IDs puramente aleatórios vale a aproximação do paradoxo do aniversário: com n IDs sorteados entre N = 2 elevado ao número de bits aleatórios, a chance de haver pelo menos um duplicado é de cerca de 1 − e^(−n²/2N), e ela chega a 50% perto de n = 1,18 × √N.
| Esquema | Bits aleatórios | IDs para 50% de chance de um duplicado | Chance de um duplicado entre 1 bilhão de IDs |
|---|---|---|---|
| UUID v4 | 122 | 2,7 × 10^18 | 9,4 × 10^-20 |
| Nano ID, 21 caracteres | 126 | 1,1 × 10^19 | 5,9 × 10^-21 |
| Nano ID, 10 caracteres | 60 | 1,3 × 10^9 | Cerca de 35% |
A última linha é a que vale lembrar: encurtar um Nano ID para deixar as URLs mais bonitas custa resistência a colisões muito depressa, então faça essa conta antes de escolher um tamanho.
No UUID v7 e no ULID, dois IDs só podem colidir se carregarem o mesmo milissegundo, então n é o número de IDs criados em um milissegundo por geradores que não compartilham um contador. Com 1.000 desses IDs no mesmo milissegundo, a chance de um duplicado é de cerca de 2,6 × 10^-17 com 74 bits aleatórios e de 4,1 × 10^-19 com os 80 do ULID. Na prática, o risco de todos os esquemas desta página é uma fonte de aleatoriedade fraca, como Math.random, e não a matemática.
Comportamento nos índices do banco de dados
Chaves primárias costumam ficar em um índice B-tree. Quando cada nova chave é maior que a anterior, como no autoincremento, no UUID v7 e no ULID, os inserts caem na borda direita do índice, as páginas ficam totalmente cheias e as linhas gravadas recentemente ficam próximas umas das outras. Chaves aleatórias, como UUID v4 e Nano ID, caem em qualquer lugar: as páginas se dividem, ficam parcialmente vazias e, quando o índice deixa de caber na memória, cada insert pode significar uma leitura aleatória.
O efeito é mais forte no MySQL e no MariaDB com InnoDB, que guardam as próprias linhas na ordem da chave primária, então chaves aleatórias espalham a tabela, e não só o índice. O PostgreSQL mantém as linhas em um heap separado, então a diferença fica restrita ao índice.
O formato de armazenamento importa tanto quanto o esquema:
- Guarde UUIDs e ULIDs em um tipo nativo de 16 bytes quando ele existir:
uuidno PostgreSQL,UUIDno MariaDB 10.7 e posteriores,BINARY(16)no MySQL.CHAR(36)ocupa mais que o dobro do espaço e deixa todos os índices maiores. - No MySQL, não passe a flag de troca para
UUID_TO_BIN(uuid, 1)com valores v7. Ela reorganiza os campos de tempo em benefício dos UUIDs v1 e quebraria a ordenação do v7. - O
uniqueidentifierdo SQL Server compara primeiro os últimos seis bytes, então valores v7 guardados nele não ficam ordenados por tempo. Os detalhes estão em UUID vs GUID. - Nano IDs são strings e são guardados como
CHARouVARCHAR, com uma collation binária se maiúsculas e minúsculas precisarem ser diferenciadas.
URLs e o que um ID entrega
Os cinco formatos são seguros em uma URL sem codificação percentual: dígitos hexadecimais, hífens, Base32 de Crockford e o alfabeto do Nano ID são todos caracteres não reservados na RFC 3986. A diferença está no tamanho (36, 26 ou 21 caracteres) e no uso de maiúsculas. UUIDs e ULIDs não diferenciam maiúsculas de minúsculas, então sobrevivem a sistemas que mudam a caixa do texto; Nano IDs não.
O que um ID revela muitas vezes é o fator decisivo:
- Autoincremento revela volume. Se a sua fatura é a 1042 hoje e a 1342 uma semana depois, a empresa emitiu cerca de 300 faturas naquela semana. IDs sequenciais também tornam trivial testar registros vizinhos, o que transforma qualquer verificação de acesso esquecida em vazamento de dados.
- UUID v7 e ULID revelam quando o registro foi criado, até o milissegundo. Para pedidos isso raramente é um problema; para contas de usuário ou prontuários médicos, pode ser. Cole um UUID v7 no inspetor do gerador de UUID v7 e ele mostra o timestamp.
- UUID v4 e Nano ID não revelam nada sobre quando ou onde foram criados.
Nenhum desses IDs é um segredo. Um ID impossível de adivinhar dificulta a sondagem, mas não substitui a verificação de que o usuário tem permissão para ver aquele registro.
Qual escolher
- Tabelas internas em um único banco de dados, nunca mostradas aos usuários:
BIGINTcom autoincremento. É a chave menor e mais simples. - Chaves primárias criadas pela aplicação ou entre serviços, quando há uma coluna UUID nativa disponível: UUID v7.
- Identificadores públicos em que o momento de criação deve ficar privado, IDs de correlação e nomes de arquivo: UUID v4, do gerador de UUID v4 ou de
crypto.randomUUID(). - IDs curtos em URLs, quando a ordem não importa: Nano ID no tamanho padrão ou perto dele.
- ULID quando a sua stack já o usa ou quando você quer a forma de texto mais curta e sem diferença entre maiúsculas e minúsculas. Em projetos novos, o UUID v7 cobre o mesmo terreno em um formato padronizado.
Uma combinação comum reúne o melhor dos dois mundos: uma chave primária BIGINT ou UUID v7 para joins e índices, mais um ID público aleatório separado, com índice único, para URLs e APIs.