Logo

Chave Primária no MySQL: Natural, Composta ou Substituta? O Tradeoff Que Define o Futuro do Seu Banco

Uma decisão tomada em cinco minutos no desenho do schema pode custar meses de retrabalho anos depois. Entenda os tradeoffs entre performance e consistência antes de escolher.

Publicado em 1 de outubro de 2026

Toda tabela precisa de uma primary key, mas poucas decisões de modelagem geram tanto retrabalho silencioso quanto a escolha errada de qual coluna (ou conjunto de colunas) deve ocupar esse papel. É comum encontrar, em bancos legados, tabelas com chave composta por marca e modelo, ou chaves naturais baseadas em CPF e CNPJ, que pareciam fazer sentido no primeiro dia de projeto. O problema aparece meses ou anos depois, quando esses valores precisam mudar — uma marca é renomeada, um modelo é descontinuado e recriado, um CPF é corrigido pela Receita Federal — e a decisão de design se transforma em uma cascata de UPDATEs arriscados, chaves estrangeiras órfãs e, nos piores casos, inconsistência silenciosa nos dados. Neste artigo, vamos comparar as três estratégias de primary key mais comuns no MySQL — chave natural, chave composta e chave substituta (surrogate key) — sob a ótica de performance e consistência, e indicar quando cada uma faz sentido.

Por Que Essa Escolha Importa Tanto no MySQL

No MySQL, quando a tabela usa o mecanismo de armazenamento InnoDB (o padrão), a primary key não é apenas uma restrição de unicidade: ela é o índice clusterizado da tabela. Isso significa duas coisas importantes:

  • As linhas são fisicamente armazenadas no disco na ordem da primary key. Inserções com valores sequenciais (como um AUTO_INCREMENT) acontecem sempre no final da estrutura em árvore B+, de forma rápida e sem reorganização. Já inserções com valores não sequenciais ou aleatórios (como um CPF ou um UUID aleatório) caem em posições intermediárias da árvore, forçando divisões de página (page splits) e fragmentação, que tornam inserções e leituras mais lentas ao longo do tempo.
  • Todo índice secundário da tabela armazena, internamente, uma cópia do valor da primary key para localizar a linha correspondente no índice clusterizado. Ou seja: quanto maior e mais complexa for a primary key (uma string longa, ou várias colunas combinadas), maior e mais lento fica cada índice secundário da tabela — não apenas a própria chave primária.

Com isso em mente, a escolha da primary key deixa de ser um detalhe estético do schema e passa a afetar diretamente o tamanho em disco, o uso de memória do buffer pool e a velocidade de praticamente todas as consultas da tabela.

As Três Estratégias Possíveis

1. Chave natural — usa uma coluna que já existe no domínio do negócio e que, em teoria, identifica a entidade de forma única: CPF e CNPJ para pessoas e empresas, ISBN para livros, placa para veículos.

  • A favor: elimina a necessidade de um join extra quando a busca é feita justamente por esse identificador; comunica significado de negócio só de olhar a chave.
  • Contra: identificadores "únicos" do mundo real raramente são tão imutáveis quanto parecem — CPFs podem ser corrigidos por erro de cadastro, CNPJs mudam em fusões e cisões societárias, placas de veículo são reemitidas. Além disso, são tipicamente armazenados como CHAR/VARCHAR, mais pesados que um inteiro em todo índice secundário da tabela.

2. Chave composta — combina duas ou mais colunas de negócio para formar a unicidade, como (marca, modelo) em um catálogo de produtos ou (ano, placa) em um controle de frotas.

  • A favor: parece natural quando a unicidade do mundo real realmente depende da combinação de atributos; evita uma coluna "artificial" adicional.
  • Contra: qualquer tabela filha que referencie essa chave como foreign key precisa carregar todas as colunas da composição — não apenas uma. Se um dos valores da composição muda (a marca é renomeada, por exemplo), a mudança precisa se propagar para cada tabela filha, o que é lento, arriscado e, em tabelas grandes, pode travar o sistema durante a atualização.

3. Chave substituta (surrogate key) — um identificador artificial, sem significado de negócio, gerado exclusivamente para o papel de identidade única da linha. No MySQL, normalmente um INT/BIGINT UNSIGNED AUTO_INCREMENT, ou mais raramente um UUID.

  • A favor: é pequena (4 ou 8 bytes), sequencial (inserções rápidas, sem fragmentação) e, o mais importante, nunca precisa mudar mesmo que os atributos de negócio da linha mudem. A identidade da linha fica desacoplada dos dados que ela representa.
  • Contra: não impede, sozinha, que o mesmo CPF ou a mesma combinação marca/modelo seja cadastrada duas vezes — é necessário reforçar a regra de negócio com uma constraint UNIQUE separada sobre as colunas naturais.

O Impacto na Performance

Na prática, a diferença de performance entre as três abordagens costuma aparecer em três frentes:

Tamanho dos índices secundários: como todo índice secundário carrega uma cópia da primary key, uma chave composta por duas colunas VARCHAR (ou uma chave natural como CNPJ) pode facilmente dobrar ou triplicar o tamanho de cada índice adicional da tabela, comparado a um simples BIGINT. Em tabelas grandes, isso se traduz em mais I/O em disco e menos linhas cabendo no buffer pool.

Localidade das inserções: um AUTO_INCREMENT garante que novas linhas sejam sempre inseridas no final do índice clusterizado — rápido e previsível. Já CPFs, CNPJs ou UUIDs aleatórios (UUID() gerado sem ordenação temporal) inserem em posições aleatórias da árvore, causando divisões de página constantes. Se o projeto realmente precisar de um identificador globalmente único e não sequencial, vale considerar variantes ordenadas por tempo (como UUID v7) e armazená-las como BINARY(16) em vez de CHAR(36), usando UUID_TO_BIN()/BIN_TO_UUID() para reduzir o tamanho e preservar parte da sequencialidade.

-- Evite: UUID armazenado como texto, grande e com inserção aleatória
CREATE TABLE pedidos_ruim (
  id CHAR(36) NOT NULL PRIMARY KEY,
  ...
);

-- Melhor, quando um identificador não sequencial for realmente necessário
CREATE TABLE pedidos_melhor (
  id BINARY(16) NOT NULL PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID(), 1)),
  ...
);

Custo de comparação: comparar dois inteiros é mais barato, para o otimizador e a CPU, do que comparar strings (que ainda dependem de collation). Em joins pesados, envolvendo milhões de linhas, essa diferença se acumula.

O Impacto em Foreign Keys e Relacionamentos

É aqui que o custo de uma chave composta ou natural mal escolhida costuma aparecer com mais força, geralmente tarde demais. Considere um catálogo de produtos com chave composta (marca, modelo):

-- Chave composta como PK: toda tabela filha herda as duas colunas
CREATE TABLE produtos (
  marca VARCHAR(50) NOT NULL,
  modelo VARCHAR(50) NOT NULL,
  descricao VARCHAR(255),
  PRIMARY KEY (marca, modelo)
);

CREATE TABLE estoque (
  marca VARCHAR(50) NOT NULL,
  modelo VARCHAR(50) NOT NULL,
  quantidade INT NOT NULL,
  FOREIGN KEY (marca, modelo) REFERENCES produtos(marca, modelo)
);

-- Se a marca for renomeada, a mudança precisa se propagar
-- (via ON UPDATE CASCADE ou manualmente) para TODAS as tabelas filhas,
-- em uma operação potencialmente longa e bloqueante.
UPDATE produtos SET marca = 'Nova Marca S.A.' WHERE marca = 'Marca Antiga Ltda';

Compare com o equivalente usando chave substituta, onde a mudança de nome da marca fica isolada em uma única linha de uma única tabela, sem qualquer efeito cascata sobre quem referencia o produto:

-- Chave substituta como PK, com unicidade de negócio garantida à parte
CREATE TABLE produtos (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  marca VARCHAR(50) NOT NULL,
  modelo VARCHAR(50) NOT NULL,
  descricao VARCHAR(255),
  UNIQUE KEY uq_produtos_marca_modelo (marca, modelo)
);

CREATE TABLE estoque (
  produto_id BIGINT UNSIGNED NOT NULL,
  quantidade INT NOT NULL,
  FOREIGN KEY (produto_id) REFERENCES produtos(id)
);

-- Renomear a marca agora é uma mudança local, sem cascata:
UPDATE produtos SET marca = 'Nova Marca S.A.' WHERE id = 42;

O mesmo raciocínio vale para chaves naturais como CPF ou CNPJ: usá-las apenas como coluna UNIQUE, e não como primary key, garante a mesma regra de integridade de negócio (não permitir duplicidade) sem amarrar a identidade técnica da linha a um valor que, na prática, pode precisar ser corrigido.

Há uma exceção importante a essa recomendação: tabelas de associação (muitos-para-muitos), como uma tabela pedido_produto que relaciona pedidos e produtos. Nesse caso, a chave composta é formada pelas próprias foreign keys — que já são substitutas e estáveis — e não por atributos de negócio sujeitos a mudança. Usar PRIMARY KEY (pedido_id, produto_id) nesse cenário é uma prática sólida e recomendada.

O Impacto na Consistência dos Dados

Além da performance, existe uma dimensão de consistência que costuma pesar ainda mais a longo prazo:

  • Mutabilidade disfarçada de imutabilidade: é tentador tratar CPF, CNPJ ou um código de modelo como "eternos", mas erros de cadastro, fraudes identificadas, fusões societárias e reestruturações de catálogo acontecem. Quando esses valores são a própria primary key, corrigi-los exige recriar a identidade da linha (e de tudo que a referencia), em vez de simplesmente atualizar um atributo.
  • Histórico e auditoria: sistemas que precisam manter histórico (quem comprou o quê, quando um contrato foi assinado) se beneficiam de uma identidade de linha que nunca muda, mesmo que os atributos de negócio associados a ela sejam corrigidos ou atualizados. A chave substituta preserva essa estabilidade; a chave natural ou composta, não.
  • Duplicidade por formatação: chaves naturais como CPF frequentemente aparecem em formatos diferentes entre sistemas integrados (com ou sem pontuação, com ou sem zeros à esquerda). Sem uma normalização rigorosa na entrada, isso pode gerar duplicidade lógica mesmo com uma constraint de unicidade tecnicamente correta.
  • Regra de negócio preservada: adotar uma chave substituta não significa abrir mão da integridade — a constraint UNIQUE sobre a(s) coluna(s) de negócio continua garantindo, no nível do banco, que duas linhas não representem a mesma entidade real.

Guia Rápido: Qual Abordagem Escolher

Cenário Recomendação
Tabelas transacionais de alto volume (pedidos, logs, eventos) Chave substituta (BIGINT UNSIGNED AUTO_INCREMENT)
Entidades com documentos legais (clientes com CPF/CNPJ) Chave substituta como PK + UNIQUE sobre o documento
Atributos de negócio sujeitos a mudança (marca, modelo, categoria) Chave substituta como PK + UNIQUE composto sobre os atributos
Tabelas de referência pequenas e verdadeiramente estáveis (código de país ISO, moeda ISO) Chave natural é aceitável — o valor raramente muda e é curto
Tabelas de associação N:N entre entidades que já usam chave substituta Chave composta pelas foreign keys substitutas (ex.: pedido_id + produto_id)
Identificador precisa ser gerado fora do banco ou em múltiplos nós (sistemas distribuídos) UUID ordenado por tempo (v7) armazenado como BINARY(16)

Conclusão

Na grande maioria dos projetos, a combinação mais segura — tanto para performance quanto para consistência — é usar uma chave substituta compacta (BIGINT UNSIGNED AUTO_INCREMENT) como primary key, e reforçar as regras de unicidade do negócio com constraints UNIQUE sobre as colunas naturais ou compostas que realmente importam para a aplicação. Essa abordagem mantém os índices da tabela pequenos e as inserções rápidas, ao mesmo tempo em que isola a identidade técnica de cada linha de qualquer mudança futura nos dados de negócio — uma mudança de marca, a correção de um CPF ou a reestruturação de um catálogo deixam de exigir cascatas de atualização arriscadas. As exceções fazem sentido apenas quando o valor "natural" é genuinamente estável e pequeno (como códigos ISO) ou quando a chave composta é formada por foreign keys que já são substitutas, como em tabelas de associação. Se você está em dúvida sobre a modelagem de um schema existente ou em construção, a MySQL Master pode ajudar a avaliar os tradeoffs específicos do seu cenário e planejar uma eventual migração de chave com segurança.

Referências