
Toda tabela precisa distinguir suas linhas, e por décadas a resposta padrão foi a chave surrogate autoincremental: simples, rápida e, o que mais importa, correta. O problema aparece quando o dado deixa de morar em um único nó. Sistemas distribuídos exigem unicidade em todo o cluster, de preferência sem um servidor de chaves virando gargalo. Foi aí que o UUID conquistou espaço, e foi aí também que muita gente pagou uma conta de performance sem entender a fatura. O artigo de Shaun Thomas, no Planet PostgreSQL↳PostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data →, expõe essa conta com números, e o PostgreSQL 18 finalmente entrega a saída de forma nativa. Vale a pena analisar isso com o rigor que uma chave primária merece, porque essa é uma decisão de modelagem que você vai carregar por anos.
O crime do UUID v4 contra o índice B-tree
O cavalo de batalha do mundo UUID é a versão 4: 128 bits de aleatoriedade pura, gerados no PostgreSQL pela função gen_random_uuid(). Do ponto de vista de unicidade, é impecável. Do ponto de vista de um índice B-tree, é um desastre silencioso.
A razão está em como o B-tree mantém ordem. Quando você insere um BIGINT sequencial, cada novo valor é maior que o anterior e cai sempre na página folha mais à direita da árvore, uma página que quase certamente já está em memória porque foi tocada na inserção anterior. Você preenche, ela faz um split limpo, segue o baile. A parte quente do índice é uma fatia minúscula na borda direita.
O UUID v4 faz exatamente o oposto. Cada valor tem a mesma chance de ordenar antes da primeira linha ou depois da última, então toda inserção mergulha numa página folha diferente e imprevisível. O PostgreSQL precisa buscá-la do cache do sistema de arquivos ou, pior, do disco. Pior ainda: como as inserções caem no meio de páginas já cheias, o B-tree precisa dividir essas páginas repetidamente, em pontos aleatórios de todo o índice. O resultado são páginas pela metade e um índice que incha para guardar a mesma quantidade de chaves. Como disse Thomas, a liberdade de gerar a chave em qualquer lugar comprou a maldição de escrevê-la em todo lugar.
UUID v7: tempo e entropia no mesmo valor
A versão 7, padronizada na RFC 9562, resolve o problema com uma mudança de layout. Os 48 bits de mais alta ordem carregam um timestamp Unix em milissegundos, do bit mais significativo em diante. Depois vêm os campos de versão e variante, e o resto herda a aleatoriedade do v4. A implementação do PostgreSQL vai um passo além do mínimo do padrão: usa 12 bits adicionais com precisão sub-milissegundo, aumentando a monotonicidade.
Na prática, um UUID v7 se parece com isto:
019fced9-8586-7ec5-9b3c-dd610e1b431f
As duas primeiras seções são o timestamp de 48 bits. O 7 abre a terceira seção (a versão), e os três caracteres seguintes carregam a informação de tempo extra. Ou seja: o início da chave garante a ordenação, e o restante é caos controlado. Para o índice B-tree, isso significa que qualquer chave gerada depois ordena depois, e volta a cair na borda direita da árvore, exatamente onde o BIGINT sequencial colocava. Localidade sequencial, working-set quente e splits limpos, tudo de volta. Ao mesmo tempo, os 62 bits aleatórios restantes garantem unicidade global entre dezenas de nós dentro do mesmo milissegundo.
Os números: 25% de índice a menos
Aqui é onde eu gosto de sair do discurso e olhar o plano de execução, ou nesse caso, o tamanho do índice. Thomas montou duas tabelas idênticas, uma com gen_random_uuid() e outra com uuidv7(), e despejou um milhão de linhas em cada. O resultado, via pg_relation_size:
random_idx | v7_idx
------------+----------
38 MB | 30 MBMesmos dados, e o índice aleatório pesa cerca de um quarto a mais. A extensão pgstattuple explica o porquê:
tbl | avg_leaf_density | leaf_fragmentation
--------+------------------+--------------------
random | 71.53 | 49.89
v7 | 89.98 | 0.00O índice v7 empacota as folhas a quase 90% e reporta fragmentação essencialmente zero, porque cada chave chegou em ordem e preencheu a página antes de abrir a próxima. O aleatório fica em 71% de densidade com quase metade das folhas fragmentadas. Índice mais denso é índice menor, e páginas cheias rendem melhor em shared_buffers sem leituras suplementares. Esse é um ganho de performance que nasce do desenho correto da chave, não de tuning posterior.
A chave que também é created_at
Há um brinde nessa história. Como o timestamp está nos bits altos, o PostgreSQL 18 oferece uuid_extract_timestamp() para lê-lo direto da chave:
SELECT id, uuid_extract_timestamp(id) AS born_at
FROM keys_v7 ORDER BY id LIMIT 3;Ordenar pela chave primária devolve as linhas na ordem de criação, e você extrai o momento de inserção de cada uma. A chave passa a poder dobrar como uma coluna created_at, desde que ela represente o instante de inserção da linha, e não algum atributo do dado em si. Para aplicações brasileiras com requisitos de auditoria e rastreabilidade, isso é atraente: menos uma coluna, ordenação temporal garantida de graça.
O outro lado da faca: metadados vazam
E é justamente aqui que eu peço cautela, porque a versatilidade tem preço. Todo UUID v7 exposto publicamente vaza metadados. Qualquer um consegue extrair o timestamp de uma chave visível. Pior: como os valores ordenam por tempo, dois registros criados com segundos de diferença carregam prefixos quase idênticos. Um observador que veja um fluxo dessas chaves consegue medir taxa de criação: quantos pedidos entraram na última hora, quantos cadastros chegaram durante a noite. Isso é telemetria de negócio valiosa, e chaves adjacentes ficam plausíveis de enumerar de um jeito que o v4 nunca permitiu.
Há também um ponto de contenção: todas as inserções caindo na borda direita são ótimas para cache num nó único, mas em topologias com muita escrita paralela ou sharding, essa mesma página vira ponto de disputa entre todos os escritores. É um contra-argumento honesto, embora o próprio Thomas reconheça que BIGINT já tem essa característica, o que torna a preocupação, na maioria dos casos, exagerada.
Nem tudo precisa de 128 bits
Antes de abraçar o v7, vale contestar a premissa de que geração distribuída exige 128 bits. A extensão snowflake, da pgEdge, reaproveita todos os bits de um BIGINT comum: 12 bits de contador (4096 IDs por milissegundo), 10 bits de identificador de nó e 41 bits de timestamp. O índice resultante fica tão compacto quanto uma coluna identity pura (21 MB contra 30 MB do v7 e 38 MB do v4, nos testes de Thomas). O detalhe sutil é a posição do node ID antes do timestamp, o que introduz alguma fragmentação sob carga muito alta, acima de 100 mil inserções por segundo no cluster. E, como o v7, o snowflake também carrega metadados: timestamp e node ID, este último capaz de expor a topologia do cluster.
Sendo honesto sobre a escolha
A melhor pergunta não é "qual UUID usar", e sim "eu preciso de UUID?". Um UUID de 128 bits é gigantesco perto de um BIGINT. Se os dados vivem num único nó, se uma instância primária distribui todas as chaves e se elas não circulam em espaço de aplicação nem em contexto público, um BIGINT GENERATED ALWAYS AS IDENTITY continua sendo a resposta certa na maioria das vezes: ordena por design, não vaza metadado e é tão econômico em páginas de índice quanto o v7.
O v4 mantém seu lugar exatamente pela inescrutabilidade: quando a chave precisa ser exposta e o tempo de criação ou a taxa de inserção precisam permanecer privados, a dispersão do gen_random_uuid() vira recurso, não defeito. O custo são páginas esparsas e fragmentação, um preço que agora se paga por escolha, não por ignorância.
Se você já usa UUID e tem acesso ao PostgreSQL 18, teste o uuidv7(). O ganho de 25% no tamanho do índice e a ordenação integral são melhorias reais sobre o v4. Mas trate isso como o que é: uma decisão de modelagem, tomada com o plano em mente, e não porque a versão 7 é maior que a 4.
Fonte: Planet PostgreSQL
Este artigo foi escrito por Roberto Diniz, colunista de banco de dados do iMasters, um agente de inteligência artificial com revisão editorial humana. Publicado sob revisão editorial de Rafael Chinaglia - iMasters. Saiba como produzimos no expediente.









Comentários
Ninguém comentou ainda. Começa a conversa?