Artigo Build·Desenvolvimento·12 min de leitura

UUID v7 no PostgreSQL: performance, indexação e quando evitar

UUID v7 gera IDs monotônicos que melhoram indexação B-tree no PostgreSQL. Entenda como funciona, quando v4 ainda faz sentido e os trade-offs reais.

Vitor Morais

Por Vitor Morais

Fundador do MochaLabz ·

UUID v7 no PostgreSQL resolve um problema que UUID v4 carrega desde sempre: fragmentação de índice B-tree causada por valores aleatórios. Enquanto v4 espalha inserts por toda a árvore — forçando page splits constantes —, v7 gera identificadores monotônicos baseados em timestamp que se acumulam no final do índice, na mesma direção que o tempo. O resultado prático é insert mais rápido, leitura sequencial mais eficiente e índices que ocupam menos espaço em disco. Mas v7 não é bala de prata, e trocar sem entender os trade-offs cria problemas novos.

Como UUID v7 funciona por dentro

UUID v7 embute um timestamp Unix em milissegundos nos primeiros 48 bits do identificador. Os bits restantes são preenchidos com dados aleatórios, mas a ordenação temporal já está garantida pela porção inicial. Isso torna o valor monotonicamente crescente — dois UUIDs gerados em sequência sempre respeitam a ordem cronológica, mesmo vindos de processos diferentes (desde que os relógios estejam sincronizados com precisão razoável).

UUID v4, por contraste, é 122 bits de puro random. Não existe relação entre a hora de criação e o valor gerado. Para a aplicação isso geralmente não importa — um ID é um ID. Mas para o mecanismo de indexação do banco, a diferença é brutal.

Por que B-tree sofre com UUID v4

Índices B-tree no PostgreSQL armazenam valores em páginas ordenadas de 8 KB. Quando a chave primária é um inteiro auto-incremento (SERIAL, BIGSERIAL), novos registros sempre caem na última página da árvore — o chamado right-append pattern. O banco mantém essa página no buffer pool, os inserts são sequenciais e os page splits são raros.

UUID v4 destrói esse padrão. Cada novo valor cai numa posição aleatória da árvore, forçando o PostgreSQL a carregar páginas dispersas do disco pro buffer pool, dividir páginas cheias (page split) e reorganizar ponteiros. Em tabelas com milhões de registros, isso se traduz em:

  • Write amplification — cada insert pode tocar 3–5 páginas em vez de 1.
  • Índice inchado — page splits criam páginas parcialmente vazias (fill factor real cai pra 50–70%).
  • Buffer pool churn — páginas aleatórias expulsam páginas quentes do cache, prejudicando leituras.
  • Vacuum mais pesado — mais dead tuples espalhadas significam mais trabalho pro autovacuum.

UUID v7 elimina a raiz do problema. Como os valores são monotônicos, o padrão de insert volta a ser right-append — igual a um BIGSERIAL, mas com a vantagem de ser globalmente único sem coordenação centralizada.

Gerando UUID v7 no PostgreSQL

PostgreSQL 18 inclui a função nativa uuidv7(), que gera UUIDs versão 7 diretamente no banco. Em versões anteriores (14–17), é preciso usar uma extensão como pg_uuidv7 ou gerar o valor na aplicação. O caminho mais comum em stacks com Supabase ou ORMs como Drizzle/Prisma é gerar no lado da aplicação e enviar como parâmetro — o que funciona igualmente bem, desde que o relógio da máquina esteja razoavelmente sincronizado.

Criar tabela com UUID v7 como PK no PostgreSQL 18+

CREATE TABLE orders ( id uuid PRIMARY KEY DEFAULT uuidv7(), customer_id uuid NOT NULL, total_cents bigint NOT NULL, created_at timestamptz NOT NULL DEFAULT now() );

Para versões anteriores ao PostgreSQL 18, a geração pode ser feita no lado do servidor da aplicação. Em TypeScript com a lib uuidv7:

Gerar UUID v7 em TypeScript

import { uuidv7 } from 'uuidv7'; const id = uuidv7(); // Ex.: '01902e5c-4a3b-7def-8c91-3f4a5b6c7d8e' // Os primeiros 48 bits codificam o timestamp em ms

Timestamp extraível

Como o timestamp está embutido no UUID v7, é possível extrair a data de criação de qualquer registro sem consultar a coluna `created_at`. Isso é útil pra debug e auditoria, mas também significa que o ID vaza informação temporal — se isso for problema de privacidade, v4 continua sendo a escolha certa.

Medindo a diferença: EXPLAIN ANALYZE na prática

A melhoria de performance do UUID v7 sobre v4 não é teórica. Ela aparece em três métricas concretas que qualquer instância PostgreSQL consegue medir: tamanho do índice em disco, latência média de INSERT em batch, e eficiência de range scans sobre a PK.

Comparar tamanho de índice v4 vs v7 após 1M inserts

-- Após popular ambas as tabelas com 1M registros: SELECT relname AS tabela, pg_size_pretty(pg_relation_size(indexrelid)) AS tamanho_indice FROM pg_stat_user_indexes WHERE relname IN ('orders_v4', 'orders_v7') ORDER BY relname;

Em testes típicos com 1 milhão de registros, o índice B-tree de uma tabela com UUID v4 ocupa significativamente mais espaço que o equivalente com v7 — a diferença varia conforme o hardware e a configuração do fill_factor, mas o padrão é consistente: menos page splits resultam em páginas mais cheias e índice menor.

Para medir a latência de insert, a forma mais direta é usar \timing no psql e rodar um batch de INSERT INTO ... SELECT com generate_series. A diferença fica mais visível à medida que a tabela cresce — em tabelas pequenas (< 100 mil registros), v4 e v7 são quase indistinguíveis.

Range scans — queries do tipo WHERE id > $1 ORDER BY id LIMIT 100 — se beneficiam diretamente da monotonicidade. Como UUIDs v7 são cronológicos, um range scan sobre a PK equivale a uma paginação temporal. Com v4, o mesmo range scan retorna registros em ordem arbitrária, sem utilidade prática pra cursores.

Quando UUID v4 ainda é a escolha certa

UUID v7 não substitui v4 em todos os cenários. Existem pelo menos três situações onde v4 continua fazendo mais sentido:

  1. Privacidade do identificador. UUID v7 revela quando o registro foi criado. Em APIs públicas onde o timestamp do ID é informação sensível (tokens de acesso, IDs de usuário expostos em URL), v4 é preferível porque não vaza cronologia.
  2. Compatibilidade com sistemas legados. Muitos ORMs, bibliotecas de validação e APIs de terceiros esperam UUID v4 ou tratam qualquer UUID como opaco. Migrar pra v7 pode exigir atualizar validações, serializers e testes — custo que nem sempre se paga em tabelas pequenas.
  3. Tabelas que nunca passam de 100 mil registros. A vantagem de indexação do v7 escala com o volume. Em tabelas de configuração, lookup ou referência que raramente crescem, a diferença de performance é irrelevante e v4 é mais simples de gerar em qualquer linguagem.

Relógio desalinhado quebra a monotonicidade

UUID v7 depende do relógio do sistema. Se a aplicação roda em múltiplos servidores com NTP mal configurado (drift > 1 segundo), dois UUIDs "simultâneos" podem sair fora de ordem. Em stacks com Supabase ou PaaS gerenciado isso raramente é problema — o relógio é sincronizado pelo provider. Em infra própria, verifique o chrony ou systemd-timesyncd antes de confiar na ordenação.

UUID v7 vs v4 vs BIGSERIAL: comparação direta

Trade-offs de cada tipo de chave primária
CritérioBIGSERIALUUID v4UUID v7
Ordenação temporalSim (auto-incremento)Não (aleatório)Sim (timestamp embutido)
Globalmente único sem coordenaçãoNão (depende de sequence)SimSim
Performance de insert em escalaExcelente (right-append)Ruim (random scatter)Excelente (right-append)
Tamanho do valor8 bytes16 bytes16 bytes
Vaza cronologia no IDSimNãoSim
Suporte nativo no PostgreSQLSempreSempre (gen_random_uuid)A partir do PG 18 (uuidv7)

A tabela deixa claro o posicionamento: UUID v7 combina a vantagem de performance do BIGSERIAL com a unicidade global do UUID. O custo é o dobro do tamanho (16 bytes vs 8) e a exposição do timestamp — trade-offs aceitáveis na maioria dos casos onde um UUID como chave primária já é a decisão tomada.

Migrando uma tabela existente de v4 para v7

Migrar registros existentes de UUID v4 pra v7 quase nunca vale a pena. Os IDs antigos já estão referenciados em foreign keys, logs, URLs, webhooks e caches externos. Reescrever todos é arriscado e caro. A abordagem pragmática é:

  1. Mudar o default da coluna pra uuidv7() (ou gerar v7 na aplicação) — novos registros já entram monotônicos.
  2. Manter os UUIDs v4 antigos como estão — a coluna continua sendo uuid, o tipo não muda.
  3. Rodar REINDEX na tabela depois de um tempo, quando a proporção de v7 for majoritária — o índice vai se reorganizar naturalmente com os novos valores monotônicos na ponta.
  4. Monitorar pg_stat_user_indexes pra confirmar que o tamanho do índice estabiliza ou diminui após o reindex.

Alterar default sem reescrever dados existentes (PG 18+)

ALTER TABLE orders ALTER COLUMN id SET DEFAULT uuidv7(); -- Registros antigos (v4) continuam intactos. -- Novos inserts usam v7 automaticamente.

Quem usa versões anteriores ao PostgreSQL 18 pode criar uma função PL/pgSQL que implementa a geração, ou simplesmente gerar na aplicação e passar o valor explicitamente no INSERT. O artigo sobre diferenças entre v4 e v7 detalha os formatos de bits de cada versão.

Armadilhas que a documentação não destaca

  • Cursor-based pagination com v7 parece mágica, mas tem edge case. Se dois registros forem criados no mesmo milissegundo, a ordem entre eles depende dos bits aleatórios. Pra paginação com garantia estrita de ordem, combine id com created_at como tiebreaker — ou use um sub-milissegundo counter (algumas implementações de v7 já fazem isso).
  • `ORDER BY id` não substitui `ORDER BY created_at`. Embora v7 seja cronológico, a resolução é de milissegundos. Se o timestamp importa pra regra de negócio (SLA, auditoria, billing), mantenha a coluna created_at com timestamptz — não dependa do ID pra isso.
  • ORMs podem validar versão. Prisma, TypeORM e Drizzle usam bibliotecas de validação de UUID que podem rejeitar v7 se estiverem verificando a version nibble. Teste antes de migrar e atualize as dependências se necessário.
  • Replicação lógica ordena por LSN, não por PK. Se a arquitetura usa CDC (Change Data Capture) ou replicação lógica, a ordem dos eventos não depende do UUID — depende do Write-Ahead Log. A monotonicidade do v7 ajuda o índice local, não o pipeline de replicação.

Teste com dados reais, não com tabela vazia

A diferença de performance entre v4 e v7 é marginal em tabelas com menos de 100 mil registros. Se quiser validar o ganho antes de migrar, popule uma tabela de teste com pelo menos 1 milhão de rows, rode EXPLAIN (ANALYZE, BUFFERS) em queries representativas e compare shared_hit ratio e I/O.

Para quem está otimizando queries e índices além da PK, o guia de performance com índices SQL cobre estratégias complementares — partial indexes, covering indexes e análise de pg_stat_user_tables.

Perguntas frequentes

UUID v7 é compatível com a coluna uuid do PostgreSQL?+

Sim. O tipo `uuid` do PostgreSQL armazena qualquer UUID válido independente da versão. Não é preciso alterar o tipo da coluna pra usar v7 — basta mudar o valor default ou a forma de geração na aplicação.

Preciso do PostgreSQL 18 pra usar UUID v7?+

Não. A função nativa `uuidv7()` chegou no PostgreSQL 18, mas é possível gerar o valor na aplicação (TypeScript, Python, Go) e enviar como parâmetro no INSERT. Extensões como `pg_uuidv7` também funcionam em versões anteriores.

UUID v7 substitui a coluna created_at?+

Na prática, não. O timestamp embutido no UUID v7 tem resolução de milissegundos e pode ser extraído, mas regras de negócio que dependem de data/hora devem usar uma coluna `timestamptz` explícita — mais legível, indexável separadamente e sem acoplamento ao formato do ID.

UUID v7 é mais seguro que UUID v4?+

Depende do critério. UUID v4 é completamente aleatório, o que dificulta enumeração. UUID v7 revela o timestamp de criação, facilitando inferências sobre volume e cronologia. Pra tokens de acesso ou IDs expostos publicamente onde a temporalidade é sensível, v4 continua sendo mais adequado.

Qual o impacto de misturar UUID v4 e v7 na mesma tabela?+

Nenhum problema funcional — o tipo da coluna é o mesmo. O índice B-tree vai ter uma porção fragmentada (registros antigos v4) e uma porção ordenada (registros novos v7). Com o tempo, à medida que os inserts v7 dominam, um `REINDEX` reorganiza a árvore e o ganho de performance se consolida.

Gere UUIDs v7 pra teste agora

Gere UUIDs v1, v4 e v7 no navegador sem instalar nada — útil pra seed de banco, teste de migração e debug de formato.

Gerar UUID v7 →
#uuid-v7-postgresql#uuid-v7-vs-uuid-v4#postgresql-indexacao#b-tree-performance#chave-primaria-uuid#postgresql-18#supabase-uuid#banco-de-dados#performance-sql

Artigos relacionados