Chave estrangeira: o que realmente é e como usar sem dor de cabeça
Chave estrangeira banco de dados é a forma que um SGBD oferece para garantir relacionamento entre tabelas. Na prática, é uma coluna (ou conjunto de colunas) em uma tabela que referencia a chave primária de outra. O banco impõe a integridade referencial. Se você tentar inserir um registro com um valor que não existe na tabela referenciada, a operação falha. Simples assim, mas existem detalhes que as pessoas costumam ignorar até receberem um erro no meio de uma implantação. A sintaxe básica em MySQL e PostgreSQL é direta:
Definição ao criar tabela
CREATE TABLE pedidos (
id INT PRIMARY KEY AUTO_INCREMENT,
cliente_id INT NOT NULL,
data_pedido DATE,
FOREIGN KEY (cliente_id) REFERENCES clientes(id)
); Você também pode declarar depois, num bloco separado:
ALTER TABLE pedidos ADD CONSTRAINT fk_pedidos_clientes FOREIGN KEY (cliente_id) REFERENCES clientes(id); O segundo jeito é mais seguro em produção. Assim você consegue ver o DDL inteiro antes de aplicar, verificar índices e validar se não vai travar a tabela durante a execução.
ON DELETE e ON UPDATE
Essa parte é onde a maioria das pessoas estraga deployment. A cláusula ON DELETE define o que acontece quando o registro pai é excluído. As opções mais comuns são CASCADE, SET NULL, RESTRICT e NO ACTION. RESTRICT bloqueia a exclusão se houver filhos. NO ACTION em PostgreSQL é similar, mas a verificação acontece no final da transação, não no momento exato do comando. Essa diferença sutil causou problema real para mim em um projeto onde tínhamos triggers que inseriam registros na tabela filha dentro da mesma transação que deletava o pai. O banco aceitava COM NO ACTION mas rejeitava COM RESTRICT, e eu gastei duas horas debugando o erro antes de entender que o trigger criava os filhos antes do check ser aplicado.
O workaround foi simples: trocar para NO ACTION e garantir que a transação inteira fosse executada com isolamento READ COMMITTED, onde o check só validaria no commit. Se estiver usando MySQL, o comportamento é mais previsível — RESTRICT e NO ACTION se comportam de forma essencialmente igual, mas confirme sempre na documentação da versão específica.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Como funciona na prática
Quando uma chave estrangeira existe, o SGBD cria implicitamente um mecanismo de verificação em cada INSERT, UPDATE e DELETE. Isso tem custo. Em tabelas com milhões de linhas sofrendo UPDATE frequente na chave referenciada, a trava pode serializar operações concorrentes. Eu vi um caso onde um UPDATE batch de uma tabela de clientes com 4 milhões de linhas travava requisições de leitura na tabela pedidos por segundos. A solução foi dividir o UPDATE em lotes de 10 mil linhas com commits intermediários, usando LIMIT e OFFSET controlado, o que reduziu o tempo de bloqueio de 4 segundos para menos de 200 milissegundos por lote. Outro ponto que ninguém menciona com frequência: chave estrangeira não cria índice automaticamente na maioria dos bancos. No MySQL com InnoDB, se você declarar a FK sem índice na coluna referenciada, ele não cria um automaticamente na tabela filha. Você precisa criar explicitamente. Em PostgreSQL, a situação é parecida — FK não gera índice automaticamente. Se a coluna cliente_id na tabela pedidos não tiver índice, cada JOIN e cada consulta de verificação de integridade vai fazer full scan, o que mata a performance rapidamente em tabelas grandes.
Crie o índice assim: CREATE INDEX idx_pedidos_cliente_id ON pedidos(cliente_id);
Cascata versus restritiva
CASCADE é confortável no desenvolvimento. Você deleta um cliente e todos os pedidos somem junto. Parece inteligente até o momento em que você precisa recuperar dados excluídos por engano. SET NULL mantém o pedido mas perde a referência ao cliente. Útil quando você quer manter o histórico mas o cliente foi removido do sistema. Eu recomendo SET NULL apenas para tabelas que realmente precisam preservar o registro por conformidade ou auditoria. Para tudo mais, RESTRICT é mais seguro porque obriga o desenvolvedor a tomar uma decisão consciente sobre o que fazer com os dependentes.
Soft delete e chave estrangeira
Um problema que aparece com frequência é soft delete. Se a tabela clientes usa uma flag is_active para marcar exclusão lógica, a chave estrangeira continua apontando para um registro que tecnicamente "existe" mas não deveria ser consultado. A solução comum é adicionar uma constraint adicional ou usar uma view que filtre registros ativos, mas isso não resolve o problema de fundo. Às vezes o mais adequado é tratar a exclusão lógica no código da aplicação em vez de depender do banco para isso.
Limitações que todo mundo esquece
Chave estrangeira não funciona entre tabelas de esquemas diferentes no MySQL sem configuração extra. No PostgreSQL funciona normalmente, mas em Oracle e SQL Server você precisa de permissões explícitas entre schemas. Se você estiver migando um banco de outro SGBD para MySQL, verifique isso antes, senão vai receber erros silenciosos de integridade quebrada. Tabelas temporárias também não suportam FK. Se você está trabalhando com ETL e precisa relacionar dados temporários, use CTEs ou tabelas permanentes intermediárias. FK em temp table vai falhar sem aviso em alguns bancos e funcionar de forma limitada em outros.
Por fim, chave estrangeira não substitui validação na aplicação. O banco é a última linha de defesa, não a primeira. Sempre valide os dados antes de enviar ao banco, porque a mensagem de erro de integridade referencial que chega ao usuário final é inútil e causa suporte desnecessário.