CPF como chave primária: por que dá errado e como migrar para um id interno
Entenda por que CPF, e-mail e CNPJ não servem como chave primária, desenhe a tabela com id interno e documento único, e migre um banco que já usa o CPF nas relações.
A atendente da academia digitou o CPF de um aluno com dois números trocados. O erro só apareceu três meses depois, na emissão da nota. Corrigir parecia simples, mas o CPF era a chave primária da tabela alunos, e estava copiado em matriculas, pagamentos, presencas e no log de acesso da catraca. Mudar um número virou uma operação em cinco tabelas, com a catraca fora do ar.
A resposta curta: a chave primária deve ser um identificador interno, sem significado fora do sistema, que nunca muda. O CPF, o e-mail ou o CNPJ ficam numa coluna comum, com índice UNIQUE. Assim o documento pode ser corrigido, ficar em branco para quem não tem, ou mudar de formato, e nenhuma relação sofre.
Por que o documento parece uma boa chave e não é
CPF parece perfeito: é único, todo mundo tem, já vem pronto. Cada uma dessas três certezas falha na prática.
- "É único" vale para o documento certo, não para o que foi digitado. Erro de digitação existe, e a correção precisa ser um
UPDATEnuma coluna, não uma cirurgia em todas as tabelas. - "Todo mundo tem" falha no primeiro estrangeiro, no dependente menor cadastrado sem documento ou no cliente que só quer comprar sem se identificar. A chave primária não pode ser nula, então o sistema inventa CPF falso para caber.
- "Já vem pronto" ignora que formato muda. A Receita Federal anunciou o CNPJ com letras para novos cadastros a partir de 2026. Quem guardou CNPJ como número inteiro, e ainda por cima como chave, tem uma migração pela frente.
Há ainda a privacidade. Chave primária vai para toda parte: URL, log, planilha exportada, mensagem de erro. Usar CPF como chave espalha um dado pessoal por lugares que ninguém controla, como o histórico do navegador e o log do proxy. Isso vai contra o princípio de usar o mínimo de dado pessoal de que fala a adequação à LGPD num sistema pequeno.
E tem o tipo. Quem guarda CPF como BIGINT perde o zero à esquerda: "012.345.678-90" vira 1234567890. Documento é texto com dígitos, não número. Ninguém soma dois CPFs. O mesmo raciocínio está em tipo certo da coluna.
Como desenhar a tabela do jeito certo
- Chave primária interna.
BIGINT UNSIGNED AUTO_INCREMENTresolve para a imensa maioria dos sistemas. Ela é estreita, rápida em índice e nunca precisa mudar. - Documento numa coluna própria, como texto.
CHAR(11)para CPF guardado só com dígitos, sem ponto nem traço. A máscara é coisa da tela. UNIQUEno documento, aceitando nulo. No MySQL, um índice único aceita várias linhas comNULL, então quem não tem CPF não trava o cadastro de ninguém.- Validação na aplicação. Dígitos verificadores corretos antes de gravar. O banco garante unicidade; a aplicação garante que o número faz sentido.
- Chaves estrangeiras pelo id.
matriculas.aluno_idaponta paraalunos.id. Corrigir o CPF passa a ser umUPDATEde uma linha.
CREATE TABLE alunos (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
nome VARCHAR(120) NOT NULL,
cpf CHAR(11) NULL,
email VARCHAR(254) NULL,
criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_alunos_cpf (cpf),
UNIQUE KEY uk_alunos_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE matriculas (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
aluno_id BIGINT UNSIGNED NOT NULL,
plano VARCHAR(40) NOT NULL,
inicio DATE NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_matriculas_aluno FOREIGN KEY (aluno_id) REFERENCES alunos (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
A validação dos dígitos verificadores, em PHP:
function cpfValido(string $cpf): bool
{
$cpf = preg_replace('/\D/', '', $cpf);
if (strlen($cpf) !== 11 || preg_match('/^(\d)\1{10}$/', $cpf)) {
return false;
}
for ($t = 9; $t < 11; $t++) {
$soma = 0;
for ($i = 0; $i < $t; $i++) {
$soma += (int) $cpf[$i] * ($t + 1 - $i);
}
if ((int) $cpf[$t] !== ($soma * 10) % 11 % 10) {
return false;
}
}
return true;
}
A segunda checagem do if recusa sequências como "11111111111", que passam na conta dos dígitos mas não são CPF de ninguém.
E o id na URL?
Id sequencial na URL não é falha por si só, desde que cada requisição confira se quem pediu pode ver aquele registro. O problema aparece quando o sistema trata "saber o número" como permissão: aí trocar /alunos/1042 por /alunos/1043 mostra o vizinho. Se você quer um identificador público que não revele quantos clientes tem nem permita adivinhar o próximo, crie uma segunda coluna com um valor aleatório, como um UUID, e use essa na URL. Mas lembre: id difícil de adivinhar não substitui a checagem de permissão.
Como migrar um banco que já usa CPF como chave
A academia não podia parar. A migração foi feita em etapas, cada uma reversível, no espírito de migração em dois passos. Antes de tudo, confirme qual é a chave atual:
SELECT COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = 'academia'
AND TABLE_NAME = 'alunos'
AND CONSTRAINT_NAME = 'PRIMARY';
Depois, com backup testado:
- Crie o índice único no CPF, para que as chaves estrangeiras atuais continuem com um índice para apontar.
- Troque a chave primária pelo novo
id. - Em cada tabela filha, crie
aluno_id, preencha pelo CPF e crie a chave estrangeira nova. - Faça a aplicação gravar e ler pelo
aluno_id. Rode assim por uns dias. - Só então remova a chave estrangeira antiga e a coluna
aluno_cpfdas tabelas filhas.
ALTER TABLE alunos ADD UNIQUE KEY uk_alunos_cpf (cpf);
ALTER TABLE alunos
DROP PRIMARY KEY,
ADD COLUMN id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;
ALTER TABLE matriculas ADD COLUMN aluno_id BIGINT UNSIGNED NULL;
UPDATE matriculas m
JOIN alunos a ON a.cpf = m.aluno_cpf
SET m.aluno_id = a.id;
ALTER TABLE matriculas
MODIFY aluno_id BIGINT UNSIGNED NOT NULL,
ADD CONSTRAINT fk_matriculas_aluno FOREIGN KEY (aluno_id) REFERENCES alunos (id);
Antes do MODIFY ... NOT NULL, rode SELECT COUNT(*) FROM matriculas WHERE aluno_id IS NULL. Se voltar algo diferente de zero, há matrícula apontando para um CPF que não existe em alunos, e isso precisa ser resolvido antes. Ensaie a sequência inteira numa cópia do banco: a troca da chave primária reconstrói a tabela, e o tempo e os bloqueios que isso causa só aparecem com o volume real.
Erros comuns
- CPF ou e-mail como chave primária. Qualquer correção vira alteração em cascata em todas as tabelas relacionadas.
- Guardar documento como número. O zero à esquerda some e o CNPJ com letras não cabe.
- Inventar CPF falso para quem não tem. O dado mente, e o falso de um colide com o verdadeiro de outro.
- Chave interna sem
UNIQUEno documento. O mesmo cliente é cadastrado duas vezes e o histórico se divide. - Tratar id difícil de adivinhar como permissão. Um link vazado dá acesso para sempre.
- Documento na URL. O CPF vai parar no histórico do navegador, no log do servidor e em ferramentas de análise.
Checklist
- Toda tabela principal tem chave primária interna, sem significado de negócio.
- CPF, CNPJ e e-mail ficam em colunas de texto com
UNIQUE. - As chaves estrangeiras apontam para o id, nunca para o documento.
- A aplicação valida os dígitos verificadores antes de gravar.
- Nenhuma URL ou log contém CPF.
- Toda rota que recebe um id confere se o usuário pode ver aquele registro.
- Corrigir o CPF de alguém é um
UPDATEde uma linha.
Escolha um cliente de teste e corrija o e-mail ou o documento dele agora: quantas tabelas você precisou tocar?