10 pontos por GN⁺ 2025-12-16 | 1 comentários | Compartilhar no WhatsApp
  • 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

 
GN⁺ 2025-12-16
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

    • Acho que este exemplo na verdade é um problema de definição incorreta de valor padrão
      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
    • O número de identificação nacional da Itália também inclui o sexo, o que vira problema após cirurgia de redesignação sexual
      “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
    • UUIDv7 é apenas uma forma de geração com viés temporal(random bias), não algo que armazene informação real
      Escolher entre UUID aleatório e baseado em tempo pode gerar diferença de desempenho não em milissegundos, mas em segundos
    • Em bancos pequenos isso é otimização prematura, mas em grande escala é preciso a abordagem oposta
      Em bancos grandes, shard e distribuição são essenciais, então UUID funciona melhor que autoincremento
    • Sobre o exemplo do número norueguês, fico em dúvida se realmente pode haver tantas pessoas nascidas em 1º de janeiro
      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

    • Mesmo em bancos distribuídos, é melhor ter uma chave com tendência de crescimento do que algo totalmente aleatório
      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
    • Isso deve ser visto mais como diferença de estrutura do banco do que de tipo de banco
      Em um banco típico sem shard, chaves aleatórias causam fragmentação de B-tree
    • No Google Cloud Bigtable, usam chaves sequenciais em ordem reversa(reverse) para induzir distribuição automática
    • Em Postgres com shard, PK aleatória é vantajosa
      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
    • Se a carga for centrada em escrita e tiver forte viés temporal, PK aleatória pode ser melhor até no Postgres
  • 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

    • Eu prefiro UUIDv4 a UUIDv7
      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
    • No Postgres, gosto de usar uma única sequência
      Há um pequeno vazamento de informação, mas em termos operacionais isso é suficientemente obscuro
    • Se você só quer esconder a quantidade de usuários, basta aplicar uma permutação criptográfica à chave autoincremental
      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

    • UUIDv7 também traz vantagem de desempenho no Postgres por causa da característica monotonicamente crescente
    • Nós também sofremos no processo de sharding por não termos UUID
      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

    • Mas isso pode causar o problema de impossibilidade de descriptografia se a chave for perdida ou trocada
    • Fiquei curioso sobre a gestão de chaves — se é injetada por variável de ambiente, embutida no código, se usam um esquema AEAD como AES-GCM etc.; a administração de segurança é importante
  • 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

    • UUIDv4 é útil em ambientes distribuídos, mas você precisa aceitar o custo do espaço de 128 bits e da não sequencialidade
    • O autor disse que depois acrescentou um experimento comparando índices B-tree
      PK inteira encaixava bem na memória, enquanto UUIDv4 exigia mais acesso a páginas e aumentava a latência(latency)
    • Também houve opiniões de que a fundamentação técnica era fraca
    • Em B-tree, quanto mais crescente a chave, mais eficiente é a inserção; chaves aleatórias têm pior afinidade com cache
    • Quanto mais o acesso aos dados estiver ligado ao momento de criação, mais vantajosa em desempenho tende a ser a ordenação temporal
  • 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

    • Mas, uma vez que você começa com UUIDv4, depois é quase impossível rekeyar para int64
    • Na prática, quando o problema de desempenho aparece, a empresa já está em fase de crescimento e não há folga para trocar a PK
  • 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

    • Ainda assim, o ponto principal do texto é a recomendação de usar PK de sequência ou inteira sempre que possível
    • A partir do Postgres 18, existe a função embutida uuidv7(), mas ainda não está claro se extensões oferecem mais recursos
    • A maioria dos usuários agora provavelmente não vai mais precisar de extensão separada