Programação Por Equipe DevBras

Capa do artigo: CPF como chave primária: por que dá errado e como migrar para um id interno

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 UPDATE numa 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

  1. Chave primária interna. BIGINT UNSIGNED AUTO_INCREMENT resolve para a imensa maioria dos sistemas. Ela é estreita, rápida em índice e nunca precisa mudar.
  2. 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.
  3. UNIQUE no documento, aceitando nulo. No MySQL, um índice único aceita várias linhas com NULL, então quem não tem CPF não trava o cadastro de ninguém.
  4. 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.
  5. Chaves estrangeiras pelo id. matriculas.aluno_id aponta para alunos.id. Corrigir o CPF passa a ser um UPDATE de 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:

  1. Crie o índice único no CPF, para que as chaves estrangeiras atuais continuem com um índice para apontar.
  2. Troque a chave primária pelo novo id.
  3. Em cada tabela filha, crie aluno_id, preencha pelo CPF e crie a chave estrangeira nova.
  4. Faça a aplicação gravar e ler pelo aluno_id. Rode assim por uns dias.
  5. Só então remova a chave estrangeira antiga e a coluna aluno_cpf das 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 UNIQUE no 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 UPDATE de uma linha.

Escolha um cliente de teste e corrija o e-mail ou o documento dele agora: quantas tabelas você precisou tocar?

Avalie este artigo

Referência: http://www.sydney.eti.br/como-descobrir-a-primary-key-chave-primaria-de-uma-table-tabela-de-um-data-base-banco-de-dados/