UUID v1 vs v4 vs v7: escolhendo uma chave primária de BD

Publicado em 2026-04-13 8 min de leitura

Resumo (TL;DR)

Sem rodeios: usar UUID v4 como chave primária de uma tabela com muitos inserts é tiro no pé. Olhei recentemente uma carga PostgreSQL 16 em que events.id vinha por padrão de gen_random_uuid() (um v4), e cada INSERT caía em uma folha aleatória da B-tree — esfriando o shared_buffers e empurrando a fragmentação de índice para a casa dos altos dígitos simples reportados pelo pgstattuple. Trocar o default da coluna por um gerador v7 — mesmo tipo uuid de 16 bytes, mesmo índice — cortou a latência média de INSERT para cerca de um terço do que era, e os números de fragmentação estabilizaram. Escolher uma versão de UUID é uma decisão de design de banco de dados, não uma decisão de criptografia. Um UUID é um valor de 128 bits escrito como dígitos hexadecimais 8-4-4-4-12, com quatro bits fixos como versão e dois ou três bits fixos como variante. A versão 1 carimba timestamp e um identificador de nó (historicamente um endereço MAC) nesses 128 bits — aproximadamente ordenável, mas vaza o host. A versão 4 preenche os bits não reservados com dados aleatórios: forte imprevisibilidade, mas sem ordem, então inserções caem em posições arbitrárias do índice. A versão 7, formalizada na RFC 9562 (2024), coloca um timestamp Unix em milissegundos nos bits altos e uma cauda aleatória, combinando a segurança do v4 com a localidade de índice do v1. Para uma API pública em que aleatoriedade opaca realmente importa, v4 ainda é o padrão. Em quase todos os outros lugares, v7 é a melhor chave primária. v1 é legado — aceito por bancos, mas não deveria ser uma escolha nova.

Contexto e conceitos

Um UUID tem 128 bits. Por convenção é impresso como 32 dígitos hexadecimais agrupados com hífens: xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx. O dígito M guarda a versão (1, 4, 7 etc.) e os bits superiores de N guardam a variante. Todo o resto é específico da versão.

A versão 1 foi desenhada para unicidade entre máquinas e ao longo do tempo. Seu timestamp de 60 bits conta intervalos de 100 nanossegundos desde 1582-10-15, um campo clock-sequence lida com reversões de relógio, e um node ID de 48 bits originalmente era o endereço MAC da rede. A consequência de privacidade é direta: um UUID v1 gerado no seu laptop codifica o MAC do laptop, e um uuid -d de uma linha reverte. Bibliotecas modernas às vezes randomizam o node ID para evitar o vazamento, mas muitas ainda seguem a regra original.

A versão 4 é 122 bits de aleatoriedade com 6 bits fixos para versão e variante. Ela assume um RNG forte (o crypto.randomUUID() do navegador ou o gen_random_uuid() do PostgreSQL). Colisões são efetivamente impossíveis em qualquer escala realista — o limite do aniversário dá cerca de uma em um trilhão depois de gerar aproximadamente um bilhão de v4s. A desvantagem é que v4s sucessivos não são correlacionados, então inseri-los em um índice toca páginas aleatórias, prejudicando localidade de cache e amplificação de escrita (WAL mais full-page writes no Postgres).

A versão 7 faz parte da RFC 9562, publicada em 2024, que obsoletou a RFC 4122 e adicionou as versões 6, 7 e 8. Um UUID v7 guarda um timestamp Unix de 48 bits em milissegundos nos bits altos, seguido pela tag de versão, um campo pequeno rand_a, a tag de variante e uma cauda rand_b de 62 bits. O efeito prático é que valores v7 gerados dentro do mesmo milissegundo ficam adjacentes na ordem de classificação, e valores entre milissegundos se classificam cronologicamente. A cauda aleatória ainda fornece entropia mais que suficiente para unicidade dentro de um milissegundo. O PostgreSQL 18 traz um uuidv7() nativo; em versões anteriores você pode usar a extensão pg_uuidv7 ou gerá-los na camada de aplicação (Node uuid 9.x, Python uuid6).

Os bits de variante importam porque distinguem a família RFC 9562 / 4122 dos UUIDs legados da Microsoft e Apollo. Para este guia, assuma a variante RFC (o primeiro dígito hex do grupo N é 8, 9, a ou b).

O formato de armazenamento é uma preocupação separada. O tipo nativo uuid do PostgreSQL 16 guarda o valor como 16 bytes; o MySQL normalmente usa BINARY(16) ou CHAR(36) — a forma string dobra o armazenamento e torna as comparações char a char em vez de byte a byte. A escolha de versão e a escolha de formato de armazenamento interagem: classificar v7 como valor binário é barato e correto, classificá-lo como string hex é correto mas mais lento, e classificar v4 não tem sentido independentemente do armazenamento.

Comparação e dados

Propriedadev1v4v7
Entrada de geraçãoTimestamp + clock sequence + node ID122 bits aleatóriosTimestamp Unix de 48 bits em ms + cauda aleatória
PrivacidadeVaza node ID (frequentemente o MAC)Sem info de host nem tempoVaza tempo de criação em ms, sem host
Ordenável por tempoSim (mas ordem de bytes ≠ ordem de tempo sem rearranjo)NãoSim — ordem lexicográfica bate com ordem cronológica
Localidade de índiceModeradaRuim (inserts aleatórios pela B-tree)Boa (quase monotônica)
Caso de uso típicoSistemas legados, alguns IDs COM/WindowsIDs de API pública, tokens de sessão, saltsLogs de evento, inserts em alto volume, tabelas com paginação por tempo
EntropiaBaixa (a maioria dos bits é tempo/node)Alta (cerca de 122 bits)Cauda alta (cerca de 74 bits), baixa colisão dentro do ms

Um modelo mental aproximado: v4 maximiza imprevisibilidade ao custo do comportamento de índice, v7 mantém imprevisibilidade suficiente para a maioria das aplicações enquanto restaura o padrão “inserir no fim” que bancos adoram, e v1 é um artefato histórico que vale reconhecer mas não escolher.

Cenários reais

Cenário 1 — Log de eventos append-heavy. É exatamente a carga da anedota de abertura. Uma tabela que ingere milhões de linhas por dia e costuma ser consultada como “últimas 24 horas, ordenadas por tempo” se beneficia diretamente do v7. Novas linhas caem no fim do índice da chave primária, então as páginas quentes permanecem aquecidas e varreduras de intervalo por faixas de tempo mapeiam em segmentos contíguos de índice. Migrar de v4 para v7 aqui muitas vezes reduz latência de escrita e fragmentação de índice sem mudar nenhum código de consulta — tipicamente uma mudança de uma linha no default da coluna.

Cenário 2 — IDs públicos voltados ao usuário. Links compartilháveis como /orders/{id} precisam ser imprevisíveis para que visitantes não consigam enumerar pedidos de outros usuários. v4 é o padrão seguro. Se você também quer os benefícios do v7, esteja ciente de que um v7 revela seu timestamp de criação no milissegundo, o que pode estar OK para pedidos mas pode vazar sinais de negócio (por exemplo, volume exato de checkout por minuto) em contextos mais sensíveis. O compromisso que recomendei a times é um padrão de ID duplo: v7 internamente como chave primária, e um v4 separado ou um slug aleatório curto exposto para fora. Você fica com o comportamento de índice sem vazar tempo externamente.

Cenário 3 — Sistemas multi-região ou sharded. O prefixo de timestamp do v7 significa que duas regiões gerando UUIDs no mesmo milissegundo vão se intercalar limpamente por tempo, mas dentro de um milissegundo não há garantia de ordem entre regiões. Se você precisa de uma ordem mais estrita entre regiões, ULID (um timestamp de 48 bits + 80 bits de aleatoriedade codificados como Crockford Base32) tem propriedades quase idênticas e uma forma textual mais compacta de 26 caracteres. IDs estilo Snowflake vão além, incluindo um ID de máquina explícito para ordem por nó (o design original do Twitter e a variante do Discord são ambos de 64 bits), ao custo de exigir coordenação para alocar esses IDs.

Equívocos comuns

“UUIDs são sempre lentos em bancos de dados.” São mais lentos que um int de 4 bytes em termos brutos, mas o custo real vem da fragmentação de inserts aleatórios em índices B-tree, que o v7 elimina em grande parte. Armazenar UUIDs como 16 bytes em vez de uma string de 36 caracteres corta o tamanho do índice pela metade e acelera comparações. Muitos benchmarks de “UUIDs são lentos” são na verdade “v4 armazenado como CHAR(36) no MySQL é lento.”

“v4 é a única versão segura.” A cauda aleatória do v7 ainda é um grande pool de entropia, e para a maioria das aplicações — referências de sessão, IDs de API — é imprevisível o suficiente para que um atacante não as enumere. Onde a previsibilidade importa é no prefixo de timestamp: o v7 revela quando a linha foi criada. Se isso é aceitável (e geralmente é), v7 é uma escolha razoável até para IDs externos.

“UUIDs precisam ser armazenados como strings.” A forma string tem 36 caracteres (32 hex mais 4 hífens) contra 16 bytes binários. Binário é mais compacto e classifica corretamente como bytes, o que importa para v1 com sua ordem de bytes não monotônica e para v7 em que a ordem de bytes deveria alinhar com o tempo. O tipo uuid do PostgreSQL já guarda o valor como 16 bytes, então não há nada extra para pensar ali.

“O vazamento de MAC do v1 não importa porque ninguém olha.” O MAC em um UUID v1 é uma reversão conhecida — uuid -d e qualquer ferramenta forense extraem. Se seus UUIDs aparecem em URLs, tickets de suporte ou dumps de log compartilhados com terceiros, é uma divulgação real de informação.

Checklist

  1. Esse UUID vai aparecer em um índice de banco que recebe muitos inserts? Prefira v7. Recorra a v4 só se a imprevisibilidade do tempo de criação for essencial.
  2. O UUID é visível para usuários finais ou parceiros? Qualquer versão funciona; só confirme se o vazamento de timestamp do v7 é aceitável para o contexto.
  3. Você está no Postgres? Armazene como uuid (16 bytes). No MySQL, use BINARY(16) a menos que a compatibilidade de string compense o custo de tamanho.
  4. Você precisa de garantias de ordem entre múltiplos geradores? v7 sozinho não é suficiente; considere ULID com o mesmo prefixo de tempo, ou um ID estilo Snowflake com ID de máquina explícito.
  5. Ainda há v1 no código? Documente o vazamento de MAC e planeje uma migração quando o schema permitir.
  6. Você está gerando UUIDs no lado do cliente? Use uma biblioteca que chame um RNG criptograficamente forte (crypto.randomUUID() em navegadores modernos para v4; bibliotecas v7 normalmente embrulham o mesmo RNG).

Ferramenta relacionada

O gerador de UUID da Patrache Studio produz UUIDs v4 e v7 localmente, então os valores gerados nunca são logados em um serviço de terceiros. UUIDs quase sempre viajam dentro de payloads JSON — Formatação, validação e JSON Schema na prática cobre os padrões de schema que mantêm esses IDs bem tipados à medida que se movem entre serviços. Quando você precisa de uma forma textual compacta — por exemplo, um ID curto de 22 caracteres derivado de um UUID de 16 bytes — as regras em Base64 e codificação de URL: propósito, armadilhas e uso correto explicam por que Base64URL, em vez de Base64 padrão, é a escolha certa.

Referências