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