Chave primária natural ou surrogate key
Chave primária natural ou surrogate key: veja conceitos essenciais, boas práticas e exemplos fáceis de adaptar aos seus projetos.
A migração parecia trivial: a tabela de clientes usava o CPF como chave primária, porque "CPF é único e não muda nunca". Aí entrou um cliente cujo CPF tinha sido digitado errado no cadastro de 2019, e corrigir um único dígito significava atualizar a primary key em sete tabelas com foreign key apontando pra ela. O que era pra ser um UPDATE de um campo virou uma cirurgia de banco de dados de um fim de semana inteiro. O CPF não mudou — o erro de digitação mudou, e isso bastou.
Este post é sobre a decisão entre chave primária natural e surrogate key, e por que ela quase sempre tem uma resposta. Antes, se você ainda está estruturando o schema do zero, vale ler como modelar tabelas, porque essa escolha é uma das primeiras que define se o resto vai doer ou não.
Chave natural parece imutável até deixar de ser
Chave natural é usar um dado de negócio que já existe como primary key: CPF, e-mail, código de produto, ISBN. A sedução é óbvia — esse dado já é único, então por que criar uma coluna a mais? O problema é que "imutável e único" é uma promessa que o mundo real adora quebrar.
- CPF digitado errado: o valor existe, é válido sintaticamente, mas está errado. Corrigir vira propagação em cascata por todas as foreign keys.
- E-mail muda: a pessoa troca de provedor, casa e muda o sobrenome, ou simplesmente quer outro endereço. Se o e-mail é PK, cada mudança é uma migração.
- Código de produto que "nunca muda": até a empresa ser adquirida e padronizar a nomenclatura com a da matriz.
O padrão é sempre o mesmo: o atributo de negócio era estável o suficiente até o dia em que não foi, e nesse dia o custo de ter ancorado a identidade da linha nele aparece de uma vez. Identidade de registro e valor de negócio são coisas diferentes, e misturar as duas é a raiz do problema.
Surrogate key: IDENTITY ou UUID
Surrogate key é uma coluna sem significado de negócio, criada só pra identificar a linha. Ela nunca muda porque não representa nada que possa mudar. Existem duas famílias:
-- IDENTITY (auto-incremento), Postgres 10+
CREATE TABLE cliente (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
cpf CHAR(11) NOT NULL,
email TEXT NOT NULL
);
-- UUID
CREATE TABLE cliente (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cpf CHAR(11) NOT NULL,
email TEXT NOT NULL
);
IDENTITY (o sucessor moderno do SERIAL no Postgres) gera inteiros sequenciais. São compactos, ótimos pra índice e fáceis de ler em debug. A desvantagem: são previsíveis (dá pra adivinhar /pedido/124 a partir de /pedido/123) e exigem um round-trip ao banco pra saber o valor antes de inserir registros relacionados.
UUID resolve esses dois pontos — é gerável no cliente antes do insert e não é enumerável — ao custo de 16 bytes em vez de 8 e de uma característica importante de performance que merece seção própria.
UUID v4 versus v7 em tabelas com muita escrita
Aqui mora um detalhe que derruba performance de tabela grande e quase ninguém comenta. UUID v4 é completamente aleatório. Quando ele é a primary key e o índice é uma B-tree (o padrão), cada insert cai numa posição aleatória do índice, fragmentando as páginas e destruindo a localidade. Em tabela com muita escrita, isso vira page split constante e cache miss.
UUID v7 corrige isso embutindo um timestamp nos bits mais significativos, então valores gerados em sequência ficam próximos no índice — o melhor dos dois mundos: não-enumerável o suficiente pra URL pública, mas ordenado por tempo pra não detonar o índice.
-- Postgres 18 trouxe uuidv7() nativo; antes, use uma extensão ou gere na app
CREATE TABLE evento (
id UUID PRIMARY KEY DEFAULT uuidv7(),
payload JSONB NOT NULL,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
Se você precisa gerar UUIDs pra testar um schema ou semear dados, um gerador de UUID produz valores v4 e v7 na hora, sem precisar abrir um console SQL. A regra prática pra escolher: tabela com escrita intensa e índice B-tree, prefira v7; v4 só quando a aleatoriedade pura for requisito de segurança e o volume de escrita for baixo.
A natural ainda importa — como UNIQUE constraint
Trocar a primary key por surrogate não significa jogar a chave natural no lixo. Pelo contrário: o CPF ainda precisa ser único, o e-mail ainda não pode duplicar. A diferença é que essa garantia vira uma UNIQUE constraint, não a identidade da linha.
CREATE TABLE cliente (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
cpf CHAR(11) NOT NULL,
email TEXT NOT NULL,
CONSTRAINT uq_cliente_cpf UNIQUE (cpf),
CONSTRAINT uq_cliente_email UNIQUE (email)
);
Assim você tem o melhor dos dois lados: a integridade do dado de negócio é garantida pelo banco, mas a identidade do registro está ancorada numa coluna que nunca muda. Corrigir um CPF digitado errado vira um UPDATE cliente SET cpf = ... WHERE id = ... que não toca em nenhuma foreign key. O problema do começo do post simplesmente não existe.
A minha posição: surrogate PK, natural UNIQUE
Depois de já ter pago o preço da migração de fim de semana, minha regra é direta e quase sem exceção: primary key sempre surrogate, chave natural sempre como UNIQUE constraint. A surrogate dá estabilidade de identidade e foreign keys baratas; o UNIQUE dá a integridade de negócio. Você não abre mão de nada.
Entre IDENTITY e UUID, decido pelo contexto: sistema fechado, IDs internos que não vazam em URL pública, escrita moderada, IDENTITY pela compactação e legibilidade. Sistema distribuído, IDs em URL pública, geração no cliente, ou múltiplas fontes inserindo, UUID v7. UUID v4 fica reservado pros casos raros em que a não-previsibilidade total é requisito de segurança e a escrita é baixa. O erro a evitar é o oposto da escolha: ancorar identidade num dado de negócio porque "ele nunca vai mudar". Vai.
Perguntas frequentes
Quando faz sentido usar chave natural como primary key?
Quase nunca em sistemas transacionais. A exceção razoável são tabelas de lookup estáveis e pequenas, como uma tabela de códigos de país (ISO), onde o valor é controlado por um padrão externo e dificilmente muda. Mesmo aí, surrogate + UNIQUE costuma ser mais seguro.
IDENTITY ou UUID, qual escolher?
IDENTITY pra IDs internos, escrita moderada e quando legibilidade e compactação importam. UUID pra sistemas distribuídos, IDs expostos em URL pública ou geração no cliente. Se for UUID em tabela com muita escrita, use v7 e não v4.
Por que UUID v4 é ruim para primary key de tabela grande?
Porque é aleatório e fragmenta o índice B-tree a cada insert, causando page splits e cache misses em volume alto de escrita. UUID v7 embute um timestamp e mantém os valores próximos no índice, eliminando esse problema.
Posso ter chave natural e surrogate ao mesmo tempo?
Sim, e é a recomendação. A surrogate é a primary key e ancora a identidade; a natural vira uma UNIQUE constraint que garante a integridade do dado de negócio. Você ganha estabilidade e integridade sem escolher entre as duas.
O que levar daqui
A escolha entre chave natural e surrogate tem uma resposta que vale pra esmagadora maioria dos casos: surrogate como primary key, natural como UNIQUE constraint. Dados de negócio que parecem imutáveis acabam mudando, e ancorar a identidade da linha neles é comprar uma migração futura. Use IDENTITY pra IDs internos e UUID v7 pra tabelas com muita escrita ou IDs públicos, mantenha o UNIQUE sobre o CPF e o e-mail, e durma tranquilo sabendo que corrigir um dígito errado é só um UPDATE.
- 01 Nubank Croma: o que vem no plano, quanto custa e pra quem realmente compensa O Nubank lançou o Croma, um plano de média renda entre o cartão gratuito e o Ultravioleta. Veja o que está incluído, o cashback real, os R$ 39 de mensalidade (e como zerar) e faça a conta antes de aderir.
- 02 Como importar CSV sem corromper caracteres: encoding, BOM e delimitadores UTF-8, latin1, cp1252, BOM — por que seus acentos aparecem como lixo e como corrigir de uma vez, no Excel, Python e linha de comando.