UUID v4 vs v7: quando migrar e o que esperar de ganho real
Por que UUIDs v4 aleatórios fragmentam o índice do seu banco e como o v7, ordenado por tempo, devolveu +260% de throughput no módulo de relatórios de um SaaS de varejo europeu.
Um cliente nos procurou convencido de que precisava reescrever o sistema inteiro em Go. O sintoma estava no módulo de relatórios de vendas de um SaaS de varejo europeu: os relatórios ficavam mais lentos conforme o intervalo crescia (mês → trimestre → semestre), e essa latência sangrava para as inserções e para outros serviços internos. Profiling e benchmarking apontaram o gargalo para outro lugar — o banco de dados, não a linguagem — e a solução não trocou uma linha de linguagem.
O problema do UUID v4
UUID v4 é aleatório por design. Cada novo identificador cai em uma posição imprevisível do espaço de chaves. Isso parece inofensivo até você lembrar como um índice B-tree do PostgreSQL funciona: ele mantém as chaves ordenadas em páginas. Quando você insere chaves em ordem (ou quase), as inserções caem nas últimas páginas e o índice cresce de forma compacta. Quando você insere chaves aleatórias, cada inserção pode cair em qualquer página — espalhando escritas por toda a árvore.
O resultado é fragmentação: o índice incha, as páginas ficam parcialmente cheias, o cache do banco fica menos eficiente porque páginas "quentes" estão espalhadas, e cada inserção pode forçar um page split. Em volume baixo isso não aparece. Em bancos distribuídos com bilhões de registros, vira o gargalo dominante.
Por que o v7 resolve
UUID v7 (padronizado na RFC 9562) coloca um timestamp de milissegundos nos bits mais significativos, seguido de bits aleatórios. Na prática, isso torna os UUIDs ordenáveis por tempo: identificadores gerados em sequência ficam próximos no espaço de chaves.
Para o índice B-tree, isso muda tudo. As inserções voltam a cair em páginas adjacentes, como uma chave sequencial faria, mas você mantém as vantagens do UUID — geração distribuída sem coordenação central, sem expor contagem de registros, sem colisão prática. É o melhor dos dois mundos: a unicidade global do UUID com o comportamento de localidade de uma chave sequencial.
O ganho real
Neste caso, a migração para v7 entregou +260% de throughput de inserção e cerca de -40% no tempo de queries de range — porque registros próximos no tempo agora estão próximos no índice e no disco. Tudo isso com zero downtime e rollback preparado, instrumentado com Jaeger para validar cada etapa em tracing distribuído. A reescrita completa para Go que o cliente cogitava teria custado meses e não resolveria nada: o gargalo nunca esteve na linguagem da aplicação.
Como migrar com segurança
Migrar chave primária em tabelas grandes — ainda mais distribuídas — exige cuidado. O caminho que usamos evitou um big-bang e uma migration de schema destrutiva:
- Tabela de lookup mapeando o
idv4 ↔ o novouuidv7 — a fonte de verdade da correspondência durante a transição. Uma ferramenta customizada em Rust gerou os v7 e populou o mapa para os bilhões de registros, em janelas controladas. - Trocar o id pelo v7 progressivamente nas tabelas, consultando a lookup — sem uma coluna nova convivendo com a antiga.
- Propagar o v7 nas relações e referências antes de mexer no índice: tanto foreign keys tradicionais quanto referências informais (um valor "referenciado" entre bancos distintos, sem FK formal) passam a apontar para o v7.
- Reconstruir o índice aos poucos, à medida que o v7 já estava propagado — evitando o custo e o lock de recriar tudo de uma vez.
- Medir antes e depois — sem benchmark comparativo, você não sabe se ganhou.
Não foi o caminho mais óbvio, mas funcionou sem downtime. Como o v7 carrega um timestamp, não dá para derivá-lo do v4: um mapa de correspondência é mesmo a peça central — e cada caso pede o seu próprio plano.
Quando vale a pena
Migrar UUID v4 para v7 vale quando: você tem tabelas grandes (dezenas de milhões de linhas ou mais), usa UUID como chave primária, e observa inserção lenta ou índices inchados em relação ao volume de dados. Para projetos novos, é simples: comece em v7 e evite o problema desde o início.
Não vale o esforço em tabelas pequenas, onde a fragmentação é irrelevante. Como sempre em performance, a decisão é guiada pela métrica, não pela moda. Mas quando o sintoma bate, a migração para v7 é um dos melhores retornos sobre esforço que existem — corrige a causa raiz sem reescrever a aplicação.