Artigo Build·Desenvolvimento·13 min de leitura

UUID como Chave Primária: Vantagens, Problemas e Alternativas

Usar UUID como chave primária é decisão arquitetural com benefícios reais em sistemas distribuídos — mas com armadilhas sérias de performance se você escolher a versão errada. Veja o trade-off completo, por que UUID v7 mudou o jogo e como comparar com ULID, NanoID, CUID2 e Snowflake.

Vitor Morais

Por Vitor Morais

Fundador do MochaLabz ·

🔑

Gere UUIDs para seu banco de dados

v1, v4 e v7 — escolha a versão correta para cada caso de uso.

Usar gerador de UUID →

Usar UUID como chave primária deixou de ser decisão controversa nos últimos anos: PostgreSQL tem suporte nativo, MySQL melhorou drasticamente, frameworks modernos (Prisma, Drizzle, Hibernate) já tratam como cidadão de primeira classe. Mas a escolha errada da versão (v4 vs v7) ainda pode custar 30–50% de performance em tabelas grandes. Este guia cobre os trade-offs, por que UUID v7 resolve o problema clássico de fragmentação e como comparar com alternativas modernas (ULID, NanoID, CUID2, Snowflake).

Por que considerar UUID como chave primária

A motivação principal é geração descentralizada. Com BIGINT auto-incremento, só o banco pode atribuir um ID — o que significa um round-trip obrigatório no INSERT. UUID quebra essa dependência: qualquer instância do seu serviço gera o ID localmente, sem consultar nada. Isso destrava várias arquiteturas comuns:

  • Microserviços: serviços diferentes geram IDs sem coordenação central.
  • Multi-região / multi-master: evita conflito entre regiões que escrevem em paralelo.
  • Geração no cliente (offline-first): apps mobile criam IDs antes de sincronizar.
  • Bulk insert: aplicação prepara milhões de linhas com IDs antes de mandar para o banco.
  • IDs opacos em APIs públicas: não revelam volume nem ordem cronológica.

BIGINT auto-incremento vs UUID — quando cada um vence

BIGINT auto-incremento vs UUID v4 vs UUID v7 como chave primária
CritérioBIGINT (auto-inc)UUID v4UUID v7
Tamanho8 bytes16 bytes16 bytes
Geração descentralizada
Performance INSERT em tabela grandeExcelenteRuim (fragmentação)Excelente
Ordenação por criaçãoSimNãoSim (timestamp)
Ocupa espaço no índiceMínimoMaiorMaior
Revela volume de dadosSimNãoParcialmente (timestamp)
Seguro em URL públicaNãoSimSim
Suporte nativo PostgresSimSimSim (em v18+)
Suporte nativo MySQLSimSimNão (gerar no app)

O problema clássico: fragmentação do UUID v4

UUID v4 é gerado por aleatoriedade criptográfica. Quando um banco relacional usa B-tree balanceada como estrutura de índice (padrão em PostgreSQL e MySQL), inserções esperam valores próximos uns dos outros — assim novas chaves caem em páginas adjacentes, com poucas reorganizações.

Com UUID v4, cada INSERT cai numa página aleatória do índice. Resultado:

  • Page splits frequentes: páginas cheias precisam ser divididas para acomodar nova chave.
  • Cache miss: páginas aleatórias não estão no buffer pool, força I/O em disco.
  • Fragmentação: índice cresce desproporcionalmente em relação aos dados.
  • Vacuum/REINDEX mais frequentes em Postgres.

Em números

Em tabelas com 10M+ de linhas e alta taxa de escrita, UUID v4 como PK pode ter INSERT 30–50% mais lento que BIGINT auto-incremento. O índice fica 2× maior que o necessário e cache do banco vira ineficiente.

UUID v7 resolve o problema

UUID v7 (RFC 9562, ratificado em 2024) tem estrutura completamente diferente:

UUID v7 (128 bits = 16 bytes): ┌────────────────────────┬──────────┬────────────────────────────┐ │ 48 bits — timestamp ms │ 12 bits │ 62 bits — random + version │ │ desde Unix epoch │ rand_a │ + variant │ └────────────────────────┴──────────┴────────────────────────────┘ Exemplo: 018f4e8e-3a2b-7000-8c5d-2a8f3b1c4e5f └─── timestamp ─────┘ │ └── version 7 Como começa com timestamp, dois UUIDs criados em sequência ficam próximos no espaço lexicográfico → ideal para índice B-tree.

Resultado prático: UUID v7 tem performance de INSERT equivalente a BIGINT auto-incremento, mantendo todos os benefícios de geração descentralizada. Veja comparativo completo entre UUID v4 e v7.

Implementação em PostgreSQL

Schema com UUID v4 (não recomendado em alta escala)

-- pgcrypto vem ativado em Postgres 14+ CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4 email TEXT NOT NULL UNIQUE, name TEXT, created_at TIMESTAMPTZ DEFAULT now() ); INSERT INTO users (email, name) VALUES ('a@b.com', 'Ana');

Schema com UUID v7 (recomendado)

-- Postgres 18+ terá uuidv7() nativa. -- Em versões anteriores, gere no app (Node, Python, Go) e insira: CREATE TABLE users ( id UUID PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT, created_at TIMESTAMPTZ DEFAULT now() ); -- Com lib npm uuid (^9.0): INSERT INTO users (id, email, name) VALUES ('018f4e8e-3a2b-7000-8c5d-2a8f3b1c4e5f', 'a@b.com', 'Ana');

Implementação em MySQL

MySQL não tem tipo UUID nativo. As escolhas práticas:

-- ❌ Pior opção: CHAR(36) — 36 bytes por linha + hífens CREATE TABLE users ( id CHAR(36) PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE ); -- ✅ Melhor: BINARY(16) — 16 bytes (55% menos) CREATE TABLE users ( id BINARY(16) PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE ); -- Inserção com conversão (UUID v7 gerado no app) INSERT INTO users (id, email) VALUES (UNHEX(REPLACE('018f4e8e-3a2b-7000-8c5d-2a8f3b1c4e5f', '-', '')), 'a@b.com'); -- Leitura formatada de volta SELECT LOWER(CONCAT_WS('-', SUBSTR(HEX(id), 1, 8), SUBSTR(HEX(id), 9, 4), SUBSTR(HEX(id), 13, 4), SUBSTR(HEX(id), 17, 4), SUBSTR(HEX(id), 21, 12) )) AS id_str, email FROM users;

Geração de UUID v7 nas linguagens

Como gerar UUID v7 nas principais linguagens (2026)
CritérioSuporte nativo / lib
Node.js / TypeScriptnpm install uuid → uuidv7()
Pythonpip install uuid7 ou uuid.uuid7() em Python 3.13+
Gogithub.com/google/uuid → uuid.NewV7()
JavaUUID.randomUUID() é v4; use libs como uuid-creator para v7
Rustcrate uuid → Uuid::now_v7()
PHPcomposer require ramsey/uuid → Uuid::uuid7()
Rubygem 'uuid7'

Alternativas modernas ao UUID

Para cenários específicos, outras estruturas são interessantes:

UUID v7, ULID, NanoID, CUID2, Snowflake comparados
CritérioTamanhoOrdenado por tempoQuando usar
UUID v716 bytes (36 chars)SimPadrão moderno; melhor escolha em 2026
ULID16 bytes (26 chars Base32)SimQuando quer encoding compacto e URL-friendly
NanoID~21 chars (configurável)NãoURLs curtas, secret tokens, slugs únicos
CUID224 charsNãoWeb apps, resistente a colisão e seguro
Snowflake (Twitter)8 bytes (BIGINT)SimUltra-escala, quando 16 bytes incomoda
BIGINT auto-inc8 bytesSimSingle-instance, simples, sem URLs públicas

Trade-offs além de performance

  • Espaço em disco: UUID (16 bytes) ocupa 2× mais que BIGINT (8 bytes). Em tabela com 100M de linhas, são ~800 MB extra só na PK + índices.
  • Cache: índices maiores cabem menos no buffer pool, podendo causar mais I/O em disco.
  • Joins: joins entre UUIDs são levemente mais caros que entre BIGINTs (mais bytes para comparar). Em queries com muitos joins, importa.
  • Debug e logs: ler 018f4e8e-3a2b-7000... em log é mais difícil que ler “42”. Considere ferramentas que decodifiquem.
  • Migração: migrar tabela grande de BIGINT para UUID é caro — planeje cedo.

Quando UUID NÃO é a melhor escolha

Casos onde BIGINT vence

  • App single-instance, single-region, sem necessidade de geração descentralizada.
  • Tabelas internas/auxiliares que nunca aparecem em URL pública.
  • Sistemas com restrição extrema de espaço em disco ou RAM.
  • Joins muito frequentes em queries de alta concorrência.
  • Quando legibilidade do ID é importante para suporte e operação.

Padrão híbrido: BIGINT interno + UUID público

Quando você precisa do melhor dos dois mundos: PK BIGINT interno + UUID público para URLs e APIs:

CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, -- interno, joins rápidos public_id UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(), -- exposto em URLs email TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX ON users (public_id); -- API expõe public_id; código interno usa id. -- Ganha performance interna + opacidade externa.

Erros comuns ao adotar UUID como PK

Anti-padrões

  • UUID v4 em tabela grande de alta escrita: fragmentação destrói performance. Use v7.
  • CHAR(36) em MySQL: 55% mais espaço sem benefício. Use BINARY(16).
  • Esquecer índice em FK que aponta para UUID: joins ficam dolorosos sem índice na coluna FK.
  • Confiar só em UUID para autorização: ID opaco é proteção em camadas, não substituição de checagem de permissão.
  • Misturar UUID v4 e v7 no mesmo schema: perde os benefícios de ordenação. Padronize.

Decision tree: qual usar

Você precisa expor o ID em URL pública? ├─ Não → BIGINT auto-incremento (mais simples) └─ Sim │ ├─ Single-instance, baixo volume? │ └─ Padrão híbrido (BIGINT + UUID exposto) │ └─ Distribuído, multi-instância, ou volume alto? ├─ Tabela com muito INSERT? │ └─ UUID v7 (preserva performance B-tree) │ ├─ Encoding compacto importa? (URLs curtas) │ └─ ULID ou NanoID │ └─ Ultra-escala (>100M/dia)? └─ Snowflake ID (cabe em BIGINT)

Checklist para adotar UUID como PK

  • ✅ Justificativa clara: geração descentralizada, IDs opacos, multi-região.
  • ✅ Versão correta: UUID v7 para projetos novos.
  • ✅ PostgreSQL: tipo UUID nativo. MySQL: BINARY(16).
  • ✅ Índices em todas as FKs que apontam para UUIDs.
  • ✅ Padrão consistente em todo o schema (não misture v4 e v7).
  • ✅ Considerou padrão híbrido (BIGINT interno + UUID público) se faz sentido.
  • ✅ Benchmark de INSERT/SELECT antes de produção em tabelas críticas.
  • ✅ Logs e ferramentas de suporte preparadas para IDs longos.
  • ✅ Backup/restore testado com UUID em vez de BIGINT.

Perguntas frequentes

UUID como chave primária vale a pena?+

Vale para sistemas distribuídos, multi-instância e arquiteturas onde IDs precisam ser gerados sem round-trip ao banco. Não vale em sistemas pequenos, single-instance, onde BIGINT auto-incremento é mais simples e rápido. A regra prática: comece com BIGINT em projetos pequenos; vá para UUID v7 quando precisar de geração descentralizada ou IDs opacos para o usuário final.

Qual a diferença entre UUID v4 e UUID v7 como PK?+

UUID v4 é totalmente aleatório — péssimo para índices B-tree, causa page splits frequentes e fragmenta o índice. UUID v7 começa com 48 bits de timestamp Unix em milissegundos, gerando UUIDs sequencialmente crescentes — comportamento ideal para B-tree, performance comparável a BIGINT auto-incremento em INSERT.

Por que UUID v4 prejudica performance de índice?+

O índice B-tree espera valores ordenados ou semi-ordenados para inserção eficiente. Com UUID v4 aleatório, cada INSERT cai numa página aleatória do índice, forçando page splits e reorganizações constantes. Em tabelas grandes (1M+ linhas) com alto volume de escrita, INSERT pode ser 30–50% mais lento que com BIGINT auto-incremento.

ULID é melhor que UUID v7?+

Tecnicamente são equivalentes na propriedade de ordenação temporal — ambos começam com timestamp. ULID tem encoding mais compacto (26 caracteres em Crockford Base32, sem hífen) e é amigável para URLs. UUID v7 segue o padrão RFC 9562 e tem suporte crescente em libs nativas (PostgreSQL, drivers, frameworks). Para projetos novos, prefira UUID v7 pelo padrão; ULID continua sendo opção sólida.

Devo armazenar UUID como CHAR(36) ou BINARY(16)?+

Depende do banco. PostgreSQL tem tipo UUID nativo (16 bytes binários internamente, exibe como string) — use sempre. MySQL/MariaDB não tem tipo UUID nativo: armazenar como CHAR(36) gasta 36 bytes; como BINARY(16) gasta 16 bytes (50% menos espaço, índice mais rápido). Para MySQL, BINARY(16) é a melhor escolha técnica, embora exija conversão na aplicação.

UUID em URL pública é seguro?+

É um dos maiores benefícios do UUID. Diferente de IDs sequenciais (que vazam volume de dados e permitem enumeração de URLs), UUIDs são opacos: do /usuario/12345 atacantes podem chutar /usuario/12346; de /usuario/018f4e8e-3a2b-7000-8c5d-2a8f3b1c4e5f, não. Mas UUID não substitui autorização — sempre valide no servidor se o usuário tem permissão para acessar o recurso.

Posso usar UUID v7 como PK no MySQL 8?+

Sim. MySQL 8 suporta funções como UUID() (retorna v1) e tem boa performance em UUID armazenado como BINARY(16). Para UUID v7 especificamente, gere na aplicação (libs em todas as linguagens) e insira como BINARY(16). MySQL 8.4+ tem proposta de suporte nativo a UUID v7 mas ainda não é padrão.

Snowflake ID é uma alternativa válida?+

Sim, especialmente em sistemas de altíssima escala. Snowflake (criado pelo Twitter, hoje X) é 64 bits ordenado por tempo: 41 bits timestamp + 10 bits machine ID + 12 bits sequence. Vantagem: cabe em BIGINT, é menor que UUID. Desvantagem: precisa de coordenação para alocar machine IDs. Discord e Twitter/X usam em produção. Para a maioria dos casos web, UUID v7 é mais simples.

#uuid#uuid v7#chave primária#banco de dados#postgresql#mysql#ulid#snowflake#performance#primary key

Continue lendo