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.
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
| Critério | BIGINT (auto-inc) | UUID v4 | UUID v7 |
|---|---|---|---|
| Tamanho | 8 bytes | 16 bytes | 16 bytes |
| Geração descentralizada | ❌ | ✅ | ✅ |
| Performance INSERT em tabela grande | Excelente | Ruim (fragmentação) | Excelente |
| Ordenação por criação | Sim | Não | Sim (timestamp) |
| Ocupa espaço no índice | Mínimo | Maior | Maior |
| Revela volume de dados | Sim | Não | Parcialmente (timestamp) |
| Seguro em URL pública | Não | Sim | Sim |
| Suporte nativo Postgres | Sim | Sim | Sim (em v18+) |
| Suporte nativo MySQL | Sim | Sim | Nã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
| Critério | Suporte nativo / lib |
|---|---|
| Node.js / TypeScript | npm install uuid → uuidv7() |
| Python | pip install uuid7 ou uuid.uuid7() em Python 3.13+ |
| Go | github.com/google/uuid → uuid.NewV7() |
| Java | UUID.randomUUID() é v4; use libs como uuid-creator para v7 |
| Rust | crate uuid → Uuid::now_v7() |
| PHP | composer require ramsey/uuid → Uuid::uuid7() |
| Ruby | gem 'uuid7' |
Alternativas modernas ao UUID
Para cenários específicos, outras estruturas são interessantes:
| Critério | Tamanho | Ordenado por tempo | Quando usar |
|---|---|---|---|
| UUID v7 | 16 bytes (36 chars) | Sim | Padrão moderno; melhor escolha em 2026 |
| ULID | 16 bytes (26 chars Base32) | Sim | Quando quer encoding compacto e URL-friendly |
| NanoID | ~21 chars (configurável) | Não | URLs curtas, secret tokens, slugs únicos |
| CUID2 | 24 chars | Não | Web apps, resistente a colisão e seguro |
| Snowflake (Twitter) | 8 bytes (BIGINT) | Sim | Ultra-escala, quando 16 bytes incomoda |
| BIGINT auto-inc | 8 bytes | Sim | Single-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.
Continue lendo
O que é UUID? Guia Completo com Exemplos
Entenda o que são UUIDs, os diferentes tipos (v1, v4, v7) e quando usar cada um em seus projetos.
UUID v4 vs UUID v7: Qual Usar no Seu Projeto?
Comparação completa entre UUID v4 e UUID v7: diferenças de performance, ordenação, uso em banco de dados e quando escolher cada versão.
DATETIME vs TIMESTAMP no Banco de Dados (Guia 2026): MySQL e PostgreSQL
Comparativo técnico completo: armazenamento, fuso horário, range, tamanho, comportamento em mudanças de TZ, TIMESTAMPTZ, problema 2038 e migração.
Performance SQL com Índices: Guia Completo 2026 (PostgreSQL e MySQL)
Aprenda a usar índices em SQL para acelerar queries de segundos para milissegundos. Tipos de índice (B-tree, GIN, GiST, BRIN), índices compostos, parciais, funcionais, EXPLAIN ANALYZE, anti-padrões e checklist de manutenção.