- UUID v4 tem alta aleatoriedade e causa ineficiência de índice e I/O excessivo; quando usado como chave primária no PostgreSQL, provoca queda de desempenho
- Devido às inserções aleatórias, page splits e fragmentação de índice acontecem com frequência, aumentando o tamanho do log WAL e causando latência de escrita
- UUID ocupa 16 bytes, o dobro de um bigint, o que reduz a taxa de acerto de cache e leva a desperdício de memória
- Embora às vezes seja confundido com um identificador seguro, segundo a RFC 4122 UUID não é um mecanismo de segurança para impedir adivinhação
- Para novos bancos de dados, recomenda-se usar chaves baseadas em sequência de inteiros e, quando isso não for possível, optar por UUID v7 em ordem temporal
Problemas de desempenho do UUID v4
- Bancos de dados PostgreSQL que usam UUID v4 como chave primária vêm apresentando de forma consistente queda de desempenho e I/O excessivo nos últimos 10 anos
- O UUID v4 gera 122 bits aleatórios, o que impede a ordenação eficiente do índice
- Como as inserções não vão para páginas sequenciais, ocorre acesso aleatório; em atualizações e exclusões também é necessária uma busca ineficiente
- Índices B-Tree pressupõem dados ordenados, mas o UUID v4 não tem ordenação, então a eficiência de inserção é baixa
- Cada inserção é gravada em uma página arbitrária, provocando page splits no meio do índice com frequência
- Isso aumenta a latência de escrita e o volume de WAL
Estrutura do UUID e alternativas
- UUID é um identificador de 128 bits (16 bytes) e, no PostgreSQL, é armazenado como tipo uuid binário
- UUID v4 é baseado em bits aleatórios; UUID v7 inclui um timestamp nos 48 bits iniciais, melhorando a eficiência do índice
- O PostgreSQL 18 (previsto para 2025) deve oferecer suporte nativo a UUID v7
- UUID v7 permite ordenação temporal, melhorando a densidade das páginas e a eficiência do cache
Motivos para escolher UUID e suas limitações
- UUID é usado quando é necessário gerar identificadores sem colisão em ambientes com vários clientes ou microsserviços
- Ex.: geração de IDs simultaneamente em várias instâncias de banco de dados
- Porém, a RFC 4122 afirma explicitamente que “não se deve assumir que UUIDs são difíceis de adivinhar”, o que os torna inadequados como identificadores de segurança
- A probabilidade de colisão chega a 50% com 2,71×10¹⁸ UUIDs gerados; na prática, a chance é baixa, mas o custo de desempenho é alto
Ineficiência de espaço e I/O do UUID
- UUID ocupa o dobro do espaço de um bigint (8 bytes) e quatro vezes o de um int (4 bytes)
- Em tabelas grandes, isso aumenta o uso de armazenamento e o tempo de backup e restauração
- Resultado de experimento sobre densidade de páginas de índice
- índice integer: 97.64%
- índice UUID v4: 79.06%
- índice UUID v7: 90.09%
- Em testes da Cybertec, uma consulta em índice UUID v4 exigiu 8,5 milhões de acessos extras a páginas, com aumento de 31229% no I/O
- Nas mesmas condições, o índice bigint teve 27.332 acessos a buffer, enquanto o UUID v4 teve 8.562.960 acessos a buffer
Impacto em cache e memória
- Como UUID tem distribuição aleatória, a taxa de acerto do buffer cache (cache hit ratio) é menor
- Mais páginas precisam ser carregadas no cache, e páginas necessárias são frequentemente expulsas (eviction)
- A piora na eficiência do cache causa latência nas consultas e aumento no uso de memória
- Para manter o desempenho, recomenda-se reconstruir índices periodicamente (
REINDEX CONCURRENTLY) ou usar pg_repack
Formas de mitigar o impacto no desempenho
- Expandir memória: recomenda-se RAM equivalente a 4 vezes o tamanho do banco (ex.: banco de 25 GB → 128 GB de memória)
- Ajustar
work_mem: alocar mais memória para operações de ordenação pode melhorar o desempenho
- Em ambientes Rails, use
implicit_order_column para ordenar por um campo ordenável, como created_at, em vez de UUID
- O comando CLUSTER pode reorganizar a tabela com base em um campo ordenável, mas exige bloqueio exclusivo
Recomendação de chaves inteiras e sequências
- Para novos bancos de dados, recomenda-se usar chaves baseadas em sequência de inteiros
integer (4 bytes) suporta cerca de 2 bilhões de valores, e bigint (8 bytes) oferece muito mais valores únicos
- Para a maioria dos aplicativos de negócio, integer é suficiente; para serviços maiores, bigint é mais adequado
- Em vez de UUID v4, uma alternativa prática é usar UUID v7 ou a extensão sequential_uuids
Resumo
- UUID v4 causa ineficiência de índice, alto I/O e baixa eficiência de cache por causa da aleatoriedade
- Não serve como identificador de segurança e ainda causa desperdício de espaço
- Chaves sequenciais inteiras são mais adequadas para a maioria das aplicações
- Se for inevitável usar UUID, deve-se escolher UUID v7 em ordem temporal
- É melhor evitar usar
gen_random_uuid() como chave primária no PostgreSQL
1 comentários
Comentários no Hacker News
Este é um exemplo clássico de otimização prematura
Colocar dados em um identificador permanente é um tabu de gestão de dados
Se você coloca a data de nascimento no ID, como no número de identificação nacional da Noruega, depois pode enfrentar casos de imigrantes cuja data de nascimento estava errada, ou o problema de haver gente demais com aniversário em 1º de janeiro e faltar números
Na época dos catálogos em fichas, misturar dados e identificadores para reduzir o custo de busca fazia sentido, mas hoje temos bancos de dados poderosos, então não há muita necessidade disso
O problema foi definir aniversários desconhecidos como 1º de janeiro; colocar a data na chave não é o problema essencial
Se tivessem usado um valor não correspondente a data, como 00 ou 99, não haveria colisões
Colocar um timestamp no UUID não é para atribuir significado, mas para otimização de desempenho
Chaves que crescem em ordem temporal reduzem o custo de reescrita de B-tree e melhoram o desempenho de inserção no banco
“Não coloque dados em identificadores permanentes” é apenas uma regra geral; dependendo do caso, pode valer a pena aceitar esse trade-off
Por exemplo, se você usar um hash md5 como UUID para montar índices, haverá fragmentação, mas em um nível administrável
Escolher entre UUID aleatório e baseado em tempo pode gerar diferença de desempenho não em milissegundos, mas em segundos
Em bancos grandes, shard e distribuição são essenciais, então UUID funciona melhor que autoincremento
Se o formato é DDMMYYXXXXX, isso cobre até 100 mil pessoas; fico curioso se realmente dá para concentrar tanta gente assim
Provavelmente seria uma situação especial, como uma entrada em massa de refugiados em um determinado ano
UUID não deve ser usado como token de segurança
É perigoso usá-lo como recurso de segurança só porque é difícil de adivinhar
O objetivo de um valor aleatório não é apenas impedir adivinhação, mas também esconder a relação entre IDs consecutivos
Dependendo do tipo de banco, a estratégia de PK muda completamente
No Postgres, PK aleatória é ineficiente, mas em bancos distribuídos como Cockroach ou Spanner, uma chave monotonicamente crescente pode causar o problema de hot shard
UUIDv7 tem os bits superiores ordenáveis e os inferiores aleatórios, então consegue ao mesmo tempo distribuição entre nós e eficiência no armazenamento local
Em um banco típico sem shard, chaves aleatórias causam fragmentação de B-tree
Mas se houver muitas consultas por intervalo (range query), chave aleatória é desvantajosa
No fim, a escolha deve depender das características da carga de trabalho
O texto apontou bem as desvantagens de usar UUIDv4 como PK, mas o método de ofuscação de inteiro proposto parece inadequado para serviço em produção
Para um banco pequeno, UUIDv7 é um meio-termo razoável
Porque não quero que o momento de geração fique exposto
A menos que você tenha dados o bastante para a aleatoriedade do UUIDv4 virar problema de desempenho, v4 é a escolha mais segura
Há um pequeno vazamento de informação, mas em termos operacionais isso é suficientemente obscuro
Por exemplo, converter com AES-128 e depois codificar em base64 pode fazer parecer um ID de vídeo do YouTube
Vejo muitas empresas durante due diligence técnica, e a possibilidade de fazer shard rapidamente é essencial para o crescimento da empresa
Se você colocar UUID em todas as tabelas, poderá escalar durante o sharding sem mudar a estrutura
Isso traz uma vantagem de escalabilidade muito maior do que a pequena perda de espaço e tempo
No fim, como o modelo de dados era complexo, a própria migração foi difícil independentemente de haver UUID ou não
Nosso app criptografa PKs inteiras para que pareçam UUIDs
Porque, se IDs sequenciais ficarem expostos, é possível estimar o número de clientes ou fazer ataques de dicionário
IDs criptografados permitem detectar imediatamente tentativas de varredura por falha na descriptografia
Dizer que “2 bilhões já bastam” é perigoso
Todo DBA tem pelo menos um caso de pesadelo que começou com esse tipo de decisão
O texto disse que “valores aleatórios são ineficientes para ordenação”, mas na verdade ordenação por ordem de bytes é possível
Só que, como chaves aleatórias não são inseridas de forma sequencial, o rebalanceamento de B-tree acontece com frequência e isso causa perda de desempenho
PK inteira encaixava bem na memória, enquanto UUIDv4 exigia mais acesso a páginas e aumentava a latência(latency)
Este texto parece um caso de otimização prematura em que a solução veio antes do problema
UUIDv4 é bom o bastante na maioria dos casos
Questões de desempenho só precisam ser consideradas quando realmente aparecerem
Resumindo, no Postgres UUIDv7 mostra desempenho um pouco melhor que v4
Nas versões mais recentes, já é possível ter suporte a UUIDv7 sem plugin
uuidv7(), mas ainda não está claro se extensões oferecem mais recursos